From 404f42abd52a953f31ee146efd8578df88ddaa71 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Tue, 16 Jul 2024 12:11:18 +0800 Subject: [PATCH 01/14] fallback to 2 button --- src/components/InvoiceSearch/InvoiceSearch.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx index 0e1409a..f76db96 100644 --- a/src/components/InvoiceSearch/InvoiceSearch.tsx +++ b/src/components/InvoiceSearch/InvoiceSearch.tsx @@ -86,7 +86,7 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic const formData = new FormData(); formData.append('multipartFileList', file); - const response = await importInvoices(formData); + const response = await importIssuedInovice(formData); // response: status, message, projectList, emptyRowList, invoiceList console.log(response) @@ -271,8 +271,8 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic flexWrap="wrap" spacing={2} > - {/* */} - {/* */} - {/* */} - + + {/* + */} { // tabIndex == 0 && From d6d0092f109ff1ef5ba8a2399f82a6da1f03393a Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Tue, 16 Jul 2024 13:55:10 +0800 Subject: [PATCH 02/14] Update combined column field, updated display of settle Date and Amount received --- src/app/utils/formatUtil.ts | 3 +++ src/components/InvoiceSearch/InvoiceSearch.tsx | 2 +- src/components/InvoiceSearch/InvoiceSearchWrapper.tsx | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index d5edb6f..32ccc76 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -131,6 +131,9 @@ export function convertLocaleStringToNumber(numberString: string): number { } export function timestampToDateString(timestamp: string): string { + if (timestamp === null){ + return "-" + } const date = new Date(timestamp); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx index f76db96..acc7b4a 100644 --- a/src/components/InvoiceSearch/InvoiceSearch.tsx +++ b/src/components/InvoiceSearch/InvoiceSearch.tsx @@ -236,7 +236,7 @@ const InvoiceSearch: React.FC = ({ issuedInvoice, receivedInvoice, invoic { name: "projectName", label: t("Project Name") }, { name: "team", label: t("Team") }, { name: "issuedDate", label: t("Issue Date") }, - { name: "receivedAmount", label: t("Amount (HKD)") }, + { name: "issuedAmount", label: t("Amount (HKD)") }, { name: "receiptDate", label: t("Settle Date") }, { name: "receivedAmount", label: t("Actual Received Amount (HKD)") }, ], diff --git a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx index ad3c49c..f155c03 100644 --- a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx +++ b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx @@ -56,9 +56,9 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => { projectName: invoice.projectName, team: invoice.team, issuedDate: timestampToDateString(invoice.invoiceDate)!!, - receiptDate: timestampToDateString(invoice.receiptDate??0)!!, + receiptDate: timestampToDateString(invoice.receiptDate??null)!!, issuedAmount: moneyFormatter.format(invoice.issueAmount), - receivedAmount: moneyFormatter.format(invoice.paidAmount) + receivedAmount: invoice.paidAmount === null ? "-" : moneyFormatter.format(invoice.paidAmount) } }) From 0deeda45a9296647422f5a49fcd5654a63cff046 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 16 Jul 2024 14:25:13 +0800 Subject: [PATCH 03/14] update cross team report (cherry picked from commit 13b6cc9f1205190872a2ebc0ca852ad7e89abb57) --- src/app/api/reports/index.ts | 2 + .../GenerateCrossTeamChargeReport.tsx | 22 +++++- .../GenerateCrossTeamChargeReportWrapper.tsx | 7 +- .../NavigationContent/NavigationContent.tsx | 54 ++++++++++++- src/middleware.ts | 76 ++++++++++++++++++- 5 files changed, 150 insertions(+), 11 deletions(-) diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 934f02c..7db4809 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -120,8 +120,10 @@ export interface CostAndExpenseReportRequest { // - Cross Team Charge Report export interface CrossTeamChargeReportFilter { month: string; + team: string[]; } export interface CrossTeamChargeReportRequest { month: string; + teamId: number | "All"; } \ No newline at end of file diff --git a/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx index 2d7aaa4..683b701 100644 --- a/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx +++ b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReport.tsx @@ -6,15 +6,20 @@ import { useTranslation } from "react-i18next"; import { CrossTeamChargeReportFilter } from "@/app/api/reports"; import { fetchCrossTeamChargeReport } from "@/app/api/reports/actions"; import { downloadFile } from "@/app/utils/commonUtil"; +import { TeamResult } from "@/app/api/team"; +import { SessionStaff } from "@/config/authConfig"; interface Props { + teams: TeamResult[]; + userStaff: SessionStaff; } type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; -const GenerateCrossTeamChargeReport: React.FC = () => { +const GenerateCrossTeamChargeReport: React.FC = ({ teams, userStaff }) => { const { t } = useTranslation("report"); + const teamCombo = teams.map(team => `${team.code} - ${team.name}`) const searchCriteria: Criterion[] = useMemo( () => [ @@ -23,6 +28,13 @@ const GenerateCrossTeamChargeReport: React.FC = () => { paramName: "month", type: "monthYear", }, + { + label: t("Team"), + paramName: "team", + type: "select", + options: teamCombo, + needAll: !Boolean(userStaff?.isTeamLead) + }, ], [t], ); @@ -33,10 +45,12 @@ const GenerateCrossTeamChargeReport: React.FC = () => { criteria={searchCriteria} onSearch={async (query) => { - console.log(query.month) - if (Boolean(query.month)) { + console.log(query) + if (Boolean(query.month) && Boolean(query.team)) { // const projectIndex = projectCombo.findIndex(({value}) => value === parseInt(query.project)) - const response = await fetchCrossTeamChargeReport({ month: query.month }) + const teamIndex = teamCombo.findIndex(team => team === query.team) + + const response = await fetchCrossTeamChargeReport({ month: query.month, teamId: teamIndex >= 0 ? teams[teamIndex].id : "All", }) if (response) { downloadFile(new Uint8Array(response.blobValue), response.filename!!) } diff --git a/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx index 5e7d2e5..a9aaea2 100644 --- a/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx +++ b/src/components/GenerateCrossTeamChargeReport/GenerateCrossTeamChargeReportWrapper.tsx @@ -1,13 +1,18 @@ import React from "react"; import GenerateCrossTeamChargeReportLoading from "./GenerateCrossTeamChargeReportLoading"; import GenerateCrossTeamChargeReport from "./GenerateCrossTeamChargeReport"; +import { fetchTeam } from "@/app/api/team"; +import { getUserStaff } from "@/app/utils/commonUtil"; interface SubComponents { Loading: typeof GenerateCrossTeamChargeReportLoading; } const GenerateCrossTeamChargeReportWrapper: React.FC & SubComponents = async () => { - return ; + + const [teams, userStaff] = await Promise.all([fetchTeam(), getUserStaff()]) + + return team.id === userStaff?.teamId)} userStaff={userStaff}/>; }; GenerateCrossTeamChargeReportWrapper.Loading = GenerateCrossTeamChargeReportLoading; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index bab1493..83a5535 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -36,7 +36,6 @@ import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import { - GENERATE_REPORTS, IMPORT_INVOICE, IMPORT_RECEIPT, MAINTAIN_PROJECT, @@ -68,6 +67,16 @@ import { MAINTAIN_GROUP, MAINTAIN_HOLIDAY, VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, + GENERATE_LATE_START_REPORTS, + GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, + GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, + GENERATE_COST_ANT_EXPENSE_REPORT, + GENERATE_PROJECT_COMPLETION_REPORT, + GENERATE_PROJECT_PANDL_REPORT, + GENERATE_FINANCIAL_STATUS_REPORT, + GENERATE_PROJECT_CASH_FLOW_REPORT, + GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, + GENERATE_CROSS_TEAM_CHARGE_REPORT, } from "@/middleware"; import { SessionWithAbilities } from "../AppBar/NavigationToggle"; import { authOptions } from "@/config/authConfig"; @@ -180,7 +189,18 @@ const NavigationContent: React.FC = ({ abilities, username }) => { icon: , label: "Analysis Report", path: "", - isHidden: ![GENERATE_REPORTS].some((ability) => + isHidden: ![ + GENERATE_LATE_START_REPORTS, + GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, + GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, + GENERATE_COST_ANT_EXPENSE_REPORT, + GENERATE_PROJECT_COMPLETION_REPORT, + GENERATE_PROJECT_PANDL_REPORT, + GENERATE_FINANCIAL_STATUS_REPORT, + GENERATE_PROJECT_CASH_FLOW_REPORT, + GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, + GENERATE_CROSS_TEAM_CHARGE_REPORT, + ].some((ability) => abilities!.includes(ability), ), children: [ @@ -188,26 +208,41 @@ const NavigationContent: React.FC = ({ abilities, username }) => { icon: , label: "Late Start Report", path: "/analytics/LateStartReport", + isHidden: ![GENERATE_LATE_START_REPORTS].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Project Potential Delay Report", path: "/analytics/ProjectPotentialDelayReport", + isHidden: ![GENERATE_PROJECT_POTENTIAL_DELAY_REPORT].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Resource Overconsumption Report", path: "/analytics/ResourceOverconsumptionReport", + isHidden: ![GENERATE_RESOURCE_OVERCONSUMPTION_REPORT].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Cost and Expense Report", path: "/analytics/CostandExpenseReport", + isHidden: ![GENERATE_COST_ANT_EXPENSE_REPORT].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Project Completion Report", path: "/analytics/ProjectCompletionReport", + isHidden: ![GENERATE_PROJECT_COMPLETION_REPORT].some((ability) => + abilities!.includes(ability), + ), }, // { // icon: , @@ -223,26 +258,41 @@ const NavigationContent: React.FC = ({ abilities, username }) => { icon: , label: "Project P&L Report", path: "/analytics/ProjectPandLReport", + isHidden: ![GENERATE_PROJECT_COMPLETION_REPORT].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Financial Status Report", path: "/analytics/FinancialStatusReport", + isHidden: ![GENERATE_FINANCIAL_STATUS_REPORT].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Project Cash Flow Report", path: "/analytics/ProjectCashFlowReport", + isHidden: ![GENERATE_PROJECT_CASH_FLOW_REPORT].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Staff Monthly Work Hours Analysis Report", path: "/analytics/StaffMonthlyWorkHoursAnalysisReport", + isHidden: ![GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT].some((ability) => + abilities!.includes(ability), + ), }, { icon: , label: "Cross Team Charge Report", path: "/analytics/CrossTeamChargeReport", + isHidden: ![GENERATE_CROSS_TEAM_CHARGE_REPORT].some((ability) => + abilities!.includes(ability), + ), }, ], }, diff --git a/src/middleware.ts b/src/middleware.ts index cb48c5b..0448add 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -48,7 +48,6 @@ export const [ VIEW_DASHBOARD_SELF, VIEW_DASHBOARD_ALL, IMPORT_INVOICE, - GENERATE_REPORTS, VIEW_STAFF_PROFILE, IMPORT_RECEIPT, MAINTAIN_TASK_TEMPLATE, @@ -60,6 +59,16 @@ export const [ VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, MAINTAIN_NORMAL_STAFF_WORKSPACE, MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, + GENERATE_LATE_START_REPORTS, + GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, + GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, + GENERATE_COST_ANT_EXPENSE_REPORT, + GENERATE_PROJECT_COMPLETION_REPORT, + GENERATE_PROJECT_PANDL_REPORT, + GENERATE_FINANCIAL_STATUS_REPORT, + GENERATE_PROJECT_CASH_FLOW_REPORT, + GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, + GENERATE_CROSS_TEAM_CHARGE_REPORT, ] = [ 'MAINTAIN_USER', 'MAINTAIN_TIMESHEET', @@ -89,7 +98,6 @@ export const [ 'VIEW_DASHBOARD_SELF', 'VIEW_DASHBOARD_ALL', 'IMPORT_INVOICE', - 'GENERATE_REPORTS', 'VIEW_STAFF_PROFILE', 'IMPORT_RECEIPT', 'MAINTAIN_TASK_TEMPLATE', @@ -100,7 +108,17 @@ export const [ 'MAINTAIN_TIMESHEET_FAST_TIME_ENTRY', 'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', 'MAINTAIN_NORMAL_STAFF_WORKSPACE', - 'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE' + 'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE', + 'GENERATE_LATE_START_REPORTS', + 'GENERATE_PROJECT_POTENTIAL_DELAY_REPORT', + 'GENERATE_RESOURCE_OVERCONSUMPTION_REPORT', + 'GENERATE_COST_ANT_EXPENSE_REPORT', + 'GENERATE_PROJECT_COMPLETION_REPORT', + 'GENERATE_PROJECT_P&L_REPORT', + 'GENERATE_FINANCIAL_STATUS_REPORT', + 'GENERATE_PROJECT_CASH_FLOW_REPORT', + 'GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', + 'GENERATE_CROSS_TEAM_CHARGE_REPORT', ] const PRIVATE_ROUTES = [ @@ -224,7 +242,57 @@ export default async function middleware( } if (req.nextUrl.pathname.startsWith('/analytics')) { - isAuth = [GENERATE_REPORTS].some((ability) => abilities.includes(ability)); + isAuth = [ + GENERATE_LATE_START_REPORTS, + GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, + GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, + GENERATE_COST_ANT_EXPENSE_REPORT, + GENERATE_PROJECT_COMPLETION_REPORT, + GENERATE_PROJECT_PANDL_REPORT, + GENERATE_FINANCIAL_STATUS_REPORT, + GENERATE_PROJECT_CASH_FLOW_REPORT, + GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, + GENERATE_CROSS_TEAM_CHARGE_REPORT,].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/LateStartReport')) { + isAuth = [GENERATE_LATE_START_REPORTS].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/ProjectPotentialDelayReport')) { + isAuth = [GENERATE_PROJECT_POTENTIAL_DELAY_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/ResourceOverconsumptionReport')) { + isAuth = [GENERATE_RESOURCE_OVERCONSUMPTION_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/CostandExpenseReport')) { + isAuth = [GENERATE_COST_ANT_EXPENSE_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/ProjectCompletionReport')) { + isAuth = [GENERATE_PROJECT_COMPLETION_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/ProjectPandLReport')) { + isAuth = [GENERATE_PROJECT_PANDL_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/FinancialStatusReport')) { + isAuth = [GENERATE_FINANCIAL_STATUS_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/ProjectCashFlowReport')) { + isAuth = [GENERATE_PROJECT_CASH_FLOW_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/StaffMonthlyWorkHoursAnalysisReport')) { + isAuth = [GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT].some((ability) => abilities.includes(ability)); + } + + if (req.nextUrl.pathname.startsWith('/analytics/CrossTeamChargeReport')) { + isAuth = [GENERATE_CROSS_TEAM_CHARGE_REPORT].some((ability) => abilities.includes(ability)); } if (req.nextUrl.pathname.startsWith('/settings/staff/edit')) { From 3f3de75fd12a4a0c33d92b6fe6100f6dfefedaaf Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 16 Jul 2024 16:35:16 +0800 Subject: [PATCH 04/14] update --- .../NavigationContent/NavigationContent.tsx | 10 +++++----- src/middleware.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 83a5535..3203fa6 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -67,7 +67,7 @@ import { MAINTAIN_GROUP, MAINTAIN_HOLIDAY, VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, - GENERATE_LATE_START_REPORTS, + GENERATE_LATE_START_REPORT, GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, GENERATE_COST_ANT_EXPENSE_REPORT, @@ -76,7 +76,7 @@ import { GENERATE_FINANCIAL_STATUS_REPORT, GENERATE_PROJECT_CASH_FLOW_REPORT, GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, - GENERATE_CROSS_TEAM_CHARGE_REPORT, + GENERATE_CROSS_TEAM_CHARGE_REPORT } from "@/middleware"; import { SessionWithAbilities } from "../AppBar/NavigationToggle"; import { authOptions } from "@/config/authConfig"; @@ -190,7 +190,7 @@ const NavigationContent: React.FC = ({ abilities, username }) => { label: "Analysis Report", path: "", isHidden: ![ - GENERATE_LATE_START_REPORTS, + GENERATE_LATE_START_REPORT, GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, GENERATE_COST_ANT_EXPENSE_REPORT, @@ -199,7 +199,7 @@ const NavigationContent: React.FC = ({ abilities, username }) => { GENERATE_FINANCIAL_STATUS_REPORT, GENERATE_PROJECT_CASH_FLOW_REPORT, GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, - GENERATE_CROSS_TEAM_CHARGE_REPORT, + GENERATE_CROSS_TEAM_CHARGE_REPORT ].some((ability) => abilities!.includes(ability), ), @@ -208,7 +208,7 @@ const NavigationContent: React.FC = ({ abilities, username }) => { icon: , label: "Late Start Report", path: "/analytics/LateStartReport", - isHidden: ![GENERATE_LATE_START_REPORTS].some((ability) => + isHidden: ![GENERATE_LATE_START_REPORT].some((ability) => abilities!.includes(ability), ), }, diff --git a/src/middleware.ts b/src/middleware.ts index 0448add..c9208df 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -59,7 +59,7 @@ export const [ VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, MAINTAIN_NORMAL_STAFF_WORKSPACE, MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, - GENERATE_LATE_START_REPORTS, + GENERATE_LATE_START_REPORT, GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, GENERATE_COST_ANT_EXPENSE_REPORT, @@ -68,7 +68,7 @@ export const [ GENERATE_FINANCIAL_STATUS_REPORT, GENERATE_PROJECT_CASH_FLOW_REPORT, GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, - GENERATE_CROSS_TEAM_CHARGE_REPORT, + GENERATE_CROSS_TEAM_CHARGE_REPORT ] = [ 'MAINTAIN_USER', 'MAINTAIN_TIMESHEET', @@ -109,7 +109,7 @@ export const [ 'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', 'MAINTAIN_NORMAL_STAFF_WORKSPACE', 'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE', - 'GENERATE_LATE_START_REPORTS', + 'GENERATE_LATE_START_REPORT', 'GENERATE_PROJECT_POTENTIAL_DELAY_REPORT', 'GENERATE_RESOURCE_OVERCONSUMPTION_REPORT', 'GENERATE_COST_ANT_EXPENSE_REPORT', @@ -118,7 +118,7 @@ export const [ 'GENERATE_FINANCIAL_STATUS_REPORT', 'GENERATE_PROJECT_CASH_FLOW_REPORT', 'GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', - 'GENERATE_CROSS_TEAM_CHARGE_REPORT', + 'GENERATE_CROSS_TEAM_CHARGE_REPORT' ] const PRIVATE_ROUTES = [ @@ -243,7 +243,7 @@ export default async function middleware( if (req.nextUrl.pathname.startsWith('/analytics')) { isAuth = [ - GENERATE_LATE_START_REPORTS, + GENERATE_LATE_START_REPORT, GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, GENERATE_COST_ANT_EXPENSE_REPORT, @@ -252,11 +252,11 @@ export default async function middleware( GENERATE_FINANCIAL_STATUS_REPORT, GENERATE_PROJECT_CASH_FLOW_REPORT, GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, - GENERATE_CROSS_TEAM_CHARGE_REPORT,].some((ability) => abilities.includes(ability)); + GENERATE_CROSS_TEAM_CHARGE_REPORT].some((ability) => abilities.includes(ability)); } if (req.nextUrl.pathname.startsWith('/analytics/LateStartReport')) { - isAuth = [GENERATE_LATE_START_REPORTS].some((ability) => abilities.includes(ability)); + isAuth = [GENERATE_LATE_START_REPORT].some((ability) => abilities.includes(ability)); } if (req.nextUrl.pathname.startsWith('/analytics/ProjectPotentialDelayReport')) { From b4a611389d9f11aef354dec4359a271e5fc67a84 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 17 Jul 2024 11:50:20 +0800 Subject: [PATCH 05/14] update --- src/middleware.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index c9208df..d2494d9 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -109,16 +109,16 @@ export const [ 'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', 'MAINTAIN_NORMAL_STAFF_WORKSPACE', 'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE', - 'GENERATE_LATE_START_REPORT', - 'GENERATE_PROJECT_POTENTIAL_DELAY_REPORT', - 'GENERATE_RESOURCE_OVERCONSUMPTION_REPORT', - 'GENERATE_COST_ANT_EXPENSE_REPORT', - 'GENERATE_PROJECT_COMPLETION_REPORT', - 'GENERATE_PROJECT_P&L_REPORT', - 'GENERATE_FINANCIAL_STATUS_REPORT', - 'GENERATE_PROJECT_CASH_FLOW_REPORT', - 'GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', - 'GENERATE_CROSS_TEAM_CHARGE_REPORT' + 'G_LATE_START_REPORT', + 'G_PROJECT_POTENTIAL_DELAY_REPORT', + 'G_RESOURCE_OVERCONSUMPTION_REPORT', + 'G_COST_AND_EXPENSE_REPORT', + 'G_PROJECT_COMPLETION_REPORT', + 'G_PROJECT_P&L_REPORT', + 'G_FINANCIAL_STATUS_REPORT', + 'G_PROJECT_CASH_FLOW_REPORT', + 'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', + 'G_CROSS_TEAM_CHARGE_REPORT' ] const PRIVATE_ROUTES = [ @@ -252,7 +252,8 @@ export default async function middleware( GENERATE_FINANCIAL_STATUS_REPORT, GENERATE_PROJECT_CASH_FLOW_REPORT, GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, - GENERATE_CROSS_TEAM_CHARGE_REPORT].some((ability) => abilities.includes(ability)); + GENERATE_CROSS_TEAM_CHARGE_REPORT + ].some((ability) => abilities.includes(ability)); } if (req.nextUrl.pathname.startsWith('/analytics/LateStartReport')) { From b925a8cdb4096c82056b02c2dd846bab06a64067 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 18 Jul 2024 17:16:33 +0800 Subject: [PATCH 06/14] update dashboard (fix bugs) --- src/components/CreateProject/ProjectClientDetails.tsx | 4 ++-- src/components/StaffUtilization/StaffUtilization.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index e0333d4..7413f0a 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -344,7 +344,7 @@ const ProjectClientDetails: React.FC = ({ /> - + {/* = ({ {t("CLP Project")} - + */} diff --git a/src/components/StaffUtilization/StaffUtilization.tsx b/src/components/StaffUtilization/StaffUtilization.tsx index d432ca9..18049f4 100644 --- a/src/components/StaffUtilization/StaffUtilization.tsx +++ b/src/components/StaffUtilization/StaffUtilization.tsx @@ -362,7 +362,7 @@ const StaffUtilization: React.FC = ({ abilities, staff }) => { const startCount = weeklyPlanned[i].startCount const endCount = weeklyPlanned[i].endCount for (var j = 0; j < weeklyPlanned[i].searchDuration; j++) { - if (j >= startCount && j < endCount) { + if (j >= startCount && j <= endCount) { weeklyPlannedSubList.push(weeklyPlanned[i].AverageManhours) } else { weeklyPlannedSubList.push(0) @@ -503,7 +503,8 @@ const StaffUtilization: React.FC = ({ abilities, staff }) => { const fetchMonthlyUnsubmittedData = async () => { - const fetchResult = await fetchMonthlyUnsubmit(teamUnsubmitTeamId, unsubmitMonthlyFromValue.format('YYYY-MM-DD'), unsubmitMonthlyToValue.endOf('month').format('YYYY-MM-DD'), holidayDates); + const fetchResult = await fetchMonthlyUnsubmit(teamUnsubmitTeamId, unsubmitMonthlyFromValue.startOf('month').format('YYYY-MM-DD'), unsubmitMonthlyToValue.endOf('month').format('YYYY-MM-DD'), holidayDates); + const result = [] const staffList = [] var maxValue = 5 From 7439bbad589833f26ec04fc0a2654c3a2b5c5c76 Mon Sep 17 00:00:00 2001 From: Wayne Date: Thu, 18 Jul 2024 23:26:32 +0900 Subject: [PATCH 07/14] Past entry monthly summary and making sure entries are saved before submitting in timesheet input --- .../PastEntryCalendar/MonthlySummary.tsx | 196 ++++++++++++++++++ .../PastEntryCalendar/PastEntryCalendar.tsx | 3 + .../PastEntryCalendarModal.tsx | 94 +++++---- .../PastEntryCalendar/PastEntryList.tsx | 35 +++- .../TimeLeaveModal/TimeLeaveInputTable.tsx | 11 +- .../UserWorkspacePage/UserWorkspacePage.tsx | 1 + 6 files changed, 288 insertions(+), 52 deletions(-) create mode 100644 src/components/PastEntryCalendar/MonthlySummary.tsx diff --git a/src/components/PastEntryCalendar/MonthlySummary.tsx b/src/components/PastEntryCalendar/MonthlySummary.tsx new file mode 100644 index 0000000..797000a --- /dev/null +++ b/src/components/PastEntryCalendar/MonthlySummary.tsx @@ -0,0 +1,196 @@ +import { + RecordLeaveInput, + RecordTimesheetInput, +} from "@/app/api/timesheets/actions"; +import { + Box, + Card, + CardActionArea, + CardContent, + Stack, + Typography, +} from "@mui/material"; +import union from "lodash/union"; +import { useCallback, useMemo } from "react"; +import dayjs, { Dayjs } from "dayjs"; +import { getHolidayForDate } from "@/app/utils/holidayUtils"; +import { HolidaysResult } from "@/app/api/holidays"; +import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; +import { useTranslation } from "react-i18next"; +import pickBy from "lodash/pickBy"; + +interface Props { + currentMonth: Dayjs; + timesheet: RecordTimesheetInput; + leaves: RecordLeaveInput; + companyHolidays: HolidaysResult[]; + onDateSelect: (date: string) => void; +} + +const MonthlySummary: React.FC = ({ + timesheet, + leaves, + currentMonth, + companyHolidays, + onDateSelect, +}) => { + const { + t, + i18n: { language }, + } = useTranslation("home"); + + const timesheetForCurrentMonth = useMemo(() => { + return pickBy(timesheet, (_, date) => { + return currentMonth.isSame(dayjs(date), "month"); + }); + }, [currentMonth, timesheet]); + + const leavesForCurrentMonth = useMemo(() => { + return pickBy(leaves, (_, date) => { + return currentMonth.isSame(dayjs(date), "month"); + }); + }, [currentMonth, leaves]); + + const days = useMemo(() => { + return union( + Object.keys(timesheetForCurrentMonth), + Object.keys(leavesForCurrentMonth), + ); + }, [timesheetForCurrentMonth, leavesForCurrentMonth]).sort(); + + const makeSelectDate = useCallback( + (date: string) => () => { + onDateSelect(date); + }, + [onDateSelect], + ); + + return ( + + {t("Monthly Summary")} + + {days.map((day, index) => { + const dayJsObj = dayjs(day); + + const holiday = getHolidayForDate(day, companyHolidays); + const isHoliday = + holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + + const ls = leavesForCurrentMonth[day]; + const leaveHours = + ls?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; + + const ts = timesheetForCurrentMonth[day]; + const timesheetNormalHours = + ts?.reduce((acc, entry) => acc + (entry.inputHours || 0), 0) || 0; + const timesheetOtHours = + ts?.reduce((acc, entry) => acc + (entry.otHours || 0), 0) || 0; + + const timesheetHours = timesheetNormalHours + timesheetOtHours; + + return ( + + + + + {shortDateFormatter(language).format(dayJsObj.toDate())} + {holiday && ( + {`(${holiday.title})`} + )} + + + + + {t("Timesheet Hours")} + + + {manhourFormatter.format(timesheetHours)} + + + + + {t("Leave Hours")} + + + {manhourFormatter.format(leaveHours)} + + + + + + {t("Daily Total Hours")} + + + {manhourFormatter.format(timesheetHours + leaveHours)} + + + + + + + ); + })} + + + {`${t("Total Monthly Work Hours")}: ${manhourFormatter.format( + Object.values(timesheetForCurrentMonth) + .flatMap((entries) => entries) + .map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0)) + .reduce((acc, cur) => { + return acc + cur; + }, 0), + )}`} + + + {`${t("Total Monthly Leave Hours")}: ${manhourFormatter.format( + Object.values(leavesForCurrentMonth) + .flatMap((entries) => entries) + .map((entry) => entry.inputHours) + .reduce((acc, cur) => { + return acc + cur; + }, 0), + )}`} + + + ); +}; + +export default MonthlySummary; diff --git a/src/components/PastEntryCalendar/PastEntryCalendar.tsx b/src/components/PastEntryCalendar/PastEntryCalendar.tsx index a9e6a14..91c0012 100644 --- a/src/components/PastEntryCalendar/PastEntryCalendar.tsx +++ b/src/components/PastEntryCalendar/PastEntryCalendar.tsx @@ -26,6 +26,7 @@ export interface Props { timesheet: RecordTimesheetInput; leaves: RecordLeaveInput; onDateSelect: (date: string) => void; + onMonthChange: (day: Dayjs) => void; } const getColor = ( @@ -72,6 +73,7 @@ const PastEntryCalendar: React.FC = ({ timesheet, leaves, onDateSelect, + onMonthChange, }) => { const { i18n: { language }, @@ -88,6 +90,7 @@ const PastEntryCalendar: React.FC = ({ > { +interface Props + extends Omit { open: boolean; handleClose: () => void; leaveTypes: LeaveType[]; allProjects: ProjectWithTasks[]; + companyHolidays: HolidaysResult[]; } const Indicator = styled(Box)(() => ({ @@ -45,6 +50,7 @@ const PastEntryCalendarModal: React.FC = ({ const { t } = useTranslation("home"); const [selectedDate, setSelectedDate] = useState(""); + const [currentMonth, setMonthChange] = useState(dayjs()); const clearDate = useCallback(() => { setSelectedDate(""); @@ -54,40 +60,52 @@ const PastEntryCalendarModal: React.FC = ({ handleClose(); }, [handleClose]); - const content = selectedDate ? ( - <> - - - ) : ( - <> - - - - {t("Has timesheet entry")} - - - - {t("Has leave entry")} - - - - - {t("Has both timesheet and leave entry")} - - - - - + const content = ( + + + + + + + {t("Has timesheet entry")} + + + + + {t("Has leave entry")} + + + + + {t("Has both timesheet and leave entry")} + + + + + + {selectedDate ? ( + + ) : ( + + )} + ); const isMobile = useIsMobile(); @@ -115,14 +133,14 @@ const PastEntryCalendarModal: React.FC = ({ startIcon={} onClick={clearDate} > - {t("Back")} + {t("Back to Monthly Summary")} )} ) : ( - + {t("Past Entries")} {content} {selectedDate && ( @@ -132,7 +150,7 @@ const PastEntryCalendarModal: React.FC = ({ startIcon={} onClick={clearDate} > - {t("Back")} + {t("Back to Monthly Summary")} )} diff --git a/src/components/PastEntryCalendar/PastEntryList.tsx b/src/components/PastEntryCalendar/PastEntryList.tsx index 35fbb3c..a2a5d9c 100644 --- a/src/components/PastEntryCalendar/PastEntryList.tsx +++ b/src/components/PastEntryCalendar/PastEntryList.tsx @@ -57,7 +57,12 @@ const PastEntryList: React.FC = ({ const dayJsObj = dayjs(date); return ( - + = ({ leaveTypeMap={leaveTypeMap} /> ))} - - {t("Total Work Hours")}: {manhourFormatter.format(timeEntries.map(entry => (entry.inputHours ?? 0) + (entry.otHours ?? 0)).reduce((acc, cur) => { return acc + cur }, 0))} - - - {t("Total Leave Hours")}: {manhourFormatter.format(leaveEntries.map(entry => entry.inputHours).reduce((acc, cur) => { return acc + cur }, 0))} - + + {`${t("Total Work Hours")}: ${manhourFormatter.format( + timeEntries + .map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0)) + .reduce((acc, cur) => { + return acc + cur; + }, 0), + )}`} + + + {`${t("Total Leave Hours")}: ${manhourFormatter.format( + leaveEntries + .map((entry) => entry.inputHours) + .reduce((acc, cur) => { + return acc + cur; + }, 0), + )}`} + ); }; diff --git a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx index d102ce0..b858a60 100644 --- a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx +++ b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx @@ -102,7 +102,7 @@ const TimeLeaveInputTable: React.FC = ({ }, {}); }, [assignedProjects]); - const { getValues, setValue, clearErrors } = + const { getValues, setValue, clearErrors, setError } = useFormContext(); const currentEntries = getValues(day); @@ -486,8 +486,13 @@ const TimeLeaveInputTable: React.FC = ({ .filter((e): e is TimeLeaveEntry => Boolean(e)); setValue(day, newEntries); - clearErrors(day); - }, [getValues, entries, setValue, day, clearErrors]); + + if (entries.some((e) => e._isNew)) { + setError(day, { message: "There are some unsaved entries." }); + } else { + clearErrors(day); + } + }, [getValues, entries, setValue, day, clearErrors, setError]); const hasOutOfPlannedStages = entries.some( (entry) => entry._isPlanned !== undefined && !entry._isPlanned, diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index ac9aede..63ab31d 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -154,6 +154,7 @@ const UserWorkspacePage: React.FC = ({ leaves={defaultLeaveRecords} allProjects={allProjects} leaveTypes={leaveTypes} + companyHolidays={holidays} /> Date: Fri, 19 Jul 2024 12:00:33 +0800 Subject: [PATCH 08/14] tooltip auto width --- src/components/ProgressByClient/ProgressByClient.tsx | 2 +- src/components/ProgressByTeam/ProgressByTeam.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ProgressByClient/ProgressByClient.tsx b/src/components/ProgressByClient/ProgressByClient.tsx index f22d7d3..17324ee 100644 --- a/src/components/ProgressByClient/ProgressByClient.tsx +++ b/src/components/ProgressByClient/ProgressByClient.tsx @@ -409,7 +409,7 @@ const ProgressByClient: React.FC = () => { const spentManhours = chartProjectSpentHour[dataPointIndex]; const value = series[seriesIndex][dataPointIndex]; const tooltipContent = ` -
+
${projectCode} - ${projectName}
Budget Manhours: ${budgetManhours} hours diff --git a/src/components/ProgressByTeam/ProgressByTeam.tsx b/src/components/ProgressByTeam/ProgressByTeam.tsx index f855206..f1bf78c 100644 --- a/src/components/ProgressByTeam/ProgressByTeam.tsx +++ b/src/components/ProgressByTeam/ProgressByTeam.tsx @@ -492,7 +492,7 @@ const ProgressByTeam: React.FC = () => { const spentManhours = currentPageProjectSpentManhourList[dataPointIndex]; const value = series[seriesIndex][dataPointIndex]; const tooltipContent = ` -
+
${projectCode} - ${projectName}
Budget Manhours: ${budgetManhours} hours From be41356dff45008c3b1685a948b33cf2905c0d24 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Fri, 19 Jul 2024 12:08:11 +0800 Subject: [PATCH 09/14] update --- src/components/ProjectCashFlow/ProjectCashFlow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProjectCashFlow/ProjectCashFlow.tsx b/src/components/ProjectCashFlow/ProjectCashFlow.tsx index 0fd9df5..737f6a6 100644 --- a/src/components/ProjectCashFlow/ProjectCashFlow.tsx +++ b/src/components/ProjectCashFlow/ProjectCashFlow.tsx @@ -954,7 +954,7 @@ const ProjectCashFlow: React.FC = () => { className="text-sm font-medium ml-5" style={{ color: "#898d8d" }} > - Accounts Receivable + Remaining Budget
Date: Sun, 21 Jul 2024 16:38:22 +0900 Subject: [PATCH 10/14] Leave calendar --- src/components/LeaveModal/LeaveCalendar.tsx | 274 ++++++++++++++++++ src/components/LeaveModal/LeaveModal.tsx | 227 +++------------ .../UserWorkspacePage/UserWorkspacePage.tsx | 26 +- 3 files changed, 342 insertions(+), 185 deletions(-) create mode 100644 src/components/LeaveModal/LeaveCalendar.tsx diff --git a/src/components/LeaveModal/LeaveCalendar.tsx b/src/components/LeaveModal/LeaveCalendar.tsx new file mode 100644 index 0000000..be3341b --- /dev/null +++ b/src/components/LeaveModal/LeaveCalendar.tsx @@ -0,0 +1,274 @@ +import React, { useCallback, useMemo, useState } from "react"; + +import { HolidaysResult } from "@/app/api/holidays"; +import { LeaveType } from "@/app/api/timesheets"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import interactionPlugin from "@fullcalendar/interaction"; +import { Box, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { + getHolidayForDate, + getPublicHolidaysForNYears, +} from "@/app/utils/holidayUtils"; +import { + INPUT_DATE_FORMAT, + convertDateArrayToString, +} from "@/app/utils/formatUtil"; +import StyledFullCalendar from "../StyledFullCalendar"; +import { ProjectWithTasks } from "@/app/api/projects"; +import { + LeaveEntry, + RecordLeaveInput, + RecordTimesheetInput, + saveLeave, +} from "@/app/api/timesheets/actions"; +import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; +import LeaveEditModal from "../LeaveTable/LeaveEditModal"; +import dayjs from "dayjs"; +import { checkTotalHours } from "@/app/api/timesheets/utils"; +import unionBy from "lodash/unionBy"; + +export interface Props { + leaveTypes: LeaveType[]; + companyHolidays: HolidaysResult[]; + allProjects: ProjectWithTasks[]; + leaveRecords: RecordLeaveInput; + timesheetRecords: RecordTimesheetInput; +} + +interface EventClickArg { + event: { + start: Date | null; + startStr: string; + extendedProps: { + calendar?: string; + entry?: LeaveEntry; + }; + }; +} + +const LeaveCalendar: React.FC = ({ + companyHolidays, + allProjects, + leaveTypes, + timesheetRecords, + leaveRecords, +}) => { + const { t } = useTranslation(["home", "common"]); + + const theme = useTheme(); + + const projectMap = useMemo(() => { + return allProjects.reduce<{ + [id: ProjectWithTasks["id"]]: ProjectWithTasks; + }>((acc, project) => { + return { ...acc, [project.id]: project }; + }, {}); + }, [allProjects]); + + const leaveMap = useMemo(() => { + return leaveTypes.reduce<{ [id: LeaveType["id"]]: string }>( + (acc, leaveType) => ({ ...acc, [leaveType.id]: leaveType.name }), + {}, + ); + }, [leaveTypes]); + + const [localLeaveRecords, setLocalLeaveEntries] = useState(leaveRecords); + + // leave edit modal related + const [leaveEditModalProps, setLeaveEditModalProps] = useState< + Partial + >({}); + const [leaveEditModalOpen, setLeaveEditModalOpen] = useState(false); + + const openLeaveEditModal = useCallback( + (defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => { + setLeaveEditModalProps({ + defaultValues: defaultValues ? { ...defaultValues } : undefined, + recordDate, + isHoliday, + onDelete: defaultValues + ? async () => { + if (!recordDate || !leaveRecords[recordDate]) { + return; + } + const leaveEntriesAtDate = leaveRecords[recordDate]; + const newLeaveRecords = { + ...leaveRecords, + [recordDate!]: leaveEntriesAtDate.filter( + (e) => e.id !== defaultValues.id, + ), + }; + const savedLeaveRecords = await saveLeave(newLeaveRecords); + setLocalLeaveEntries(savedLeaveRecords); + setLeaveEditModalOpen(false); + } + : undefined, + }); + setLeaveEditModalOpen(true); + }, + [leaveRecords], + ); + + const closeLeaveEditModal = useCallback(() => { + setLeaveEditModalOpen(false); + }, []); + + // calendar related + const holidays = useMemo(() => { + return [ + ...getPublicHolidaysForNYears(2), + ...companyHolidays.map((h) => ({ + title: h.name, + date: convertDateArrayToString(h.date, INPUT_DATE_FORMAT), + extendedProps: { + calender: "holiday", + }, + })), + ].map((e) => ({ + ...e, + backgroundColor: theme.palette.error.main, + borderColor: theme.palette.error.main, + })); + }, [companyHolidays, theme.palette.error.main]); + + const leaveEntries = useMemo( + () => + Object.keys(localLeaveRecords).flatMap((date, index) => { + return localLeaveRecords[date].map((entry) => ({ + id: `${date}-${index}-leave-${entry.id}`, + date, + title: `${t("{{count}} hour", { + ns: "common", + count: entry.inputHours || 0, + })} (${leaveMap[entry.leaveTypeId]})`, + backgroundColor: theme.palette.warning.light, + borderColor: theme.palette.warning.light, + textColor: theme.palette.text.primary, + extendedProps: { + calendar: "leaveEntry", + entry, + }, + })); + }), + [leaveMap, localLeaveRecords, t, theme], + ); + + const timeEntries = useMemo( + () => + Object.keys(timesheetRecords).flatMap((date, index) => { + return timesheetRecords[date].map((entry) => ({ + id: `${date}-${index}-time-${entry.id}`, + date, + title: `${t("{{count}} hour", { + ns: "common", + count: (entry.inputHours || 0) + (entry.otHours || 0), + })} (${ + entry.projectId + ? projectMap[entry.projectId].code + : t("Non-billable task") + })`, + backgroundColor: theme.palette.info.main, + borderColor: theme.palette.info.main, + extendedProps: { + calendar: "timeEntry", + entry, + }, + })); + }), + [projectMap, timesheetRecords, t, theme], + ); + + const handleEventClick = useCallback( + ({ event }: EventClickArg) => { + const dayJsObj = dayjs(event.startStr); + const holiday = getHolidayForDate(event.startStr, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + + if ( + event.extendedProps.calendar === "leaveEntry" && + event.extendedProps.entry + ) { + openLeaveEditModal( + event.extendedProps.entry as LeaveEntry, + event.startStr, + Boolean(isHoliday), + ); + } + }, + [companyHolidays, openLeaveEditModal], + ); + + const handleDateClick = useCallback( + (e: { dateStr: string; dayEl: HTMLElement }) => { + const dayJsObj = dayjs(e.dateStr); + const holiday = getHolidayForDate(e.dateStr, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + + openLeaveEditModal(undefined, e.dateStr, Boolean(isHoliday)); + }, + [companyHolidays, openLeaveEditModal], + ); + + const checkTotalHoursForDate = useCallback( + (newEntry: LeaveEntry, date?: string) => { + if (!date) { + throw Error("Invalid date"); + } + const leaves = localLeaveRecords[date] || []; + const timesheets = timesheetRecords[date] || []; + + const leavesWithNewEntry = unionBy( + [newEntry as LeaveEntry], + leaves, + "id", + ); + + const totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); + + if (totalHourError) throw Error(totalHourError); + }, + [localLeaveRecords, timesheetRecords], + ); + + const handleSaveLeave = useCallback( + async (leaveEntry: LeaveEntry, recordDate?: string) => { + checkTotalHoursForDate(leaveEntry, recordDate); + const leaveEntriesAtDate = leaveRecords[recordDate!] || []; + const newLeaveRecords = { + ...leaveRecords, + [recordDate!]: [ + ...leaveEntriesAtDate.filter((e) => e.id !== leaveEntry.id), + leaveEntry, + ], + }; + const savedLeaveRecords = await saveLeave(newLeaveRecords); + setLocalLeaveEntries(savedLeaveRecords); + setLeaveEditModalOpen(false); + }, + [checkTotalHoursForDate, leaveRecords], + ); + + return ( + + + + + ); +}; + +export default LeaveCalendar; diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index 531b1aa..7ae85ba 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -1,46 +1,16 @@ -import React, { useCallback, useEffect, useMemo } from "react"; +import useIsMobile from "@/app/utils/useIsMobile"; +import React from "react"; +import FullscreenModal from "../FullscreenModal"; import { Box, - Button, Card, - CardActions, CardContent, Modal, - ModalProps, SxProps, Typography, } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { Check, Close } from "@mui/icons-material"; -import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; -import { - RecordLeaveInput, - RecordTimesheetInput, - saveLeave, -} from "@/app/api/timesheets/actions"; -import dayjs from "dayjs"; -import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; -import LeaveTable from "../LeaveTable"; -import { LeaveType } from "@/app/api/timesheets"; -import FullscreenModal from "../FullscreenModal"; -import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; -import useIsMobile from "@/app/utils/useIsMobile"; -import { HolidaysResult } from "@/app/api/holidays"; -import { - DAILY_NORMAL_MAX_HOURS, - TIMESHEET_DAILY_MAX_HOURS, - validateLeaveRecord, -} from "@/app/api/timesheets/utils"; -import ErrorAlert from "../ErrorAlert"; - -interface Props { - isOpen: boolean; - onClose: () => void; - defaultLeaveRecords?: RecordLeaveInput; - leaveTypes: LeaveType[]; - timesheetRecords: RecordTimesheetInput; - companyHolidays: HolidaysResult[]; -} +import LeaveCalendar, { Props as LeaveCalendarProps } from "./LeaveCalendar"; const modalSx: SxProps = { position: "absolute", @@ -52,167 +22,56 @@ const modalSx: SxProps = { maxWidth: 1400, }; +interface Props extends LeaveCalendarProps { + open: boolean; + onClose: () => void; +} + const LeaveModal: React.FC = ({ - isOpen, + open, onClose, - defaultLeaveRecords, - timesheetRecords, leaveTypes, companyHolidays, + allProjects, + leaveRecords, + timesheetRecords, }) => { const { t } = useTranslation("home"); + const isMobile = useIsMobile(); - const defaultValues = useMemo(() => { - const today = dayjs(); - return Array(7) - .fill(undefined) - .reduce((acc, _, index) => { - const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); - return { - ...acc, - [date]: defaultLeaveRecords?.[date] ?? [], - }; - }, {}); - }, [defaultLeaveRecords]); - - const formProps = useForm({ defaultValues }); - useEffect(() => { - formProps.reset(defaultValues); - }, [defaultValues, formProps]); - - const onSubmit = useCallback>( - async (data) => { - const errors = validateLeaveRecord( - data, - timesheetRecords, - companyHolidays, - ); - if (errors) { - Object.keys(errors).forEach((date) => - formProps.setError(date, { - message: errors[date], - }), - ); - return; - } - const savedRecords = await saveLeave(data); - - 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(); - }, - [companyHolidays, formProps, onClose, timesheetRecords], - ); - - const onCancel = useCallback(() => { - formProps.reset(defaultValues); - onClose(); - }, [defaultValues, formProps, onClose]); - - const onModalClose = useCallback>( - (_, reason) => { - if (reason !== "backdropClick") { - onCancel(); - } - }, - [onCancel], - ); - - const errorComponent = ( - { - const error = formProps.formState.errors[date]?.message; - return error - ? `${date}: ${t(error, { - TIMESHEET_DAILY_MAX_HOURS, - DAILY_NORMAL_MAX_HOURS, - })}` - : undefined; - })} + const title = t("Record leave"); + const content = ( + ); - const matches = useIsMobile(); - - return ( - - {!matches ? ( - // Desktop version - - - - - {t("Record Leave")} - - - - - {errorComponent} - - - - - - - - ) : ( - // Mobile version - - - - {t("Record Leave")} - - + return isMobile ? ( + + + + {title} + + {content} + + + ) : ( + + + + + {title} + + + {content} - - )} - + + + ); }; diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 63ab31d..e92b39d 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; -import { CalendarMonth, EditCalendar, MoreTime } from "@mui/icons-material"; +import { CalendarMonth, EditCalendar, Luggage, MoreTime } from "@mui/icons-material"; import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; import AssignedProjects from "./AssignedProjects"; import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; @@ -19,6 +19,7 @@ import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal" import { HolidaysResult } from "@/app/api/holidays"; import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; +import LeaveModal from "../LeaveModal"; export interface Props { leaveTypes: LeaveType[]; @@ -55,6 +56,7 @@ const UserWorkspacePage: React.FC = ({ const [anchorEl, setAnchorEl] = useState(null); const [isTimeLeaveModalVisible, setTimeLeaveModalVisible] = useState(false); + const [isLeaveCalendarVisible, setLeaveCalendarVisible] = useState(false); const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = useState(false); @@ -81,6 +83,15 @@ const UserWorkspacePage: React.FC = ({ setTimeLeaveModalVisible(false); }, []); + const handleOpenLeaveCalendarButton = useCallback(() => { + setAnchorEl(null); + setLeaveCalendarVisible(true); + }, []); + + const handleCloseLeaveCalendarButton = useCallback(() => { + setLeaveCalendarVisible(false); + }, []); + const handlePastEventClick = useCallback(() => { setAnchorEl(null); setPastEventModalVisible(true); @@ -136,6 +147,10 @@ const UserWorkspacePage: React.FC = ({ {t("Enter Timesheet")} + + + {t("Record Leave")} + {t("View Past Entries")} @@ -167,6 +182,15 @@ const UserWorkspacePage: React.FC = ({ timesheetRecords={defaultTimesheets} leaveRecords={defaultLeaveRecords} /> + {assignedProjects.length > 0 ? ( Date: Sun, 21 Jul 2024 17:25:52 +0900 Subject: [PATCH 11/14] 24, 25 non-negative hours and list view for assigned projects --- .../TimeLeaveModal/TimeLeaveInputTable.tsx | 8 +- .../UserWorkspacePage/ProjectGrid.tsx | 167 +++++++++++++----- 2 files changed, 125 insertions(+), 50 deletions(-) diff --git a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx index b858a60..2440741 100644 --- a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx +++ b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx @@ -393,7 +393,9 @@ const TimeLeaveInputTable: React.FC = ({ params.row._error?.[ params.field as keyof Omit ]; - const content = ; + const content = ( + + ); return errorMessage ? ( {content} @@ -423,7 +425,9 @@ const TimeLeaveInputTable: React.FC = ({ params.row._error?.[ params.field as keyof Omit ]; - const content = ; + const content = ( + + ); return errorMessage ? ( {content} diff --git a/src/components/UserWorkspacePage/ProjectGrid.tsx b/src/components/UserWorkspacePage/ProjectGrid.tsx index d7ec011..9eab16f 100644 --- a/src/components/UserWorkspacePage/ProjectGrid.tsx +++ b/src/components/UserWorkspacePage/ProjectGrid.tsx @@ -1,8 +1,19 @@ -import React from "react"; -import { Box, Card, CardContent, Grid, Typography } from "@mui/material"; +import React, { useCallback, useState } from "react"; +import { + Box, + Card, + CardContent, + Grid, + ToggleButton, + ToggleButtonGroup, + ToggleButtonProps, + Tooltip, + Typography, +} from "@mui/material"; import { useTranslation } from "react-i18next"; import { manhourFormatter } from "@/app/utils/formatUtil"; import { AssignedProject } from "@/app/api/projects"; +import { ViewList, ViewModule } from "@mui/icons-material"; interface Props { projects: AssignedProject[]; @@ -10,68 +21,128 @@ interface Props { maintainManagementStaffWorkspaceAbility?: boolean; } -const ProjectGrid: React.FC = ({ projects, maintainNormalStaffWorkspaceAbility, maintainManagementStaffWorkspaceAbility }) => { +const ProjectGrid: React.FC = ({ + projects, + maintainNormalStaffWorkspaceAbility, + maintainManagementStaffWorkspaceAbility, +}) => { const { t } = useTranslation("home"); + const [view, setView] = useState<"grid" | "list">("grid"); + + const handleViewChange = useCallback< + NonNullable + >((e, value) => { + if (value) { + setView(value); + } + }, []); + return ( - + + + + + + + + + + + + + {projects.map((project, idx) => ( - - + + {project.code} {project.name} + {/* Spacer */} + {/* Hours Spent */} - {(Boolean(maintainNormalStaffWorkspaceAbility) || Boolean(maintainManagementStaffWorkspaceAbility)) && <>{t("Hours Spent:")} - - {t("Normal")} - - {manhourFormatter.format(Boolean(maintainManagementStaffWorkspaceAbility) ? project.hoursSpent : project.currentStaffHoursSpent)} - - - - {t("Others")} - {`${manhourFormatter.format( - Boolean(maintainManagementStaffWorkspaceAbility) ? project.hoursSpentOther : project.currentStaffHoursSpentOther, - )}`} - } + {(Boolean(maintainNormalStaffWorkspaceAbility) || + Boolean(maintainManagementStaffWorkspaceAbility)) && ( + <> + + {t("Hours Spent:")} + + + {t("Normal")} + + {manhourFormatter.format( + Boolean(maintainManagementStaffWorkspaceAbility) + ? project.hoursSpent + : project.currentStaffHoursSpent, + )} + + + + {t("Others")} + {`${manhourFormatter.format( + Boolean(maintainManagementStaffWorkspaceAbility) + ? project.hoursSpentOther + : project.currentStaffHoursSpentOther, + )}`} + + + )} {/* Hours Allocated */} - {Boolean(maintainManagementStaffWorkspaceAbility) && - - {t("Hours Allocated:")} - - - {manhourFormatter.format(project.hoursAllocated)} - - } + {Boolean(maintainManagementStaffWorkspaceAbility) && ( + + + {t("Hours Allocated:")} + + + {manhourFormatter.format(project.hoursAllocated)} + + + )} From 3d551315cadb38283b5aa90e48277090d64dbedb Mon Sep 17 00:00:00 2001 From: Wayne Date: Sun, 21 Jul 2024 17:39:59 +0900 Subject: [PATCH 12/14] Remove unused code --- src/app/api/timesheets/utils.ts | 88 +-- src/components/EnterLeave/EnterLeaveModal.tsx | 109 ---- src/components/EnterLeave/LeaveInputGrid.tsx | 548 ------------------ src/components/EnterLeave/index.ts | 1 - .../EnterTimesheet/EnterTimesheetModal.tsx | 109 ---- .../EnterTimesheet/TimesheetInputGrid.tsx | 548 ------------------ src/components/EnterTimesheet/index.ts | 1 - .../TimesheetModal/TimesheetModal.tsx | 223 ------- src/components/TimesheetModal/index.ts | 1 - .../TimesheetTable/FastTimeEntryModal.tsx | 1 + 10 files changed, 4 insertions(+), 1625 deletions(-) delete mode 100644 src/components/EnterLeave/EnterLeaveModal.tsx delete mode 100644 src/components/EnterLeave/LeaveInputGrid.tsx delete mode 100644 src/components/EnterLeave/index.ts delete mode 100644 src/components/EnterTimesheet/EnterTimesheetModal.tsx delete mode 100644 src/components/EnterTimesheet/TimesheetInputGrid.tsx delete mode 100644 src/components/EnterTimesheet/index.ts delete mode 100644 src/components/TimesheetModal/TimesheetModal.tsx delete mode 100644 src/components/TimesheetModal/index.ts diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 714b1c8..81d798d 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -1,12 +1,6 @@ import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; import { HolidaysResult } from "../holidays"; -import { - LeaveEntry, - RecordLeaveInput, - RecordTimeLeaveInput, - RecordTimesheetInput, - TimeEntry, -} from "./actions"; +import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; import { convertDateArrayToString } from "@/app/utils/formatUtil"; import compact from "lodash/compact"; @@ -83,82 +77,6 @@ export const validateLeaveEntry = ( return Object.keys(error).length > 0 ? error : undefined; }; -export const validateTimesheet = ( - timesheet: RecordTimesheetInput, - leaveRecords: RecordLeaveInput, - companyHolidays: HolidaysResult[], -): { [date: string]: string } | undefined => { - const errors: { [date: string]: string } = {}; - - const holidays = new Set( - compact([ - ...getPublicHolidaysForNYears(2).map((h) => h.date), - ...companyHolidays.map((h) => convertDateArrayToString(h.date)), - ]), - ); - - Object.keys(timesheet).forEach((date) => { - const timeEntries = timesheet[date]; - - // Check each entry - for (const entry of timeEntries) { - const entryErrors = validateTimeEntry(entry, holidays.has(date)); - - if (entryErrors) { - errors[date] = "There are errors in the entries"; - return; - } - } - - // Check total hours - const leaves = leaveRecords[date] || []; - const totalHourError = checkTotalHours(timeEntries, leaves); - if (totalHourError) { - errors[date] = totalHourError; - } - }); - - return Object.keys(errors).length > 0 ? errors : undefined; -}; - -export const validateLeaveRecord = ( - leaveRecords: RecordLeaveInput, - timesheet: RecordTimesheetInput, - companyHolidays: HolidaysResult[], -): { [date: string]: string } | undefined => { - const errors: { [date: string]: string } = {}; - - const holidays = new Set( - compact([ - ...getPublicHolidaysForNYears(2).map((h) => h.date), - ...companyHolidays.map((h) => convertDateArrayToString(h.date)), - ]), - ); - - Object.keys(leaveRecords).forEach((date) => { - const leaves = leaveRecords[date]; - - // Check each leave entry - for (const entry of leaves) { - const entryError = validateLeaveEntry(entry, holidays.has(date)); - if (entryError) { - errors[date] = "There are errors in the entries"; - return; - } - } - - // Check total hours - const timeEntries = timesheet[date] || []; - - const totalHourError = checkTotalHours(timeEntries, leaves); - if (totalHourError) { - errors[date] = totalHourError; - } - }); - - return Object.keys(errors).length > 0 ? errors : undefined; -}; - export const validateTimeLeaveRecord = ( records: RecordTimeLeaveInput, companyHolidays: HolidaysResult[], @@ -191,8 +109,8 @@ export const validateTimeLeaveRecord = ( // Check total hours const totalHourError = checkTotalHours( - entries.filter((e) => e.type === "timeEntry"), - entries.filter((e) => e.type === "leaveEntry"), + entries.filter((e) => e.type === "timeEntry") as TimeEntry[], + entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], ); if (totalHourError) { diff --git a/src/components/EnterLeave/EnterLeaveModal.tsx b/src/components/EnterLeave/EnterLeaveModal.tsx deleted file mode 100644 index 97f0d3c..0000000 --- a/src/components/EnterLeave/EnterLeaveModal.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal } from "@mui/material"; -import TimesheetInputGrid from "./LeaveInputGrid"; - -// import { fetchLeaves } from "@/app/api/leave"; - -interface EnterTimesheetModalProps { - isOpen: boolean; - onClose: () => void; - modalStyle?: any; -} - -const EnterTimesheetModal: React.FC = ({ - ...props -}) => { - const [lockConfirm, setLockConfirm] = useState(false); - const columns = [ - { - id: "projectCode", - field: "projectCode", - headerName: "Project Code and Name", - flex: 1, - }, - { - id: "task", - field: "task", - headerName: "Task", - flex: 1, - }, - ]; - - const rows = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - }, - { - id: 2, - projectCode: "M1301", - task: "1.1", - }, - ]; - - const fetchTimesheet = async () => { - // fetchLeaves(); - // const res = await fetch(`http://localhost:8090/api/timesheets`, { - // // const res = await fetch(`${BASE_API_URL}/timesheets`, { - // method: "GET", - // mode: 'no-cors', - // }); - - // console.log(res.json); - }; - - return ( - -
- {/* -
- Record Leave -
-
*/} - - - -
- - -
-
-
-
- ); -}; - -export default EnterTimesheetModal; diff --git a/src/components/EnterLeave/LeaveInputGrid.tsx b/src/components/EnterLeave/LeaveInputGrid.tsx deleted file mode 100644 index 03eeaab..0000000 --- a/src/components/EnterLeave/LeaveInputGrid.tsx +++ /dev/null @@ -1,548 +0,0 @@ -"use client"; -import Grid from "@mui/material/Grid"; -import Paper from "@mui/material/Paper"; -import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import PageTitle from "../PageTitle/PageTitle"; -import { Suspense } from "react"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import Link from "next/link"; -import { t } from "i18next"; -import { - Box, - Container, - Modal, - Select, - SelectChangeEvent, - Typography, -} from "@mui/material"; -import { Close } from "@mui/icons-material"; -import AddIcon from "@mui/icons-material/Add"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/DeleteOutlined"; -import SaveIcon from "@mui/icons-material/Save"; -import CancelIcon from "@mui/icons-material/Close"; -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import Swal from "sweetalert2"; -import { msg } from "../Swal/CustomAlerts"; -import React from "react"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; -import { - GridRowsProp, - GridRowModesModel, - GridRowModes, - DataGrid, - GridColDef, - GridToolbarContainer, - GridFooterContainer, - GridActionsCellItem, - GridEventListener, - GridRowId, - GridRowModel, - GridRowEditStopReasons, - GridEditInputCell, - GridValueSetterParams, -} from "@mui/x-data-grid"; -import { LocalizationProvider } from "@mui/x-date-pickers"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import dayjs from "dayjs"; -import { Props } from "react-intl/src/components/relative"; - -const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; - -interface BottomBarProps { - getHoursTotal: (column: string) => number; - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditToolbarProps { - // setDay: (newDay : dayjs.Dayjs) => void; - setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditFooterProps { - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -const EditToolbar = (props: EditToolbarProps) => { - const { setDay } = props; - const [selectedDate, setSelectedDate] = useState(dayjs()); - - const handleClickLeft = () => { - if (selectedDate) { - const newDate = selectedDate.add(-7, "day"); - setSelectedDate(newDate); - } - }; - const handleClickRight = () => { - if (selectedDate) { - const newDate = - selectedDate.add(7, "day") > dayjs() - ? dayjs() - : selectedDate.add(7, "day"); - setSelectedDate(newDate); - } - }; - - const handleDateChange = (date: dayjs.Dayjs | Date | null) => { - const newDate = dayjs(date); - setSelectedDate(newDate); - }; - - useEffect(() => { - setDay((oldDay) => selectedDate); - }, [selectedDate]); - - return ( - -
- - Record Leave - - - - -
-
- ); -}; - -const BottomBar = (props: BottomBarProps) => { - const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; - // const getHoursTotal = props.getHoursTotal; - const [newId, setNewId] = useState(-1); - const [invalidDays, setInvalidDays] = useState(0); - - const handleAddClick = () => { - const id = newId; - setNewId(newId - 1); - setRows((oldRows) => [ - ...oldRows, - { id, projectCode: "", task: "", isNew: true }, - ]); - setRowModesModel((oldModel) => ({ - ...oldModel, - [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, - })); - }; - - const totalColDef = { - flex: 1, - // style: {color:getHoursTotal('mon')>24?"red":"black"} - }; - - const TotalCell = ({ value }: Props) => { - const [invalid, setInvalid] = useState(false); - - useEffect(() => { - const newInvalid = (value ?? 0) > 24; - setInvalid(newInvalid); - }, [value]); - - return ( - - {value} - - ); - }; - - const checkUnlockConfirmBtn = () => { - // setLockConfirm((oldLock)=> valid); - setLockConfirm((oldLock) => - weekdays.every((weekday) => { - getHoursTotal(weekday) <= 24; - }), - ); - }; - - return ( -
-
- - Total: - - - - - - - - -
- -
- ); -}; - -const EditFooter = (props: EditFooterProps) => { - return ( -
- - Total: - - ssss -
- ); -}; - -interface TimesheetInputGridProps { - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - onClose?: () => void; -} - -const initialRows: GridRowsProp = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - mon: 2.5, - }, - { - id: 2, - projectCode: "M1002", - task: "1.3", - mon: 3.25, - }, -]; - -const options = ["M1001", "M1301", "M1354", "M1973"]; -const options2 = [ - "1.1 - Preparation of preliminary Cost Estimate / Cost Plan", - "1.2 - Cash flow forecast", - "1.3 - Cost studies fo alterative design solutions", - "1.4 = Attend design co-ordination / project review meetings", - "1.5 - Prepare / Review RIC", -]; - -const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { - if (date.day() == 0) { - return date.add(weekday - date.day() - 7, "day").format("DD MMM"); - } else { - return date.add(weekday - date.day(), "day").format("DD MMM"); - } -}; - -const TimesheetInputGrid: React.FC = ({ - ...props -}) => { - const [rows, setRows] = useState(initialRows); - const [day, setDay] = useState(dayjs()); - const [rowModesModel, setRowModesModel] = React.useState( - {}, - ); - const { setLockConfirm } = props; - - const handleRowEditStop: GridEventListener<"rowEditStop"> = ( - params, - event, - ) => { - if (params.reason === GridRowEditStopReasons.rowFocusOut) { - event.defaultMuiPrevented = true; - } - }; - - const handleEditClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - }; - - const handleSaveClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }; - - const handleDeleteClick = (id: GridRowId) => () => { - setRows(rows.filter((row) => row.id !== id)); - }; - - const handleCancelClick = (id: GridRowId) => () => { - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, - }); - - const editedRow = rows.find((row) => row.id === id); - if (editedRow!.isNew) { - setRows(rows.filter((row) => row.id !== id)); - } - }; - - const processRowUpdate = (newRow: GridRowModel) => { - const updatedRow = { ...newRow, isNew: false }; - setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); - return updatedRow; - }; - - const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - setRowModesModel(newRowModesModel); - }; - - const getHoursTotal = (column: any) => { - let sum = 0; - rows.forEach((row) => { - sum += row[column] ?? 0; - }); - return sum; - }; - - const weekdayColConfig: any = { - type: "number", - // sortable: false, - //width: 100, - flex: 1, - align: "left", - headerAlign: "left", - editable: true, - renderEditCell: (value: any) => ( - - ), - }; - - const columns: GridColDef[] = [ - { - field: "actions", - type: "actions", - headerName: "Actions", - width: 100, - cellClassName: "actions", - getActions: ({ id }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - - if (isInEditMode) { - return [ - } - title="Save" - label="Save" - sx={{ - color: "primary.main", - }} - onClick={handleSaveClick(id)} - />, - } - title="Cancel" - label="Cancel" - className="textPrimary" - onClick={handleCancelClick(id)} - color="inherit" - />, - ]; - } - - return [ - } - title="Edit" - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" - />, - } - onClick={handleDeleteClick(id)} - sx={{ color: "red" }} - />, - ]; - }, - }, - { - field: "projectCode", - headerName: "Project Code", - // width: 220, - flex: 2, - editable: true, - type: "singleSelect", - valueOptions: options, - }, - { - field: "task", - headerName: "Task", - // width: 220, - flex: 3, - editable: true, - type: "singleSelect", - valueOptions: options2, - }, - { - // Mon - field: "mon", - ...weekdayColConfig, - renderHeader: () => { - return
Mon - {getDateForHeader(day, 1)}
; - }, - }, - { - // Tue - field: "tue", - ...weekdayColConfig, - renderHeader: () => { - return
Tue - {getDateForHeader(day, 2)}
; - }, - }, - { - // Wed - field: "wed", - ...weekdayColConfig, - renderHeader: () => { - return
Wed - {getDateForHeader(day, 3)}
; - }, - }, - { - // Thu - field: "thu", - ...weekdayColConfig, - renderHeader: () => { - return
Thu - {getDateForHeader(day, 4)}
; - }, - }, - { - // Fri - field: "fri", - ...weekdayColConfig, - renderHeader: () => { - return
Fri - {getDateForHeader(day, 5)}
; - }, - }, - { - // Sat - field: "sat", - ...weekdayColConfig, - renderHeader: () => { - return
Sat - {getDateForHeader(day, 6)}
; - }, - }, - { - // Sun - field: "sun", - ...weekdayColConfig, - renderHeader: () => { - return ( -
Sun - {getDateForHeader(day, 7)}
- ); - }, - }, - // { - // field: 'joinDate', - // headerName: 'Join date', - // type: 'date', - // width: 180, - // editable: true, - // }, - ]; - - return ( - - - - - - ); -}; - -export default TimesheetInputGrid; diff --git a/src/components/EnterLeave/index.ts b/src/components/EnterLeave/index.ts deleted file mode 100644 index 33541f2..0000000 --- a/src/components/EnterLeave/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EnterLeaveModal"; diff --git a/src/components/EnterTimesheet/EnterTimesheetModal.tsx b/src/components/EnterTimesheet/EnterTimesheetModal.tsx deleted file mode 100644 index d8854a1..0000000 --- a/src/components/EnterTimesheet/EnterTimesheetModal.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal } from "@mui/material"; -import TimesheetInputGrid from "./TimesheetInputGrid"; - -// import { fetchTimesheets } from "@/app/api/timesheets"; - -interface EnterTimesheetModalProps { - isOpen: boolean; - onClose: () => void; - modalStyle?: any; -} - -const EnterTimesheetModal: React.FC = ({ - ...props -}) => { - const [lockConfirm, setLockConfirm] = useState(false); - const columns = [ - { - id: "projectCode", - field: "projectCode", - headerName: "Project Code and Name", - flex: 1, - }, - { - id: "task", - field: "task", - headerName: "Task", - flex: 1, - }, - ]; - - const rows = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - }, - { - id: 2, - projectCode: "M1301", - task: "1.1", - }, - ]; - - const fetchTimesheet = async () => { - // fetchTimesheets(); - // const res = await fetch(`http://localhost:8090/api/timesheets`, { - // // const res = await fetch(`${BASE_API_URL}/timesheets`, { - // method: "GET", - // mode: 'no-cors', - // }); - - // console.log(res.json); - }; - - return ( - -
- {/* -
- Timesheet Input -
-
*/} - - - -
- - -
-
-
-
- ); -}; - -export default EnterTimesheetModal; diff --git a/src/components/EnterTimesheet/TimesheetInputGrid.tsx b/src/components/EnterTimesheet/TimesheetInputGrid.tsx deleted file mode 100644 index bc64c50..0000000 --- a/src/components/EnterTimesheet/TimesheetInputGrid.tsx +++ /dev/null @@ -1,548 +0,0 @@ -"use client"; -import Grid from "@mui/material/Grid"; -import Paper from "@mui/material/Paper"; -import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import PageTitle from "../PageTitle/PageTitle"; -import { Suspense } from "react"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import Link from "next/link"; -import { t } from "i18next"; -import { - Box, - Container, - Modal, - Select, - SelectChangeEvent, - Typography, -} from "@mui/material"; -import { Close } from "@mui/icons-material"; -import AddIcon from "@mui/icons-material/Add"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/DeleteOutlined"; -import SaveIcon from "@mui/icons-material/Save"; -import CancelIcon from "@mui/icons-material/Close"; -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import Swal from "sweetalert2"; -import { msg } from "../Swal/CustomAlerts"; -import React from "react"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; -import { - GridRowsProp, - GridRowModesModel, - GridRowModes, - DataGrid, - GridColDef, - GridToolbarContainer, - GridFooterContainer, - GridActionsCellItem, - GridEventListener, - GridRowId, - GridRowModel, - GridRowEditStopReasons, - GridEditInputCell, - GridValueSetterParams, -} from "@mui/x-data-grid"; -import { LocalizationProvider } from "@mui/x-date-pickers"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import dayjs from "dayjs"; -import { Props } from "react-intl/src/components/relative"; - -const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; - -interface BottomBarProps { - getHoursTotal: (column: string) => number; - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditToolbarProps { - // setDay: (newDay : dayjs.Dayjs) => void; - setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -interface EditFooterProps { - setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; - setRowModesModel: ( - newModel: (oldModel: GridRowModesModel) => GridRowModesModel, - ) => void; -} - -const EditToolbar = (props: EditToolbarProps) => { - const { setDay } = props; - const [selectedDate, setSelectedDate] = useState(dayjs()); - - const handleClickLeft = () => { - if (selectedDate) { - const newDate = selectedDate.add(-7, "day"); - setSelectedDate(newDate); - } - }; - const handleClickRight = () => { - if (selectedDate) { - const newDate = - selectedDate.add(7, "day") > dayjs() - ? dayjs() - : selectedDate.add(7, "day"); - setSelectedDate(newDate); - } - }; - - const handleDateChange = (date: dayjs.Dayjs | Date | null) => { - const newDate = dayjs(date); - setSelectedDate(newDate); - }; - - useEffect(() => { - setDay((oldDay) => selectedDate); - }, [selectedDate]); - - return ( - -
- - Timesheet Input - - - - -
-
- ); -}; - -const BottomBar = (props: BottomBarProps) => { - const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; - // const getHoursTotal = props.getHoursTotal; - const [newId, setNewId] = useState(-1); - const [invalidDays, setInvalidDays] = useState(0); - - const handleAddClick = () => { - const id = newId; - setNewId(newId - 1); - setRows((oldRows) => [ - ...oldRows, - { id, projectCode: "", task: "", isNew: true }, - ]); - setRowModesModel((oldModel) => ({ - ...oldModel, - [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, - })); - }; - - const totalColDef = { - flex: 1, - // style: {color:getHoursTotal('mon')>24?"red":"black"} - }; - - const TotalCell = ({ value }: Props) => { - const [invalid, setInvalid] = useState(false); - - useEffect(() => { - const newInvalid = (value ?? 0) > 24; - setInvalid(newInvalid); - }, [value]); - - return ( - - {value} - - ); - }; - - const checkUnlockConfirmBtn = () => { - // setLockConfirm((oldLock)=> valid); - setLockConfirm((oldLock) => - weekdays.every((weekday) => { - getHoursTotal(weekday) <= 24; - }), - ); - }; - - return ( -
-
- - Total: - - - - - - - - -
- -
- ); -}; - -const EditFooter = (props: EditFooterProps) => { - return ( -
- - Total: - - ssss -
- ); -}; - -interface TimesheetInputGridProps { - setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; - onClose?: () => void; -} - -const initialRows: GridRowsProp = [ - { - id: 1, - projectCode: "M1001", - task: "1.2", - mon: 2.5, - }, - { - id: 2, - projectCode: "M1002", - task: "1.3", - mon: 3.25, - }, -]; - -const options = ["M1001", "M1301", "M1354", "M1973"]; -const options2 = [ - "1.1 - Preparation of preliminary Cost Estimate / Cost Plan", - "1.2 - Cash flow forecast", - "1.3 - Cost studies fo alterative design solutions", - "1.4 = Attend design co-ordination / project review meetings", - "1.5 - Prepare / Review RIC", -]; - -const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { - if (date.day() == 0) { - return date.add(weekday - date.day() - 7, "day").format("DD MMM"); - } else { - return date.add(weekday - date.day(), "day").format("DD MMM"); - } -}; - -const TimesheetInputGrid: React.FC = ({ - ...props -}) => { - const [rows, setRows] = useState(initialRows); - const [day, setDay] = useState(dayjs()); - const [rowModesModel, setRowModesModel] = React.useState( - {}, - ); - const { setLockConfirm } = props; - - const handleRowEditStop: GridEventListener<"rowEditStop"> = ( - params, - event, - ) => { - if (params.reason === GridRowEditStopReasons.rowFocusOut) { - event.defaultMuiPrevented = true; - } - }; - - const handleEditClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); - }; - - const handleSaveClick = (id: GridRowId) => () => { - setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); - }; - - const handleDeleteClick = (id: GridRowId) => () => { - setRows(rows.filter((row) => row.id !== id)); - }; - - const handleCancelClick = (id: GridRowId) => () => { - setRowModesModel({ - ...rowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, - }); - - const editedRow = rows.find((row) => row.id === id); - if (editedRow!.isNew) { - setRows(rows.filter((row) => row.id !== id)); - } - }; - - const processRowUpdate = (newRow: GridRowModel) => { - const updatedRow = { ...newRow, isNew: false }; - setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); - return updatedRow; - }; - - const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - setRowModesModel(newRowModesModel); - }; - - const getHoursTotal = (column: any) => { - let sum = 0; - rows.forEach((row) => { - sum += row[column] ?? 0; - }); - return sum; - }; - - const weekdayColConfig: any = { - type: "number", - // sortable: false, - //width: 100, - flex: 1, - align: "left", - headerAlign: "left", - editable: true, - renderEditCell: (value: any) => ( - - ), - }; - - const columns: GridColDef[] = [ - { - field: "actions", - type: "actions", - headerName: "Actions", - width: 100, - cellClassName: "actions", - getActions: ({ id }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - - if (isInEditMode) { - return [ - } - title="Save" - label="Save" - sx={{ - color: "primary.main", - }} - onClick={handleSaveClick(id)} - />, - } - title="Cancel" - label="Cancel" - className="textPrimary" - onClick={handleCancelClick(id)} - color="inherit" - />, - ]; - } - - return [ - } - title="Edit" - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" - />, - } - onClick={handleDeleteClick(id)} - sx={{ color: "red" }} - />, - ]; - }, - }, - { - field: "projectCode", - headerName: "Project Code", - // width: 220, - flex: 2, - editable: true, - type: "singleSelect", - valueOptions: options, - }, - { - field: "task", - headerName: "Task", - // width: 220, - flex: 3, - editable: true, - type: "singleSelect", - valueOptions: options2, - }, - { - // Mon - field: "mon", - ...weekdayColConfig, - renderHeader: () => { - return
Mon - {getDateForHeader(day, 1)}
; - }, - }, - { - // Tue - field: "tue", - ...weekdayColConfig, - renderHeader: () => { - return
Tue - {getDateForHeader(day, 2)}
; - }, - }, - { - // Wed - field: "wed", - ...weekdayColConfig, - renderHeader: () => { - return
Wed - {getDateForHeader(day, 3)}
; - }, - }, - { - // Thu - field: "thu", - ...weekdayColConfig, - renderHeader: () => { - return
Thu - {getDateForHeader(day, 4)}
; - }, - }, - { - // Fri - field: "fri", - ...weekdayColConfig, - renderHeader: () => { - return
Fri - {getDateForHeader(day, 5)}
; - }, - }, - { - // Sat - field: "sat", - ...weekdayColConfig, - renderHeader: () => { - return
Sat - {getDateForHeader(day, 6)}
; - }, - }, - { - // Sun - field: "sun", - ...weekdayColConfig, - renderHeader: () => { - return ( -
Sun - {getDateForHeader(day, 7)}
- ); - }, - }, - // { - // field: 'joinDate', - // headerName: 'Join date', - // type: 'date', - // width: 180, - // editable: true, - // }, - ]; - - return ( - - - - - - ); -}; - -export default TimesheetInputGrid; diff --git a/src/components/EnterTimesheet/index.ts b/src/components/EnterTimesheet/index.ts deleted file mode 100644 index e070291..0000000 --- a/src/components/EnterTimesheet/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EnterTimesheetModal"; diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx deleted file mode 100644 index c6746d6..0000000 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import React, { useCallback, useEffect, useMemo } from "react"; -import { - Box, - Button, - Card, - CardActions, - CardContent, - Modal, - ModalProps, - SxProps, - Typography, -} from "@mui/material"; -import TimesheetTable from "../TimesheetTable"; -import { useTranslation } from "react-i18next"; -import { Check, Close } from "@mui/icons-material"; -import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; -import { - RecordLeaveInput, - RecordTimesheetInput, - saveTimesheet, -} from "@/app/api/timesheets/actions"; -import dayjs from "dayjs"; -import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; -import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; -import FullscreenModal from "../FullscreenModal"; -import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable"; -import useIsMobile from "@/app/utils/useIsMobile"; -import { HolidaysResult } from "@/app/api/holidays"; -import { - DAILY_NORMAL_MAX_HOURS, - TIMESHEET_DAILY_MAX_HOURS, - validateTimesheet, -} from "@/app/api/timesheets/utils"; -import ErrorAlert from "../ErrorAlert"; - -interface Props { - isOpen: boolean; - onClose: () => void; - allProjects: ProjectWithTasks[]; - assignedProjects: AssignedProject[]; - defaultTimesheets?: RecordTimesheetInput; - leaveRecords: RecordLeaveInput; - companyHolidays: HolidaysResult[]; - fastEntryEnabled?: boolean; -} - -const modalSx: SxProps = { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: { xs: "calc(100% - 2rem)", sm: "90%" }, - maxHeight: "90%", - maxWidth: 1400, -}; - -const TimesheetModal: React.FC = ({ - isOpen, - onClose, - allProjects, - assignedProjects, - defaultTimesheets, - leaveRecords, - companyHolidays, - fastEntryEnabled, -}) => { - const { t } = useTranslation("home"); - - const defaultValues = useMemo(() => { - const today = dayjs(); - return Array(7) - .fill(undefined) - .reduce((acc, _, index) => { - const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); - return { - ...acc, - [date]: defaultTimesheets?.[date] ?? [], - }; - }, {}); - }, [defaultTimesheets]); - - const formProps = useForm({ defaultValues }); - useEffect(() => { - formProps.reset(defaultValues); - }, [defaultValues, formProps]); - - const onSubmit = useCallback>( - async (data) => { - const errors = validateTimesheet(data, leaveRecords, companyHolidays); - if (errors) { - Object.keys(errors).forEach((date) => - formProps.setError(date, { - message: errors[date], - }), - ); - return; - } - const savedRecords = await saveTimesheet(data); - - 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(); - }, - [companyHolidays, formProps, leaveRecords, onClose], - ); - - const onCancel = useCallback(() => { - formProps.reset(defaultValues); - onClose(); - }, [defaultValues, formProps, onClose]); - - const onModalClose = useCallback>( - (_, reason) => { - if (reason !== "backdropClick") { - onClose(); - } - }, - [onClose], - ); - - const errorComponent = ( - { - const error = formProps.formState.errors[date]?.message; - return error - ? `${date}: ${t(error, { - TIMESHEET_DAILY_MAX_HOURS, - DAILY_NORMAL_MAX_HOURS, - })}` - : undefined; - })} - /> - ); - - const matches = useIsMobile(); - - return ( - - {!matches ? ( - // Desktop version - - - - - {t("Timesheet Input")} - - - - - {errorComponent} - - - - - - - - ) : ( - // Mobile version - - - - {t("Timesheet Input")} - - - - - )} - - ); -}; - -export default TimesheetModal; diff --git a/src/components/TimesheetModal/index.ts b/src/components/TimesheetModal/index.ts deleted file mode 100644 index c5197a7..0000000 --- a/src/components/TimesheetModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./TimesheetModal"; diff --git a/src/components/TimesheetTable/FastTimeEntryModal.tsx b/src/components/TimesheetTable/FastTimeEntryModal.tsx index 6181c49..c025b16 100644 --- a/src/components/TimesheetTable/FastTimeEntryModal.tsx +++ b/src/components/TimesheetTable/FastTimeEntryModal.tsx @@ -174,6 +174,7 @@ const FastTimeEntryModal: React.FC = ({ name="projectIds" render={({ field }) => ( Date: Sun, 21 Jul 2024 18:45:58 +0900 Subject: [PATCH 13/14] Add FT check for timesheet entry --- src/app/api/timesheets/index.ts | 2 ++ src/app/api/timesheets/utils.ts | 20 +++++++++++-- src/components/LeaveModal/LeaveCalendar.tsx | 15 ++++++++-- src/components/LeaveModal/LeaveModal.tsx | 2 ++ src/components/Logo/Logo.tsx | 6 ++-- .../TimeLeaveModal/TimeLeaveModal.tsx | 6 ++-- .../TimesheetAmendment/TimesheetAmendment.tsx | 18 +++++++++-- .../UserWorkspacePage/UserWorkspacePage.tsx | 11 ++++++- .../UserWorkspaceWrapper.tsx | 30 ++++++++++++++----- src/config/authConfig.ts | 7 +++-- 10 files changed, 93 insertions(+), 24 deletions(-) diff --git a/src/app/api/timesheets/index.ts b/src/app/api/timesheets/index.ts index ff0f5ad..9f0a0f3 100644 --- a/src/app/api/timesheets/index.ts +++ b/src/app/api/timesheets/index.ts @@ -13,6 +13,7 @@ export type TeamTimeSheets = { timeEntries: RecordTimesheetInput; staffId: string; name: string; + employType: string | null; }; }; @@ -21,6 +22,7 @@ export type TeamLeaves = { leaveEntries: RecordLeaveInput; staffId: string; name: string; + employType: string | null; }; }; diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 81d798d..10b128b 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -3,6 +3,7 @@ import { HolidaysResult } from "../holidays"; import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; import { convertDateArrayToString } from "@/app/utils/formatUtil"; import compact from "lodash/compact"; +import dayjs from "dayjs"; export type TimeEntryError = { [field in keyof TimeEntry]?: string; @@ -80,6 +81,7 @@ export const validateLeaveEntry = ( export const validateTimeLeaveRecord = ( records: RecordTimeLeaveInput, companyHolidays: HolidaysResult[], + isFullTime?: boolean, ): { [date: string]: string } | undefined => { const errors: { [date: string]: string } = {}; @@ -91,14 +93,18 @@ export const validateTimeLeaveRecord = ( ); Object.keys(records).forEach((date) => { + const dayJsObj = dayjs(date); + const isHoliday = + holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const entries = records[date]; // Check each entry for (const entry of entries) { let entryError; if (entry.type === "leaveEntry") { - entryError = validateLeaveEntry(entry, holidays.has(date)); + entryError = validateLeaveEntry(entry, isHoliday); } else { - entryError = validateTimeEntry(entry, holidays.has(date)); + entryError = validateTimeEntry(entry, isHoliday); } if (entryError) { @@ -111,6 +117,8 @@ export const validateTimeLeaveRecord = ( const totalHourError = checkTotalHours( entries.filter((e) => e.type === "timeEntry") as TimeEntry[], entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], + isHoliday, + isFullTime, ); if (totalHourError) { @@ -124,6 +132,8 @@ export const validateTimeLeaveRecord = ( export const checkTotalHours = ( timeEntries: TimeEntry[], leaves: LeaveEntry[], + isHoliday?: boolean, + isFullTime?: boolean, ): string | undefined => { const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); @@ -137,6 +147,12 @@ export const checkTotalHours = ( if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; + } else if ( + isFullTime && + !isHoliday && + totalInputHours + leaveHours !== DAILY_NORMAL_MAX_HOURS + ) { + return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}."; } else if ( totalInputHours + totalOtHours + leaveHours > TIMESHEET_DAILY_MAX_HOURS diff --git a/src/components/LeaveModal/LeaveCalendar.tsx b/src/components/LeaveModal/LeaveCalendar.tsx index be3341b..447f4a7 100644 --- a/src/components/LeaveModal/LeaveCalendar.tsx +++ b/src/components/LeaveModal/LeaveCalendar.tsx @@ -34,6 +34,7 @@ export interface Props { allProjects: ProjectWithTasks[]; leaveRecords: RecordLeaveInput; timesheetRecords: RecordTimesheetInput; + isFullTime: boolean; } interface EventClickArg { @@ -53,6 +54,7 @@ const LeaveCalendar: React.FC = ({ leaveTypes, timesheetRecords, leaveRecords, + isFullTime, }) => { const { t } = useTranslation(["home", "common"]); @@ -215,6 +217,10 @@ const LeaveCalendar: React.FC = ({ if (!date) { throw Error("Invalid date"); } + const dayJsObj = dayjs(date); + const holiday = getHolidayForDate(date, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const leaves = localLeaveRecords[date] || []; const timesheets = timesheetRecords[date] || []; @@ -224,11 +230,16 @@ const LeaveCalendar: React.FC = ({ "id", ); - const totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); + const totalHourError = checkTotalHours( + timesheets, + leavesWithNewEntry, + Boolean(isHoliday), + isFullTime, + ); if (totalHourError) throw Error(totalHourError); }, - [localLeaveRecords, timesheetRecords], + [companyHolidays, isFullTime, localLeaveRecords, timesheetRecords], ); const handleSaveLeave = useCallback( diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index 7ae85ba..3739bd7 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -35,6 +35,7 @@ const LeaveModal: React.FC = ({ allProjects, leaveRecords, timesheetRecords, + isFullTime, }) => { const { t } = useTranslation("home"); const isMobile = useIsMobile(); @@ -42,6 +43,7 @@ const LeaveModal: React.FC = ({ const title = t("Record leave"); const content = ( = ({ width, height }) => {
+ ))} +
+ )}
); }; diff --git a/src/components/UserWorkspacePage/ProjectTable.tsx b/src/components/UserWorkspacePage/ProjectTable.tsx new file mode 100644 index 0000000..66ead4f --- /dev/null +++ b/src/components/UserWorkspacePage/ProjectTable.tsx @@ -0,0 +1,106 @@ +import { useTranslation } from "react-i18next"; +import { Props } from "./ProjectGrid"; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import { useMemo } from "react"; +import { AssignedProject } from "@/app/api/projects"; +import { manhourFormatter } from "@/app/utils/formatUtil"; + +interface Column { + name: keyof AssignedProject; + label: string; +} + +const hourColumns: Array = [ + "currentStaffHoursSpent", + "currentStaffHoursSpentOther", + "hoursAllocated", + "hoursSpent", + "hoursSpentOther", +]; + +const ProjectTable: React.FC = ({ + projects, + maintainManagementStaffWorkspaceAbility, + maintainNormalStaffWorkspaceAbility, +}) => { + const { t } = useTranslation("home"); + const columns = useMemo(() => { + return [ + { name: "code", label: t("Project Code") }, + { name: "name", label: t("Project Name") }, + ...(maintainManagementStaffWorkspaceAbility || + maintainNormalStaffWorkspaceAbility + ? maintainManagementStaffWorkspaceAbility + ? ([ + { name: "hoursSpent", label: t("Total Normal Hours Spent") }, + { name: "hoursSpentOther", label: t("Total Other Hours Spent") }, + { name: "hoursAllocated", label: t("Hours Allocated") }, + ] satisfies Column[]) + : ([ + { + name: "currentStaffHoursSpent", + label: t("Normal Hours Spent"), + }, + { + name: "currentStaffHoursSpentOther", + label: t("Other Hours Spent"), + }, + ] satisfies Column[]) + : []), + ]; + }, [ + maintainManagementStaffWorkspaceAbility, + maintainNormalStaffWorkspaceAbility, + t, + ]); + + return ( + + + + + + {columns.map((column, idx) => ( + + {column.label} + + ))} + + + + {projects.map((project) => { + return ( + + {columns.map((column, idx) => { + const columnName = column.name; + const needsFormatting = hourColumns.includes(columnName); + + return ( + + {needsFormatting + ? manhourFormatter.format( + project[columnName] as number, + ) + : project[columnName]?.toString()} + + ); + })} + + ); + })} + +
+
+
+ ); +}; + +export default ProjectTable;