@@ -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; | ||||
}, | }, | ||||