diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx
index 176c9a2..155388f 100644
--- a/src/app/(main)/home/page.tsx
+++ b/src/app/(main)/home/page.tsx
@@ -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 (
-
+
);
};
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
index cfaa1a9..b93ed10 100644
--- a/src/app/(main)/layout.tsx
+++ b/src/app/(main)/layout.tsx
@@ -31,10 +31,10 @@ export default async function MainLayout({
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" },
}}
>
-
-
- {children}
-
+
+
+ {children}
+
>
);
diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts
index e383e89..6bb5596 100644
--- a/src/app/api/projects/actions.ts
+++ b/src/app/api/projects/actions.ts
@@ -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(`${BASE_API_URL}/projects/new`, {
- method: "POST",
- body: JSON.stringify(data),
- headers: { "Content-Type": "application/json" },
- });
+ const newProject = await serverFetchJson(
+ `${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;
};
diff --git a/src/app/api/timesheets/actions.ts b/src/app/api/timesheets/actions.ts
index 6b959ec..631c076 100644
--- a/src/app/api/timesheets/actions.ts
+++ b/src/app/api/timesheets/actions.ts
@@ -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(
+ `${BASE_API_URL}/timesheets/save`,
+ {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+
+ revalidateTag(`timesheets_${username}`);
+
+ return savedRecords;
+};
diff --git a/src/app/api/timesheets/index.ts b/src/app/api/timesheets/index.ts
new file mode 100644
index 0000000..fd7d20d
--- /dev/null
+++ b/src/app/api/timesheets/index.ts
@@ -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(`${BASE_API_URL}/timesheets`, {
+ next: { tags: [`timesheets_${username}`] },
+ });
+});
diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx
index fb57dd7..5511464 100644
--- a/src/components/CreateProject/CreateProject.tsx
+++ b/src/components/CreateProject/CreateProject.tsx
@@ -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 = ({
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>(
(_e, newValue) => {
@@ -124,51 +132,55 @@ const CreateProject: React.FC = ({
const onSubmit = useCallback>(
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>(
@@ -196,8 +208,8 @@ const CreateProject: React.FC = ({
// 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 = ({
>
{isEditMode && !(formProps.getValues("projectDeleted") === true) && (
- {!formProps.getValues("projectActualStart") && } color="success">
- {t("Start Project")}
- }
- {formProps.getValues("projectActualStart") && !formProps.getValues("projectActualEnd") && } color="info">
- {t("Complete Project")}
- }
- {!(formProps.getValues("projectActualStart") && formProps.getValues("projectActualEnd")) && } color="error" onClick={handleDelete}>
- {t("Delete Project")}
- }
+ {!formProps.getValues("projectActualStart") && (
+ }
+ color="success"
+ >
+ {t("Start Project")}
+
+ )}
+ {formProps.getValues("projectActualStart") &&
+ !formProps.getValues("projectActualEnd") && (
+ }
+ color="info"
+ >
+ {t("Complete Project")}
+
+ )}
+ {!(
+ formProps.getValues("projectActualStart") &&
+ formProps.getValues("projectActualEnd")
+ ) && (
+ }
+ color="error"
+ onClick={handleDelete}
+ >
+ {t("Delete Project")}
+
+ )}
)}
= ({
>
{t("Cancel")}
- } type="submit" disabled={formProps.getValues("projectDeleted") === true || (!!formProps.getValues("projectActualStart") && !!formProps.getValues("projectActualEnd"))}>
+ }
+ type="submit"
+ disabled={
+ formProps.getValues("projectDeleted") === true ||
+ (!!formProps.getValues("projectActualStart") &&
+ !!formProps.getValues("projectActualEnd"))
+ }
+ >
{isEditMode ? t("Save") : t("Confirm")}
diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx
index f380b2f..6ce6242 100644
--- a/src/components/CreateProject/CreateProjectWrapper.tsx
+++ b/src/components/CreateProject/CreateProjectWrapper.tsx
@@ -56,7 +56,7 @@ const CreateProjectWrapper: React.FC = async (props) => {
]);
const projectInfo = props.isEditMode
- ? await fetchProjectDetails(props.projectId!!)
+ ? await fetchProjectDetails(props.projectId!)
: undefined;
return (
diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx
index d6146b0..c336cef 100644
--- a/src/components/TimesheetModal/TimesheetModal.tsx
+++ b/src/components/TimesheetModal/TimesheetModal.tsx
@@ -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 = ({
onClose,
timesheetType,
assignedProjects,
+ username,
+ defaultTimesheets,
}) => {
const { t } = useTranslation("home");
@@ -48,15 +55,37 @@ const TimesheetModal: React.FC = ({
return Array(7)
.fill(undefined)
.reduce((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({ defaultValues });
+ const onSubmit = useCallback>(
+ async (data) => {
+ const savedRecords = await saveTimesheet(data, username);
+
+ const today = dayjs();
+ const newFormValues = Array(7)
+ .fill(undefined)
+ .reduce((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 = ({
-
+
{t(timesheetType === "time" ? "Timesheet Input" : "Record Leave")}
@@ -86,13 +118,8 @@ const TimesheetModal: React.FC = ({
>
{t("Cancel")}
- }
- type="submit"
- >
- {t("Confirm")}
+ } type="submit">
+ {t("Save")}
diff --git a/src/components/TimesheetTable/EntryInputTable.tsx b/src/components/TimesheetTable/EntryInputTable.tsx
index 87eac77..2e704fe 100644
--- a/src/components/TimesheetTable/EntryInputTable.tsx
+++ b/src/components/TimesheetTable/EntryInputTable.tsx
@@ -36,7 +36,6 @@ type TimeEntryRow = Partial<
_isNew: boolean;
_error: string;
isPlanned: boolean;
- id: string;
}
>;
@@ -74,21 +73,19 @@ const EntryInputTable: React.FC = ({ day, assignedProjects }) => {
const { getValues, setValue } = useFormContext();
const currentEntries = getValues(day);
- const [entries, setEntries] = useState(
- currentEntries.map((e, index) => ({ ...e, id: `${day}-${index}` })) || [],
- );
+ const [entries, setEntries] = useState(currentEntries || []);
const [rowModesModel, setRowModesModel] = useState({});
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 = ({ 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!,
diff --git a/src/components/UserWorkspacePage/AssignedProjects.tsx b/src/components/UserWorkspacePage/AssignedProjects.tsx
index ccd088c..6baa133 100644
--- a/src/components/UserWorkspacePage/AssignedProjects.tsx
+++ b/src/components/UserWorkspacePage/AssignedProjects.tsx
@@ -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 = ({
- assignedProjects,
-}) => {
+interface Props {
+ assignedProjects: UserWorkspaceProps["assignedProjects"];
+}
+
+const AssignedProjects: React.FC = ({ assignedProjects }) => {
const { t } = useTranslation("home");
// Projects
diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx
index fc0b233..e8d51d7 100644
--- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx
+++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx
@@ -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 = ({ assignedProjects }) => {
+const UserWorkspacePage: React.FC = ({
+ 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 = ({ assignedProjects }) => {
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
assignedProjects={assignedProjects}
+ username={username}
+ defaultTimesheets={defaultTimesheets}
/>
>
diff --git a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx
index c311488..cd5fe66 100644
--- a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx
+++ b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx
@@ -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 ;
+interface Props {
+ username: string;
+}
+
+const UserWorkspaceWrapper: React.FC = async ({ username }) => {
+ const [assignedProjects, timesheets] = await Promise.all([
+ fetchAssignedProjects(),
+ fetchTimesheets(username),
+ ]);
+
+ return (
+
+ );
};
export default UserWorkspaceWrapper;
diff --git a/src/config/authConfig.ts b/src/config/authConfig.ts
index e10f60d..2c2b9da 100644
--- a/src/config/authConfig.ts
+++ b/src/config/authConfig.ts
@@ -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;
},