| @@ -1,15 +1,24 @@ | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
| import UserWorkspacePage from "@/components/UserWorkspacePage"; | 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 = { | export const metadata: Metadata = { | ||||
| title: "User Workspace", | title: "User Workspace", | ||||
| }; | }; | ||||
| const Home: React.FC = async () => { | const Home: React.FC = async () => { | ||||
| const session = await getServerSession(authOptions); | |||||
| // Get name for caching | |||||
| const username = session!.user!.name!; | |||||
| await fetchTimesheets(username); | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["home"]}> | <I18nProvider namespaces={["home"]}> | ||||
| <UserWorkspacePage /> | |||||
| <UserWorkspacePage username={username} /> | |||||
| </I18nProvider> | </I18nProvider> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -31,10 +31,10 @@ export default async function MainLayout({ | |||||
| padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | ||||
| }} | }} | ||||
| > | > | ||||
| <Stack spacing={2}> | |||||
| <Breadcrumb /> | |||||
| {children} | |||||
| </Stack> | |||||
| <Stack spacing={2}> | |||||
| <Breadcrumb /> | |||||
| {children} | |||||
| </Stack> | |||||
| </Box> | </Box> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -1,13 +1,16 @@ | |||||
| "use server"; | "use server"; | ||||
| import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
| import { | |||||
| serverFetchJson, | |||||
| serverFetchWithNoContent, | |||||
| } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { Task, TaskGroup } from "../tasks"; | import { Task, TaskGroup } from "../tasks"; | ||||
| import { Customer } from "../customer"; | import { Customer } from "../customer"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| export interface CreateProjectInputs { | export interface CreateProjectInputs { | ||||
| // Project | |||||
| // Project | |||||
| projectId: number | null; | projectId: number | null; | ||||
| projectDeleted: boolean | null; | projectDeleted: boolean | null; | ||||
| projectCode: string; | projectCode: string; | ||||
| @@ -67,19 +70,22 @@ export interface PaymentInputs { | |||||
| } | } | ||||
| export interface CreateProjectResponse { | 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) => { | 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"); | revalidateTag("projects"); | ||||
| return newProject; | return newProject; | ||||
| @@ -94,5 +100,7 @@ export const deleteProject = async (id: number) => { | |||||
| }, | }, | ||||
| ); | ); | ||||
| return project | |||||
| revalidateTag("projects"); | |||||
| revalidateTag("assignedProjects"); | |||||
| return project; | |||||
| }; | }; | ||||
| @@ -1,9 +1,13 @@ | |||||
| "use server"; | "use server"; | ||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { ProjectResult } from "../projects"; | import { ProjectResult } from "../projects"; | ||||
| import { Task, TaskGroup } from "../tasks"; | import { Task, TaskGroup } from "../tasks"; | ||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { revalidateTag } from "next/cache"; | |||||
| export interface TimeEntry { | export interface TimeEntry { | ||||
| id: number; | |||||
| projectId: ProjectResult["id"]; | projectId: ProjectResult["id"]; | ||||
| taskGroupId: TaskGroup["id"]; | taskGroupId: TaskGroup["id"]; | ||||
| taskId: Task["id"]; | taskId: Task["id"]; | ||||
| @@ -13,3 +17,21 @@ export interface TimeEntry { | |||||
| export interface RecordTimesheetInput { | export interface RecordTimesheetInput { | ||||
| [date: string]: TimeEntry[]; | [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; | |||||
| }; | |||||
| @@ -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}`] }, | |||||
| }); | |||||
| }); | |||||
| @@ -1,6 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import DoneIcon from '@mui/icons-material/Done' | |||||
| import DoneIcon from "@mui/icons-material/Done"; | |||||
| import Check from "@mui/icons-material/Check"; | import Check from "@mui/icons-material/Check"; | ||||
| import Close from "@mui/icons-material/Close"; | import Close from "@mui/icons-material/Close"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| @@ -22,7 +22,11 @@ import { | |||||
| SubmitHandler, | SubmitHandler, | ||||
| useForm, | useForm, | ||||
| } from "react-hook-form"; | } 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 { Delete, Error, PlayArrow } from "@mui/icons-material"; | ||||
| import { | import { | ||||
| BuildingType, | BuildingType, | ||||
| @@ -38,7 +42,12 @@ import { Typography } from "@mui/material"; | |||||
| import { Grade } from "@/app/api/grades"; | import { Grade } from "@/app/api/grades"; | ||||
| import { Customer, Subsidiary } from "@/app/api/customer"; | import { Customer, Subsidiary } from "@/app/api/customer"; | ||||
| import { isEmpty } from "lodash"; | import { isEmpty } from "lodash"; | ||||
| import { deleteDialog, errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; | |||||
| import { | |||||
| deleteDialog, | |||||
| errorDialog, | |||||
| submitDialog, | |||||
| successDialog, | |||||
| } from "../Swal/CustomAlerts"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| export interface Props { | export interface Props { | ||||
| @@ -103,16 +112,15 @@ const CreateProject: React.FC<Props> = ({ | |||||
| const handleDelete = () => { | const handleDelete = () => { | ||||
| deleteDialog(async () => { | 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) { | if (clickSuccessDialog) { | ||||
| router.replace("/projects"); | router.replace("/projects"); | ||||
| } | } | ||||
| }, t) | |||||
| } | |||||
| }, t); | |||||
| }; | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | ||||
| (_e, newValue) => { | (_e, newValue) => { | ||||
| @@ -124,51 +132,55 @@ const CreateProject: React.FC<Props> = ({ | |||||
| const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( | const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( | ||||
| async (data, event) => { | async (data, event) => { | ||||
| try { | try { | ||||
| console.log("first") | |||||
| console.log("first"); | |||||
| setServerError(""); | 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") { | 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") { | } 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) { | } catch (e) { | ||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| } | } | ||||
| }, | }, | ||||
| [router, t, isEditMode], | |||||
| [router, t], | |||||
| ); | ); | ||||
| const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | ||||
| @@ -196,8 +208,8 @@ const CreateProject: React.FC<Props> = ({ | |||||
| // manhourPercentageByGrade should have a sensible default | // manhourPercentageByGrade should have a sensible default | ||||
| manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) | ||||
| ? grades.reduce((acc, grade) => { | ? grades.reduce((acc, grade) => { | ||||
| return { ...acc, [grade.id]: 1 / grades.length }; | |||||
| }, {}) | |||||
| return { ...acc, [grade.id]: 1 / grades.length }; | |||||
| }, {}) | |||||
| : defaultInputs?.manhourPercentageByGrade, | : defaultInputs?.manhourPercentageByGrade, | ||||
| }, | }, | ||||
| }); | }); | ||||
| @@ -214,15 +226,42 @@ const CreateProject: React.FC<Props> = ({ | |||||
| > | > | ||||
| {isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | {isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | ||||
| <Stack direction="row" gap={1}> | <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> | </Stack> | ||||
| )} | )} | ||||
| <Tabs | <Tabs | ||||
| @@ -290,7 +329,16 @@ const CreateProject: React.FC<Props> = ({ | |||||
| > | > | ||||
| {t("Cancel")} | {t("Cancel")} | ||||
| </Button> | </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")} | {isEditMode ? t("Save") : t("Confirm")} | ||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| @@ -56,7 +56,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
| ]); | ]); | ||||
| const projectInfo = props.isEditMode | const projectInfo = props.isEditMode | ||||
| ? await fetchProjectDetails(props.projectId!!) | |||||
| ? await fetchProjectDetails(props.projectId!) | |||||
| : undefined; | : undefined; | ||||
| return ( | return ( | ||||
| @@ -12,8 +12,11 @@ import { | |||||
| import TimesheetTable from "../TimesheetTable"; | import TimesheetTable from "../TimesheetTable"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { Check, Close } from "@mui/icons-material"; | 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 dayjs from "dayjs"; | ||||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import { AssignedProject } from "@/app/api/projects"; | import { AssignedProject } from "@/app/api/projects"; | ||||
| @@ -23,6 +26,8 @@ interface Props { | |||||
| onClose: () => void; | onClose: () => void; | ||||
| timesheetType: "time" | "leave"; | timesheetType: "time" | "leave"; | ||||
| assignedProjects: AssignedProject[]; | assignedProjects: AssignedProject[]; | ||||
| username: string; | |||||
| defaultTimesheets?: RecordTimesheetInput; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -40,6 +45,8 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| onClose, | onClose, | ||||
| timesheetType, | timesheetType, | ||||
| assignedProjects, | assignedProjects, | ||||
| username, | |||||
| defaultTimesheets, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -48,15 +55,37 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| return Array(7) | return Array(7) | ||||
| .fill(undefined) | .fill(undefined) | ||||
| .reduce<RecordTimesheetInput>((acc, _, index) => { | .reduce<RecordTimesheetInput>((acc, _, index) => { | ||||
| const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||||
| return { | return { | ||||
| ...acc, | ...acc, | ||||
| [today.subtract(index, "day").format(INPUT_DATE_FORMAT)]: [], | |||||
| [date]: defaultTimesheets?.[date] ?? [], | |||||
| }; | }; | ||||
| }, {}); | }, {}); | ||||
| }, []); | |||||
| }, [defaultTimesheets]); | |||||
| const formProps = useForm<RecordTimesheetInput>({ defaultValues }); | 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(() => { | const onCancel = useCallback(() => { | ||||
| formProps.reset(defaultValues); | formProps.reset(defaultValues); | ||||
| onClose(); | onClose(); | ||||
| @@ -66,7 +95,10 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| <Modal open={isOpen} onClose={onClose}> | <Modal open={isOpen} onClose={onClose}> | ||||
| <Card sx={modalSx}> | <Card sx={modalSx}> | ||||
| <FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
| <CardContent> | |||||
| <CardContent | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||||
| > | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
| {t(timesheetType === "time" ? "Timesheet Input" : "Record Leave")} | {t(timesheetType === "time" ? "Timesheet Input" : "Record Leave")} | ||||
| </Typography> | </Typography> | ||||
| @@ -86,13 +118,8 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| > | > | ||||
| {t("Cancel")} | {t("Cancel")} | ||||
| </Button> | </Button> | ||||
| <Button | |||||
| onClick={onClose} | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| type="submit" | |||||
| > | |||||
| {t("Confirm")} | |||||
| <Button variant="contained" startIcon={<Check />} type="submit"> | |||||
| {t("Save")} | |||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -36,7 +36,6 @@ type TimeEntryRow = Partial< | |||||
| _isNew: boolean; | _isNew: boolean; | ||||
| _error: string; | _error: string; | ||||
| isPlanned: boolean; | isPlanned: boolean; | ||||
| id: string; | |||||
| } | } | ||||
| >; | >; | ||||
| @@ -74,21 +73,19 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => { | |||||
| const { getValues, setValue } = useFormContext<RecordTimesheetInput>(); | const { getValues, setValue } = useFormContext<RecordTimesheetInput>(); | ||||
| const currentEntries = getValues(day); | 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 [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| const addRow = useCallback(() => { | const addRow = useCallback(() => { | ||||
| const id = `${day}-${Date.now()}`; | |||||
| const id = Date.now(); | |||||
| setEntries((e) => [...e, { id, _isNew: true }]); | setEntries((e) => [...e, { id, _isNew: true }]); | ||||
| setRowModesModel((model) => ({ | setRowModesModel((model) => ({ | ||||
| ...model, | ...model, | ||||
| [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectId" }, | [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectId" }, | ||||
| })); | })); | ||||
| }, [day]); | |||||
| }, []); | |||||
| const validateRow = useCallback( | const validateRow = useCallback( | ||||
| (id: GridRowId) => { | (id: GridRowId) => { | ||||
| @@ -318,9 +315,11 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => { | |||||
| e.inputHours && | e.inputHours && | ||||
| e.projectId && | e.projectId && | ||||
| e.taskId && | e.taskId && | ||||
| e.taskGroupId, | |||||
| e.taskGroupId && | |||||
| e.id, | |||||
| ) | ) | ||||
| .map((e) => ({ | .map((e) => ({ | ||||
| id: e.id!, | |||||
| inputHours: e.inputHours!, | inputHours: e.inputHours!, | ||||
| projectId: e.projectId!, | projectId: e.projectId!, | ||||
| taskId: e.taskId!, | taskId: e.taskId!, | ||||
| @@ -14,9 +14,11 @@ import { Clear, Search } from "@mui/icons-material"; | |||||
| import ProjectGrid from "./ProjectGrid"; | import ProjectGrid from "./ProjectGrid"; | ||||
| import { Props as UserWorkspaceProps } from "./UserWorkspacePage"; | 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"); | const { t } = useTranslation("home"); | ||||
| // Projects | // Projects | ||||
| @@ -10,12 +10,19 @@ import ButtonGroup from "@mui/material/ButtonGroup"; | |||||
| import AssignedProjects from "./AssignedProjects"; | import AssignedProjects from "./AssignedProjects"; | ||||
| import TimesheetModal from "../TimesheetModal"; | import TimesheetModal from "../TimesheetModal"; | ||||
| import { AssignedProject } from "@/app/api/projects"; | import { AssignedProject } from "@/app/api/projects"; | ||||
| import { RecordTimesheetInput } from "@/app/api/timesheets/actions"; | |||||
| export interface Props { | export interface Props { | ||||
| assignedProjects: AssignedProject[]; | 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 [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); | ||||
| const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -75,12 +82,15 @@ const UserWorkspacePage: React.FC<Props> = ({ assignedProjects }) => { | |||||
| isOpen={isTimeheetModalVisible} | isOpen={isTimeheetModalVisible} | ||||
| onClose={handleCloseTimesheetModal} | onClose={handleCloseTimesheetModal} | ||||
| assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
| username={username} | |||||
| defaultTimesheets={defaultTimesheets} | |||||
| /> | /> | ||||
| <TimesheetModal | <TimesheetModal | ||||
| timesheetType="leave" | timesheetType="leave" | ||||
| isOpen={isLeaveModalVisible} | isOpen={isLeaveModalVisible} | ||||
| onClose={handleCloseLeaveModal} | onClose={handleCloseLeaveModal} | ||||
| assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
| username={username} | |||||
| /> | /> | ||||
| <AssignedProjects assignedProjects={assignedProjects} /> | <AssignedProjects assignedProjects={assignedProjects} /> | ||||
| </> | </> | ||||
| @@ -1,9 +1,24 @@ | |||||
| import { fetchAssignedProjects } from "@/app/api/projects"; | import { fetchAssignedProjects } from "@/app/api/projects"; | ||||
| import UserWorkspacePage from "./UserWorkspacePage"; | 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; | export default UserWorkspaceWrapper; | ||||
| @@ -8,12 +8,10 @@ export interface SessionWithTokens extends Session { | |||||
| refreshToken?: string; | refreshToken?: string; | ||||
| } | } | ||||
| export interface ability { | export interface ability { | ||||
| actionSubjectCombo: string; | actionSubjectCombo: string; | ||||
| } | } | ||||
| export const authOptions: AuthOptions = { | export const authOptions: AuthOptions = { | ||||
| debug: process.env.NODE_ENV === "development", | debug: process.env.NODE_ENV === "development", | ||||
| providers: [ | providers: [ | ||||
| @@ -55,11 +53,12 @@ export const authOptions: AuthOptions = { | |||||
| const sessionWithToken: SessionWithTokens = { | const sessionWithToken: SessionWithTokens = { | ||||
| ...session, | ...session, | ||||
| // Add the data from the token to the 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, | accessToken: token.accessToken as string | undefined, | ||||
| refreshToken: token.refreshToken as string | undefined, | refreshToken: token.refreshToken as string | undefined, | ||||
| }; | }; | ||||
| // console.log(sessionWithToken) | // console.log(sessionWithToken) | ||||
| return sessionWithToken; | return sessionWithToken; | ||||
| }, | }, | ||||