Przeglądaj źródła

Add timehseet API

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 rok temu
rodzic
commit
795b67d686
13 zmienionych plików z 251 dodań i 102 usunięć
  1. +10
    -1
      src/app/(main)/home/page.tsx
  2. +4
    -4
      src/app/(main)/layout.tsx
  3. +22
    -14
      src/app/api/projects/actions.ts
  4. +22
    -0
      src/app/api/timesheets/actions.ts
  5. +10
    -0
      src/app/api/timesheets/index.ts
  6. +100
    -52
      src/components/CreateProject/CreateProject.tsx
  7. +1
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  8. +39
    -12
      src/components/TimesheetModal/TimesheetModal.tsx
  9. +6
    -7
      src/components/TimesheetTable/EntryInputTable.tsx
  10. +5
    -3
      src/components/UserWorkspacePage/AssignedProjects.tsx
  11. +11
    -1
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  12. +18
    -3
      src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx
  13. +3
    -4
      src/config/authConfig.ts

+ 10
- 1
src/app/(main)/home/page.tsx Wyświetl plik

@@ -1,15 +1,24 @@
import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import UserWorkspacePage from "@/components/UserWorkspacePage";
import { fetchTimesheets } from "@/app/api/timesheets";
import { authOptions } from "@/config/authConfig";
import { getServerSession } from "next-auth";

export const metadata: Metadata = {
title: "User Workspace",
};

const Home: React.FC = async () => {
const session = await getServerSession(authOptions);
// Get name for caching
const username = session!.user!.name!;

await fetchTimesheets(username);

return (
<I18nProvider namespaces={["home"]}>
<UserWorkspacePage />
<UserWorkspacePage username={username} />
</I18nProvider>
);
};


+ 4
- 4
src/app/(main)/layout.tsx Wyświetl plik

@@ -31,10 +31,10 @@ export default async function MainLayout({
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" },
}}
>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
</Box>
</>
);


+ 22
- 14
src/app/api/projects/actions.ts Wyświetl plik

@@ -1,13 +1,16 @@
"use server";

import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import {
serverFetchJson,
serverFetchWithNoContent,
} from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { Task, TaskGroup } from "../tasks";
import { Customer } from "../customer";
import { revalidateTag } from "next/cache";

export interface CreateProjectInputs {
// Project
// Project
projectId: number | null;
projectDeleted: boolean | null;
projectCode: string;
@@ -67,19 +70,22 @@ export interface PaymentInputs {
}

export interface CreateProjectResponse {
id: number,
name: string,
code: string,
category: string,
team: string,
client: string,
id: number;
name: string;
code: string;
category: string;
team: string;
client: string;
}
export const saveProject = async (data: CreateProjectInputs) => {
const newProject = await serverFetchJson<CreateProjectResponse>(`${BASE_API_URL}/projects/new`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
const newProject = await serverFetchJson<CreateProjectResponse>(
`${BASE_API_URL}/projects/new`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

revalidateTag("projects");
return newProject;
@@ -94,5 +100,7 @@ export const deleteProject = async (id: number) => {
},
);

return project
revalidateTag("projects");
revalidateTag("assignedProjects");
return project;
};

+ 22
- 0
src/app/api/timesheets/actions.ts Wyświetl plik

@@ -1,9 +1,13 @@
"use server";

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { ProjectResult } from "../projects";
import { Task, TaskGroup } from "../tasks";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";

export interface TimeEntry {
id: number;
projectId: ProjectResult["id"];
taskGroupId: TaskGroup["id"];
taskId: Task["id"];
@@ -13,3 +17,21 @@ export interface TimeEntry {
export interface RecordTimesheetInput {
[date: string]: TimeEntry[];
}

export const saveTimesheet = async (
data: RecordTimesheetInput,
username: string,
) => {
const savedRecords = await serverFetchJson<RecordTimesheetInput>(
`${BASE_API_URL}/timesheets/save`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

revalidateTag(`timesheets_${username}`);

return savedRecords;
};

+ 10
- 0
src/app/api/timesheets/index.ts Wyświetl plik

@@ -0,0 +1,10 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import { RecordTimesheetInput } from "./actions";

export const fetchTimesheets = cache(async (username: string) => {
return serverFetchJson<RecordTimesheetInput>(`${BASE_API_URL}/timesheets`, {
next: { tags: [`timesheets_${username}`] },
});
});

+ 100
- 52
src/components/CreateProject/CreateProject.tsx Wyświetl plik

@@ -1,6 +1,6 @@
"use client";

import DoneIcon from '@mui/icons-material/Done'
import DoneIcon from "@mui/icons-material/Done";
import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
@@ -22,7 +22,11 @@ import {
SubmitHandler,
useForm,
} from "react-hook-form";
import { CreateProjectInputs, deleteProject, saveProject } from "@/app/api/projects/actions";
import {
CreateProjectInputs,
deleteProject,
saveProject,
} from "@/app/api/projects/actions";
import { Delete, Error, PlayArrow } from "@mui/icons-material";
import {
BuildingType,
@@ -38,7 +42,12 @@ import { Typography } from "@mui/material";
import { Grade } from "@/app/api/grades";
import { Customer, Subsidiary } from "@/app/api/customer";
import { isEmpty } from "lodash";
import { deleteDialog, errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts";
import {
deleteDialog,
errorDialog,
submitDialog,
successDialog,
} from "../Swal/CustomAlerts";
import dayjs from "dayjs";

export interface Props {
@@ -103,16 +112,15 @@ const CreateProject: React.FC<Props> = ({

const handleDelete = () => {
deleteDialog(async () => {
await deleteProject(formProps.getValues("projectId")!!)
await deleteProject(formProps.getValues("projectId")!);

const clickSuccessDialog = await successDialog("Delete Success", t)
const clickSuccessDialog = await successDialog("Delete Success", t);

if (clickSuccessDialog) {
router.replace("/projects");
}

}, t)
}
}, t);
};

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
@@ -124,51 +132,55 @@ const CreateProject: React.FC<Props> = ({
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
async (data, event) => {
try {
console.log("first")
console.log("first");
setServerError("");

let title = t("Do you want to submit?")
let confirmButtonText = t("Submit")
let successTitle = t("Submit Success")
let errorTitle = t("Submit Fail")
const buttonName = (event?.nativeEvent as any).submitter.name
let title = t("Do you want to submit?");
let confirmButtonText = t("Submit");
let successTitle = t("Submit Success");
let errorTitle = t("Submit Fail");
const buttonName = (event?.nativeEvent as any).submitter.name;

if (buttonName === "start") {
title = t("Do you want to start?")
confirmButtonText = t("Start")
successTitle = t("Start Success")
errorTitle = t("Start Fail")
title = t("Do you want to start?");
confirmButtonText = t("Start");
successTitle = t("Start Success");
errorTitle = t("Start Fail");
} else if (buttonName === "complete") {
title = t("Do you want to complete?")
confirmButtonText = t("Complete")
successTitle = t("Complete Success")
errorTitle = t("Complete Fail")
title = t("Do you want to complete?");
confirmButtonText = t("Complete");
successTitle = t("Complete Success");
errorTitle = t("Complete Fail");
}

submitDialog(async () => {
if (buttonName === "start") {
data.projectActualStart = dayjs().format("YYYY-MM-DD")
} else if (buttonName === "complete") {
data.projectActualEnd = dayjs().format("YYYY-MM-DD")
}
submitDialog(
async () => {
if (buttonName === "start") {
data.projectActualStart = dayjs().format("YYYY-MM-DD");
} else if (buttonName === "complete") {
data.projectActualEnd = dayjs().format("YYYY-MM-DD");
}

const response = await saveProject(data);
const response = await saveProject(data);

if (response.id > 0) {
successDialog(successTitle, t).then(() => {
router.replace("/projects");
})
} else {
errorDialog(errorTitle, t).then(() => {
return false
})
}
}, t, { title: title, confirmButtonText: confirmButtonText })
if (response.id > 0) {
successDialog(successTitle, t).then(() => {
router.replace("/projects");
});
} else {
errorDialog(errorTitle, t).then(() => {
return false;
});
}
},
t,
{ title: title, confirmButtonText: confirmButtonText },
);
} catch (e) {
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t, isEditMode],
[router, t],
);

const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
@@ -196,8 +208,8 @@ const CreateProject: React.FC<Props> = ({
// manhourPercentageByGrade should have a sensible default
manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade)
? grades.reduce((acc, grade) => {
return { ...acc, [grade.id]: 1 / grades.length };
}, {})
return { ...acc, [grade.id]: 1 / grades.length };
}, {})
: defaultInputs?.manhourPercentageByGrade,
},
});
@@ -214,15 +226,42 @@ const CreateProject: React.FC<Props> = ({
>
{isEditMode && !(formProps.getValues("projectDeleted") === true) && (
<Stack direction="row" gap={1}>
{!formProps.getValues("projectActualStart") && <Button name="start" type="submit" variant="contained" startIcon={<PlayArrow />} color="success">
{t("Start Project")}
</Button>}
{formProps.getValues("projectActualStart") && !formProps.getValues("projectActualEnd") && <Button name="complete" type="submit" variant="contained" startIcon={<DoneIcon />} color="info">
{t("Complete Project")}
</Button>}
{!(formProps.getValues("projectActualStart") && formProps.getValues("projectActualEnd")) && <Button variant="outlined" startIcon={<Delete />} color="error" onClick={handleDelete}>
{t("Delete Project")}
</Button>}
{!formProps.getValues("projectActualStart") && (
<Button
name="start"
type="submit"
variant="contained"
startIcon={<PlayArrow />}
color="success"
>
{t("Start Project")}
</Button>
)}
{formProps.getValues("projectActualStart") &&
!formProps.getValues("projectActualEnd") && (
<Button
name="complete"
type="submit"
variant="contained"
startIcon={<DoneIcon />}
color="info"
>
{t("Complete Project")}
</Button>
)}
{!(
formProps.getValues("projectActualStart") &&
formProps.getValues("projectActualEnd")
) && (
<Button
variant="outlined"
startIcon={<Delete />}
color="error"
onClick={handleDelete}
>
{t("Delete Project")}
</Button>
)}
</Stack>
)}
<Tabs
@@ -290,7 +329,16 @@ const CreateProject: React.FC<Props> = ({
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit" disabled={formProps.getValues("projectDeleted") === true || (!!formProps.getValues("projectActualStart") && !!formProps.getValues("projectActualEnd"))}>
<Button
variant="contained"
startIcon={<Check />}
type="submit"
disabled={
formProps.getValues("projectDeleted") === true ||
(!!formProps.getValues("projectActualStart") &&
!!formProps.getValues("projectActualEnd"))
}
>
{isEditMode ? t("Save") : t("Confirm")}
</Button>
</Stack>


+ 1
- 1
src/components/CreateProject/CreateProjectWrapper.tsx Wyświetl plik

@@ -56,7 +56,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
]);

const projectInfo = props.isEditMode
? await fetchProjectDetails(props.projectId!!)
? await fetchProjectDetails(props.projectId!)
: undefined;

return (


+ 39
- 12
src/components/TimesheetModal/TimesheetModal.tsx Wyświetl plik

@@ -12,8 +12,11 @@ import {
import TimesheetTable from "../TimesheetTable";
import { useTranslation } from "react-i18next";
import { Check, Close } from "@mui/icons-material";
import { FormProvider, useForm } from "react-hook-form";
import { RecordTimesheetInput } from "@/app/api/timesheets/actions";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import {
RecordTimesheetInput,
saveTimesheet,
} from "@/app/api/timesheets/actions";
import dayjs from "dayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { AssignedProject } from "@/app/api/projects";
@@ -23,6 +26,8 @@ interface Props {
onClose: () => void;
timesheetType: "time" | "leave";
assignedProjects: AssignedProject[];
username: string;
defaultTimesheets?: RecordTimesheetInput;
}

const modalSx: SxProps = {
@@ -40,6 +45,8 @@ const TimesheetModal: React.FC<Props> = ({
onClose,
timesheetType,
assignedProjects,
username,
defaultTimesheets,
}) => {
const { t } = useTranslation("home");

@@ -48,15 +55,37 @@ const TimesheetModal: React.FC<Props> = ({
return Array(7)
.fill(undefined)
.reduce<RecordTimesheetInput>((acc, _, index) => {
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT);
return {
...acc,
[today.subtract(index, "day").format(INPUT_DATE_FORMAT)]: [],
[date]: defaultTimesheets?.[date] ?? [],
};
}, {});
}, []);
}, [defaultTimesheets]);

const formProps = useForm<RecordTimesheetInput>({ defaultValues });

const onSubmit = useCallback<SubmitHandler<RecordTimesheetInput>>(
async (data) => {
const savedRecords = await saveTimesheet(data, username);

const today = dayjs();
const newFormValues = Array(7)
.fill(undefined)
.reduce<RecordTimesheetInput>((acc, _, index) => {
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT);
return {
...acc,
[date]: savedRecords[date] ?? [],
};
}, {});

formProps.reset(newFormValues);
onClose();
},
[formProps, onClose, username],
);

const onCancel = useCallback(() => {
formProps.reset(defaultValues);
onClose();
@@ -66,7 +95,10 @@ const TimesheetModal: React.FC<Props> = ({
<Modal open={isOpen} onClose={onClose}>
<Card sx={modalSx}>
<FormProvider {...formProps}>
<CardContent>
<CardContent
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t(timesheetType === "time" ? "Timesheet Input" : "Record Leave")}
</Typography>
@@ -86,13 +118,8 @@ const TimesheetModal: React.FC<Props> = ({
>
{t("Cancel")}
</Button>
<Button
onClick={onClose}
variant="contained"
startIcon={<Check />}
type="submit"
>
{t("Confirm")}
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Save")}
</Button>
</CardActions>
</CardContent>


+ 6
- 7
src/components/TimesheetTable/EntryInputTable.tsx Wyświetl plik

@@ -36,7 +36,6 @@ type TimeEntryRow = Partial<
_isNew: boolean;
_error: string;
isPlanned: boolean;
id: string;
}
>;

@@ -74,21 +73,19 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
const { getValues, setValue } = useFormContext<RecordTimesheetInput>();
const currentEntries = getValues(day);

const [entries, setEntries] = useState<TimeEntryRow[]>(
currentEntries.map((e, index) => ({ ...e, id: `${day}-${index}` })) || [],
);
const [entries, setEntries] = useState<TimeEntryRow[]>(currentEntries || []);

const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});

const apiRef = useGridApiRef();
const addRow = useCallback(() => {
const id = `${day}-${Date.now()}`;
const id = Date.now();
setEntries((e) => [...e, { id, _isNew: true }]);
setRowModesModel((model) => ({
...model,
[id]: { mode: GridRowModes.Edit, fieldToFocus: "projectId" },
}));
}, [day]);
}, []);

const validateRow = useCallback(
(id: GridRowId) => {
@@ -318,9 +315,11 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
e.inputHours &&
e.projectId &&
e.taskId &&
e.taskGroupId,
e.taskGroupId &&
e.id,
)
.map((e) => ({
id: e.id!,
inputHours: e.inputHours!,
projectId: e.projectId!,
taskId: e.taskId!,


+ 5
- 3
src/components/UserWorkspacePage/AssignedProjects.tsx Wyświetl plik

@@ -14,9 +14,11 @@ import { Clear, Search } from "@mui/icons-material";
import ProjectGrid from "./ProjectGrid";
import { Props as UserWorkspaceProps } from "./UserWorkspacePage";

const AssignedProjects: React.FC<UserWorkspaceProps> = ({
assignedProjects,
}) => {
interface Props {
assignedProjects: UserWorkspaceProps["assignedProjects"];
}

const AssignedProjects: React.FC<Props> = ({ assignedProjects }) => {
const { t } = useTranslation("home");

// Projects


+ 11
- 1
src/components/UserWorkspacePage/UserWorkspacePage.tsx Wyświetl plik

@@ -10,12 +10,19 @@ import ButtonGroup from "@mui/material/ButtonGroup";
import AssignedProjects from "./AssignedProjects";
import TimesheetModal from "../TimesheetModal";
import { AssignedProject } from "@/app/api/projects";
import { RecordTimesheetInput } from "@/app/api/timesheets/actions";

export interface Props {
assignedProjects: AssignedProject[];
username: string;
defaultTimesheets: RecordTimesheetInput;
}

const UserWorkspacePage: React.FC<Props> = ({ assignedProjects }) => {
const UserWorkspacePage: React.FC<Props> = ({
assignedProjects,
username,
defaultTimesheets,
}) => {
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false);
const [isLeaveModalVisible, setLeaveModalVisible] = useState(false);
const { t } = useTranslation("home");
@@ -75,12 +82,15 @@ const UserWorkspacePage: React.FC<Props> = ({ assignedProjects }) => {
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
assignedProjects={assignedProjects}
username={username}
defaultTimesheets={defaultTimesheets}
/>
<TimesheetModal
timesheetType="leave"
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}
assignedProjects={assignedProjects}
username={username}
/>
<AssignedProjects assignedProjects={assignedProjects} />
</>


+ 18
- 3
src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx Wyświetl plik

@@ -1,9 +1,24 @@
import { fetchAssignedProjects } from "@/app/api/projects";
import UserWorkspacePage from "./UserWorkspacePage";
import { fetchTimesheets } from "@/app/api/timesheets";

const UserWorkspaceWrapper: React.FC = async () => {
const assignedProjects = await fetchAssignedProjects();
return <UserWorkspacePage assignedProjects={assignedProjects} />;
interface Props {
username: string;
}

const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => {
const [assignedProjects, timesheets] = await Promise.all([
fetchAssignedProjects(),
fetchTimesheets(username),
]);

return (
<UserWorkspacePage
assignedProjects={assignedProjects}
username={username}
defaultTimesheets={timesheets}
/>
);
};

export default UserWorkspaceWrapper;

+ 3
- 4
src/config/authConfig.ts Wyświetl plik

@@ -8,12 +8,10 @@ export interface SessionWithTokens extends Session {
refreshToken?: string;
}


export interface ability {
actionSubjectCombo: string;
}


export const authOptions: AuthOptions = {
debug: process.env.NODE_ENV === "development",
providers: [
@@ -55,11 +53,12 @@ export const authOptions: AuthOptions = {
const sessionWithToken: SessionWithTokens = {
...session,
// Add the data from the token to the session
abilities: (token.abilities as ability[]).map((item: ability) => item.actionSubjectCombo) as string[],
abilities: (token.abilities as ability[]).map(
(item: ability) => item.actionSubjectCombo,
) as string[],
accessToken: token.accessToken as string | undefined,
refreshToken: token.refreshToken as string | undefined,
};
// console.log(sessionWithToken)
return sessionWithToken;
},


Ładowanie…
Anuluj
Zapisz