diff --git a/src/app/(main)/analytics/ProjectCompletionReport/page.tsx b/src/app/(main)/analytics/ProjectCompletionReport/page.tsx index 8a13941..e277d3a 100644 --- a/src/app/(main)/analytics/ProjectCompletionReport/page.tsx +++ b/src/app/(main)/analytics/ProjectCompletionReport/page.tsx @@ -1,24 +1,28 @@ //src\app\(main)\analytics\ProjectCompletionReport\page.tsx import { Metadata } from "next"; -import { I18nProvider } from "@/i18n"; +import { I18nProvider, getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; -import ProjectCompletionReportComponent from "@/components/Report/ProjectCompletionReport"; +import { Suspense } from "react"; +import ProjectCompletionReport from "@/components/ProjectCompletionReport"; export const metadata: Metadata = { title: "Project Completion Report", }; -const ProjectCompletionReport: React.FC = () => { +const ProjectCompletionReportPage: React.FC = async () => { + const { t } = await getServerI18n("report"); + return ( - - - Project Completion Report + <> + + {t("Project Completion Report")} - {/* }> - - */} - - + + }> + + + + ); }; -export default ProjectCompletionReport; +export default ProjectCompletionReportPage; diff --git a/src/app/(main)/analytics/ProjectPandLReport/page.tsx b/src/app/(main)/analytics/ProjectPandLReport/page.tsx new file mode 100644 index 0000000..156cc3d --- /dev/null +++ b/src/app/(main)/analytics/ProjectPandLReport/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from "next"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateProjectPandLReport from "@/components/GenerateProjectPandLReport"; +import { Typography } from "@mui/material"; + +export const metadata: Metadata = { + title: "Project Cash Flow Report", +}; + +const ProjectPandLReport: React.FC = async () => { + const { t } = await getServerI18n("reports"); + fetchProjects(); + + return ( + <> + + {t("Project P&L Report")} + + + }> + + + + + ); +}; + +export default ProjectPandLReport; diff --git a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx index a1751be..ae2085a 100644 --- a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx +++ b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx @@ -1,24 +1,28 @@ -//src\app\(main)\analytics\ResourceOvercomsumptionReport\page.tsx import { Metadata } from "next"; -import { I18nProvider } from "@/i18n"; -import Typography from "@mui/material/Typography"; -import ResourceOverconsumptionReportComponent from "@/components/Report/ResourceOverconsumptionReport"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import ResourceOverconsumptionReport from "@/components/ResourceOverconsumptionReport"; +import { Typography } from "@mui/material"; export const metadata: Metadata = { - title: "Resource Overconsumption Report", + title: "Staff Monthly Work Hours Analysis Report", }; -const ResourceOverconsumptionReport: React.FC = () => { - return ( - - - Resource Overconsumption Report - - {/* }> - - */} - - - ); +const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => { + const { t } = await getServerI18n("report"); + + return ( + <> + + {t("Project Resource Overconsumption Report")} + + + }> + + + + + ); }; -export default ResourceOverconsumptionReport; + +export default StaffMonthlyWorkHoursAnalysisReport; diff --git a/src/app/(main)/analytics/StaffMonthlyWorkHoursAnalysisReport/page.tsx b/src/app/(main)/analytics/StaffMonthlyWorkHoursAnalysisReport/page.tsx index 49ba399..ed9e686 100644 --- a/src/app/(main)/analytics/StaffMonthlyWorkHoursAnalysisReport/page.tsx +++ b/src/app/(main)/analytics/StaffMonthlyWorkHoursAnalysisReport/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { }; const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => { - const { t } = await getServerI18n("User Group"); + const { t } = await getServerI18n("report"); return ( <> diff --git a/src/app/(main)/projects/createSub/not-found.tsx b/src/app/(main)/projects/createSub/not-found.tsx new file mode 100644 index 0000000..9b28f5d --- /dev/null +++ b/src/app/(main)/projects/createSub/not-found.tsx @@ -0,0 +1,17 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("projects", "common"); + + return ( + + {t("Not Found")} + {t("There was no any main projects!")} + + {t("Return to all projects")} + + + ); +} diff --git a/src/app/(main)/projects/create/sub/page.tsx b/src/app/(main)/projects/createSub/page.tsx similarity index 98% rename from src/app/(main)/projects/create/sub/page.tsx rename to src/app/(main)/projects/createSub/page.tsx index 1a8fab7..3f474de 100644 --- a/src/app/(main)/projects/create/sub/page.tsx +++ b/src/app/(main)/projects/createSub/page.tsx @@ -21,7 +21,7 @@ import { Metadata } from "next"; import { notFound } from "next/navigation"; export const metadata: Metadata = { - title: "Create Project", + title: "Create Sub Project", }; const Projects: React.FC = async () => { diff --git a/src/app/(main)/projects/create/sub/not-found.tsx b/src/app/(main)/projects/editSub/not-found.tsx similarity index 90% rename from src/app/(main)/projects/create/sub/not-found.tsx rename to src/app/(main)/projects/editSub/not-found.tsx index 1cc4df3..234e436 100644 --- a/src/app/(main)/projects/create/sub/not-found.tsx +++ b/src/app/(main)/projects/editSub/not-found.tsx @@ -8,7 +8,7 @@ export default async function NotFound() { return ( {t("Not Found")} - {t("The sub project was not found or there was no any main projects!")} + {t("The sub project was not found!")} {t("Return to all projects")} diff --git a/src/app/(main)/projects/editSub/page.tsx b/src/app/(main)/projects/editSub/page.tsx new file mode 100644 index 0000000..eb4f5c6 --- /dev/null +++ b/src/app/(main)/projects/editSub/page.tsx @@ -0,0 +1,76 @@ +import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchGrades } from "@/app/api/grades"; +import { + fetchMainProjects, + fetchProjectBuildingTypes, + fetchProjectCategories, + fetchProjectContractTypes, + fetchProjectDetails, + fetchProjectFundingTypes, + fetchProjectLocationTypes, + fetchProjectServiceTypes, + fetchProjectWorkNatures, +} from "@/app/api/projects"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; +import CreateProject from "@/components/CreateProject"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { isArray } from "lodash"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; + +interface Props { + searchParams: { [key: string]: string | string[] | undefined }; +} + +export const metadata: Metadata = { + title: "Edit Sub Project", +}; + +const Projects: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("projects"); + const projectId = searchParams["id"]; + + if (!projectId || isArray(projectId)) { + notFound(); + } + + // Preload necessary dependencies + fetchAllTasks(); + fetchTaskTemplates(); + fetchProjectCategories(); + fetchProjectContractTypes(); + fetchProjectFundingTypes(); + fetchProjectLocationTypes(); + fetchProjectServiceTypes(); + fetchProjectBuildingTypes(); + fetchProjectWorkNatures(); + fetchAllCustomers(); + fetchAllSubsidiaries(); + fetchGrades(); + preloadTeamLeads(); + preloadStaff(); + + try { + await fetchProjectDetails(projectId); + const data = await fetchMainProjects(); + + if (!Boolean(data) || data.length === 0) { + notFound(); + } + } catch (e) { + notFound(); + } + + return ( + <> + {t("Edit Sub Project")} + + + + + ); +}; + +export default Projects; diff --git a/src/app/(main)/projects/page.tsx b/src/app/(main)/projects/page.tsx index 129d601..7cf44d0 100644 --- a/src/app/(main)/projects/page.tsx +++ b/src/app/(main)/projects/page.tsx @@ -43,7 +43,7 @@ const Projects: React.FC = async () => { color="secondary" startIcon={} LinkComponent={Link} - href="/projects/create/sub" + href="/projects/createSub" > {t("Create Sub Project")} } diff --git a/src/app/(main)/settings/changepassword/page.tsx b/src/app/(main)/settings/changepassword/page.tsx index b6b9a41..3de74eb 100644 --- a/src/app/(main)/settings/changepassword/page.tsx +++ b/src/app/(main)/settings/changepassword/page.tsx @@ -14,40 +14,33 @@ import { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; - export const metadata: Metadata = { - title: "Change Password", - }; + title: "Change Password", +}; +const ChangePasswordPage: React.FC = async () => { + const { t } = await getServerI18n("changePassword"); + // preloadTeamLeads(); + // preloadStaff(); + return ( + <> + + + {t("Change Password")} + + + + }> + + + + + ); +}; - const ChangePasswordPage: React.FC = async () => { - const { t } = await getServerI18n("User Group"); - // preloadTeamLeads(); - // preloadStaff(); - return ( - <> - - - {t("Change Password")} - - - {/* - }> - - - */} - - }> - - - - - ); - }; - - export default ChangePasswordPage; \ No newline at end of file +export default ChangePasswordPage; diff --git a/src/app/(main)/settings/group/create/page.tsx b/src/app/(main)/settings/group/create/page.tsx index 9130236..9460a71 100644 --- a/src/app/(main)/settings/group/create/page.tsx +++ b/src/app/(main)/settings/group/create/page.tsx @@ -11,7 +11,7 @@ const CreateStaff: React.FC = async () => { return ( <> - {t("Create Group")} + {t("Create User Group")} diff --git a/src/app/(main)/settings/group/page.tsx b/src/app/(main)/settings/group/page.tsx index 5322132..c602990 100644 --- a/src/app/(main)/settings/group/page.tsx +++ b/src/app/(main)/settings/group/page.tsx @@ -20,7 +20,7 @@ export const metadata: Metadata = { const UserGroup: React.FC = async () => { - const { t } = await getServerI18n("User Group"); + const { t } = await getServerI18n("group"); // preloadTeamLeads(); // preloadStaff(); return ( @@ -43,7 +43,7 @@ export const metadata: Metadata = { {t("Create User Group")} - + }> diff --git a/src/app/(main)/settings/staff/user/page.tsx b/src/app/(main)/settings/staff/user/page.tsx index 86373bb..b45dc04 100644 --- a/src/app/(main)/settings/staff/user/page.tsx +++ b/src/app/(main)/settings/staff/user/page.tsx @@ -7,19 +7,15 @@ import { Suspense } from "react"; import { preloadUser } from "@/app/api/user"; import { searchParamsProps } from "@/app/utils/fetchUtil"; -const User: React.FC = async ({ - searchParams -}) => { +const User: React.FC = async ({ searchParams }) => { const { t } = await getServerI18n("user"); - preloadUser() + preloadUser(); return ( <> {t("Edit User")} }> - + diff --git a/src/app/(main)/settings/user/edit/page.tsx b/src/app/(main)/settings/user/edit/page.tsx index fc36425..f820520 100644 --- a/src/app/(main)/settings/user/edit/page.tsx +++ b/src/app/(main)/settings/user/edit/page.tsx @@ -20,10 +20,10 @@ const EditUserPage: React.FC = async ({ return ( <> - + }> diff --git a/src/app/api/cashflow/index.ts b/src/app/api/cashflow/index.ts index 978a61e..f71ba74 100644 --- a/src/app/api/cashflow/index.ts +++ b/src/app/api/cashflow/index.ts @@ -1,4 +1,7 @@ +"use server"; import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; export interface CashFlow { id: number; @@ -19,21 +22,5 @@ export const preloadProjects = () => { }; export const fetchProjectsCashFlow = cache(async () => { - return mockProjects; + return serverFetchJson(`${BASE_API_URL}/dashboard/searchCashFlowProject`); }); - -const mockProjects: CashFlow[] = [ - { - id: 1, - projectCode: "CUST-001", - projectName: "Client A", - team: "N/A", - teamLeader: "N/A", - startDate: "5", - startDateFrom: "5", - startDateTo: "5", - targetEndDate: "s", - client: "ss", - subsidiary: "ss", - }, -]; diff --git a/src/app/api/clientprojects/index.ts b/src/app/api/clientprojects/index.ts index 56065d2..e1130a5 100644 --- a/src/app/api/clientprojects/index.ts +++ b/src/app/api/clientprojects/index.ts @@ -15,72 +15,10 @@ export interface ClientProjectResult { projectNo: number; } -// export interface ClientSubsidiaryProjectResult { -// projectId: number; -// projectCode: string; -// projectName: string; -// team: string; -// teamLead: string; -// expectedStage: string; -// budgetedManhour: number; -// spentManhour: number; -// remainedManhour: number; -// manhourConsumptionPercentage: number; -// comingPaymentMilestone: string; -// } - export const preloadClientProjects = () => { fetchAllClientProjects(); }; -// export const fetchClientProjects = cache(async () => { -// return mockProjects; -// }); - export const fetchAllClientProjects = cache(async () => { return serverFetchJson(`${BASE_API_URL}/dashboard/searchCustomerSubsidiary`); }); - -// export const fetchAllClientSubsidiaryProjects = cache(async (customerId: number, subsidiaryId: number) => { -// return serverFetchJson( -// `${BASE_API_URL}/dashboard/searchCustomerSubsidiaryProject/?${customerId}&${subsidiaryId}`, -// { -// next: { tags: [`allClientSubsidiaryProjects`] }, -// }, -// ); -// }); - -// const mockProjects: ClientProjectResult[] = [ -// { -// id: 1, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "N/A", -// SubsidiaryClientName: "N/A", -// NoOfProjects: 5, -// }, -// { -// id: 2, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "SUBS-001", -// SubsidiaryClientName: "Subsidiary A", -// NoOfProjects: 5, -// }, -// { -// id: 3, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "SUBS-002", -// SubsidiaryClientName: "Subsidiary B", -// NoOfProjects: 3, -// }, -// { -// id: 4, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "SUBS-003", -// SubsidiaryClientName: "Subsidiary C", -// NoOfProjects: 1, -// }, -// ]; diff --git a/src/app/api/financialsummary/actions.ts b/src/app/api/financialsummary/actions.ts new file mode 100644 index 0000000..c1dda65 --- /dev/null +++ b/src/app/api/financialsummary/actions.ts @@ -0,0 +1,66 @@ +"use server"; + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { Dayjs } from "dayjs"; +import { cache } from "react"; + + +export interface FinancialSummaryByClientResult { + teamId:number; + id:number; + customerCode: string; + customerName: string; + projectNo: number; + totalFee: number; + totalBudget: number; + cumulativeExpenditure: number; + totalInvoiced: number; + totalReceived: number; + cashFlowStatus: string; + cpi: number; + totalUninvoiced: number; +} + +export interface FinancialSummaryByProjectResult { + teamId:number; + id:number; + projectCode: string; + projectName: string; + customerName: string; + projectNo: number; + totalFee: number; + totalBudget: number; + cumulativeExpenditure: number; + totalInvoiced: number; + totalReceived: number; + cashFlowStatus: string; + cpi: number; + totalUninvoiced: number; +} + +export const searchFinancialSummaryByClient = cache(async (teamId?: number) => { + if (teamId === undefined) { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByClient` + ); + } else { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByClient?teamId=${teamId}` + ); + } + +}); + +export const searchFinancialSummaryByProject = cache(async (teamId?: number, customerId?:number) => { + if (teamId === undefined) { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByProject` + ); + } else { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByProject?teamId=${teamId}&customerId=${customerId}` + ); + } + +}); diff --git a/src/app/api/financialsummary/index.ts b/src/app/api/financialsummary/index.ts new file mode 100644 index 0000000..7e9d78a --- /dev/null +++ b/src/app/api/financialsummary/index.ts @@ -0,0 +1,27 @@ +"use server"; +import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +// import "server-only"; + + +export interface FinancialSummaryCardResult { + teamId: number; + teamName: string; + projectNo: number; + totalFee: number; + totalBudget: number; + cumulativeExpenditure: number; + totalInvoiced: number; + totalReceived: number; + cashFlowStatus: string; + cpi: number; +} + +export const preloadFinancialSummaryCard = () => { + fetchFinancialSummaryCard(); +}; + +export const fetchFinancialSummaryCard = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/dashboard/searchFinancialSummaryCard`); +}); diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index 97cb34e..31ab2c9 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -36,8 +36,8 @@ export interface CreateProjectInputs { // Client details clientId: Customer["id"]; clientContactId?: number; - clientSubsidiaryId?: number; - subsidiaryContactId: number; + clientSubsidiaryId?: number | null; + subsidiaryContactId?: number; isSubsidiaryContact?: boolean; // Allocation diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index d25ba16..6833571 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -13,6 +13,7 @@ export interface ProjectResult { team: string; client: string; status: string; + mainProject: string; } export interface MainProject { diff --git a/src/app/api/report3/index.ts b/src/app/api/report3/index.ts deleted file mode 100644 index b2c8751..0000000 --- a/src/app/api/report3/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -//src\app\api\report\index.ts -import { cache } from "react"; - -export interface ResourceOverconsumption { - id: number; - projectCode: string; - projectName: string; - team: string; - teamLeader: string; - startDate: string; - startDateFrom: string; - startDateTo: string; - targetEndDate: string; - client: string; - subsidiary: string; - status: string; -} - -export const preloadProjects = () => { - fetchProjectsResourceOverconsumption(); -}; - -export const fetchProjectsResourceOverconsumption = cache(async () => { - return mockProjects; -}); - -const mockProjects: ResourceOverconsumption[] = [ - { - id: 1, - projectCode: "CUST-001", - projectName: "Client A", - team: "N/A", - teamLeader: "N/A", - startDate: "1/2/2024", - startDateFrom: "1/2/2024", - startDateTo: "1/2/2024", - targetEndDate: "30/3/2024", - client: "ss", - subsidiary: "sus", - status: "1", - }, -]; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index f86f044..c78f31f 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -1,7 +1,7 @@ "use server"; import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; -import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest } from "."; +import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest, ProjectCompletionReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; export interface FileResponse { @@ -35,6 +35,32 @@ export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRe return reportBlob }; +export const fetchProjectResourceOverconsumptionReport = async (data: ProjectResourceOverconsumptionReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/ProjectResourceOverconsumptionReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; + +export const fetchProjectCompletionReport = async (data: ProjectCompletionReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/ProjectCompletionReportwithOutstandingAccountsReceivable`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; + // export const fetchLateStartReport = async (data: LateStartReportRequest) => { // const response = await serverFetchBlob( // `${BASE_API_URL}/reports/downloadLateStartReport`, @@ -57,3 +83,16 @@ export const fetchLateStartReport = async (data: LateStartReportRequest) => { return response }; +export const fetchProjectPandLReport = async (data: ProjectPandLReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/projectpandlreport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; + diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index fd10c99..361d531 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -4,6 +4,19 @@ export interface FinancialStatusReportFilter { project: string[]; } +// - Project P&L Report +export interface ProjectPandLReportFilter { + project: string[]; + startMonth: string; + startMonthTo: string; +} + +export interface ProjectPandLReportRequest { + projectId: number; + startMonth: string; + endMonth: string; +} + // - Project Cash Flow Report export interface ProjectCashFlowReportFilter { project: string[]; @@ -15,6 +28,7 @@ export interface ProjectCashFlowReportRequest { dateType: string; } + // - Monthly Work Hours Report export interface MonthlyWorkHoursReportFilter { staff: string[]; @@ -25,6 +39,20 @@ export interface MonthlyWorkHoursReportRequest { id: number; yearMonth: string; } +// - Project Resource Overconsumption Report +export interface ProjectResourceOverconsumptionReportFilter { + team: string[]; + customer: string[]; + status: string[]; + lowerLimit: number; +} + +export interface ProjectResourceOverconsumptionReportRequest { + teamId?: number + custId?: number + status: "All" | "Within Budget" | "Potential Overconsumption" | "Overconsumption" + lowerLimit: number +} export interface LateStartReportFilter { remainedDays: number; @@ -38,3 +66,15 @@ export interface LateStartReportRequest { remainedDate: string; remainedDateTo: string; } + +export interface ProjectCompletionReportFilter { + startDate: String; + startDateTo: String; + outstanding: String; +} + +export interface ProjectCompletionReportRequest { + startDate: String; + endDate: String; + outstanding: Boolean; +} diff --git a/src/app/api/teamprojects/actions.ts b/src/app/api/teamprojects/actions.ts new file mode 100644 index 0000000..2460a6b --- /dev/null +++ b/src/app/api/teamprojects/actions.ts @@ -0,0 +1,29 @@ +"use server"; + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { Dayjs } from "dayjs"; +import { cache } from "react"; + + +export interface ClientSubsidiaryProjectResult { + color: string; + projectId: number; + projectCode: string; + projectName: string; + team: string; + teamLead: string; + expectedStage: string; + budgetedManhour: number; + spentManhour: number; + remainedManhour: number; + manhourConsumptionPercentage: number; + comingPaymentMilestone: string; +} + +export const fetchAllTeamProjects = cache(async (teamLeadId: number) => { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchTeamProject?teamLeadId=${teamLeadId}` + ); + +}); diff --git a/src/app/api/teamprojects/index.ts b/src/app/api/teamprojects/index.ts index bba7ba9..59b2a3e 100644 --- a/src/app/api/teamprojects/index.ts +++ b/src/app/api/teamprojects/index.ts @@ -1,10 +1,15 @@ import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import "server-only"; export interface TeamProjectResult { id: number; + teamId: number; + teamLeadId: number; teamCode: string; teamName: string; - NoOfProjects: number; + projectNo: number; } export const preloadProjects = () => { @@ -12,32 +17,6 @@ export const preloadProjects = () => { }; export const fetchTeamProjects = cache(async () => { - return mockProjects; + return serverFetchJson(`${BASE_API_URL}/dashboard/searchTeamProjectNo`); }); -const mockProjects: TeamProjectResult[] = [ - { - id: 1, - teamCode: "TEAM-001", - teamName: "Team A", - NoOfProjects: 5, - }, - { - id: 2, - teamCode: "TEAM-002", - teamName: "Team B", - NoOfProjects: 5, - }, - { - id: 3, - teamCode: "TEAM-003", - teamName: "Team C", - NoOfProjects: 3, - }, - { - id: 4, - teamCode: "TEAM-004", - teamName: "Team D", - NoOfProjects: 1, - }, -]; diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 22d31ce..7a6dad3 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -1,4 +1,13 @@ -import { LeaveEntry, TimeEntry } from "./actions"; +import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; +import { HolidaysResult } from "../holidays"; +import { + LeaveEntry, + RecordLeaveInput, + RecordTimesheetInput, + TimeEntry, +} from "./actions"; +import { convertDateArrayToString } from "@/app/utils/formatUtil"; +import compact from "lodash/compact"; export type TimeEntryError = { [field in keyof TimeEntry]?: string; @@ -6,7 +15,7 @@ export type TimeEntryError = { /** * @param entry - the time entry - * @returns the field where there is an error, or an empty string if there is none + * @returns an object where the keys are the error fields and the values the error message, and undefined if there are no errors */ export const validateTimeEntry = ( entry: Partial, @@ -58,6 +67,61 @@ export const isValidLeaveEntry = (entry: Partial): string => { return error; }; +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 leaveHours = + leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; + + const totalNormalHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.inputHours || 0); + }, 0); + + const totalOtHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.otHours || 0); + }, 0); + + if (totalNormalHours > DAILY_NORMAL_MAX_HOURS) { + errors[date] = + "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours."; + } else if ( + totalNormalHours + totalOtHours + leaveHours > + TIMESHEET_DAILY_MAX_HOURS + ) { + errors[date] = + "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; + } + }); + + return Object.keys(errors).length > 0 ? errors : undefined; +}; + export const DAILY_NORMAL_MAX_HOURS = 8; export const LEAVE_DAILY_MAX_HOURS = 8; export const TIMESHEET_DAILY_MAX_HOURS = 20; diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 8c1b9b0..77a4eda 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -13,11 +13,12 @@ import { I18nProvider } from "@/i18n"; const pathToLabelMap: { [path: string]: string } = { "": "Overview", "/home": "User Workspace", + "/dashboard": "Dashboard", "/projects": "Projects", "/projects/create": "Create Project", - "/projects/create/sub": "Sub Project", + "/projects/createSub": "Sub Project", "/projects/edit": "Edit Project", - "/projects/edit/sub": "Sub Project", + "/projects/editSub": "Sub Project", "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/staffReimbursement": "Staff Reimbursement", diff --git a/src/components/ChangePassword/ChangePasswordForm.tsx b/src/components/ChangePassword/ChangePasswordForm.tsx index 19e2a29..ff2965e 100644 --- a/src/components/ChangePassword/ChangePasswordForm.tsx +++ b/src/components/ChangePassword/ChangePasswordForm.tsx @@ -14,7 +14,7 @@ import { Visibility, VisibilityOff } from "@mui/icons-material"; import { IconButton, InputAdornment } from "@mui/material"; const ChagnePasswordForm: React.FC = () => { - const { t } = useTranslation(); + const { t } = useTranslation("changePassword"); const [showNewPassword, setShowNewPassword] = useState(false); const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword); @@ -33,19 +33,12 @@ const ChagnePasswordForm: React.FC = () => { setValue, } = useFormContext(); - // const resetGroup = useCallback(() => { - // console.log(defaultValues); - // if (defaultValues !== undefined) { - // resetField("description"); - // } - // }, [defaultValues]); - return ( - {t("Group Info")} + {t("Please Fill in All Fields")} diff --git a/src/components/ChangePassword/ChangePasswordWrapper.tsx b/src/components/ChangePassword/ChangePasswordWrapper.tsx index 30acb9d..2de12db 100644 --- a/src/components/ChangePassword/ChangePasswordWrapper.tsx +++ b/src/components/ChangePassword/ChangePasswordWrapper.tsx @@ -7,10 +7,6 @@ interface SubComponents { } const ChangePasswordWrapper: React.FC & SubComponents = async () => { - // const records = await fetchAuth() - // const users = await fetchUser() - // console.log(users) - // const auth = records.records as auth[] return ; }; diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx index e37f348..ed7e81b 100644 --- a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx +++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx @@ -1,6 +1,6 @@ "use client" -import { Autocomplete, MenuItem, TextField, Checkbox } from "@mui/material"; +import { Autocomplete, MenuItem, TextField, Checkbox, Chip } from "@mui/material"; import { Controller, FieldValues, Path, Control, RegisterOptions } from "react-hook-form"; import { useTranslation } from "react-i18next"; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; @@ -18,6 +18,7 @@ interface Props + disabled?: boolean, } function ControlledAutoComplete< @@ -27,11 +28,10 @@ function ControlledAutoComplete< props: Props ) { const { t } = useTranslation() - const { control, options, name, label, noOptionsText, isMultiple, rules } = props; + const { control, options, name, label, noOptionsText, isMultiple, rules, disabled } = props; // set default value if value is null if (!Boolean(isMultiple) && !Boolean(control._formValues[name])) { - console.log(name, control._formValues[name]) control._formValues[name] = options[0]?.id ?? undefined } else if (Boolean(isMultiple) && !Boolean(control._formValues[name])) { control._formValues[name] = [] @@ -42,7 +42,6 @@ function ControlledAutoComplete< name={name} control={control} rules={rules} - render={({ field, fieldState, formState }) => { return ( @@ -51,7 +50,8 @@ function ControlledAutoComplete< multiple disableClearable disableCloseOnSelect - disablePortal + // disablePortal + disabled={disabled} noOptionsText={noOptionsText ?? t("No Options")} value={options.filter(option => { return field.value?.includes(option.id) @@ -61,7 +61,7 @@ function ControlledAutoComplete< isOptionEqualToValue={(option, value) => option.id === value.id} renderOption={(params, option, { selected }) => { return ( -
  • +
  • ); }} + renderTags={(tagValue, getTagProps) => { + return tagValue.map((option, index) => ( + + )) + }} onChange={(event, value) => { field.onChange(value?.map(v => v.id)) }} @@ -80,7 +85,8 @@ function ControlledAutoComplete< : option.id === field.value) ?? options[0]} options={options} @@ -88,13 +94,18 @@ function ControlledAutoComplete< isOptionEqualToValue={(option, value) => option?.id === value?.id} renderOption={(params, option) => { return ( - + {option.label ?? option.name} ); }} + renderTags={(tagValue, getTagProps) => { + return tagValue.map((option, index) => ( + + )) + }} onChange={(event, value) => { - field.onChange(value?.id) + field.onChange(value?.id ?? null) }} renderInput={(params) => } />) diff --git a/src/components/CreateGroup/AuthorityAllocation.tsx b/src/components/CreateGroup/AuthorityAllocation.tsx index bdd4ccb..794b98d 100644 --- a/src/components/CreateGroup/AuthorityAllocation.tsx +++ b/src/components/CreateGroup/AuthorityAllocation.tsx @@ -84,8 +84,8 @@ const AuthorityAllocation: React.FC = ({ auth }) => { onClick: addAuth, buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, // { label: t("Current Position"), name: "currentPosition" }, ], [addAuth, t] @@ -97,10 +97,10 @@ const AuthorityAllocation: React.FC = ({ auth }) => { label: t("Remove"), name: "id", onClick: removeAuth, - buttonIcon: , + buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, ], [removeAuth, selectedAuths, t] ); @@ -115,16 +115,13 @@ const AuthorityAllocation: React.FC = ({ auth }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + setFilteredAuths( + initialAuths.filter( + (a) => + a.authority.toLowerCase().includes(query.toLowerCase()) || + a.name?.toLowerCase().includes(query.toLowerCase()) + ) + ); }, [auth, query]); const resetAuth = React.useCallback(() => { @@ -152,7 +149,7 @@ const AuthorityAllocation: React.FC = ({ auth }) => { > - {t("Authority")} + {/* {t("Authority")} */} @@ -162,7 +159,7 @@ const AuthorityAllocation: React.FC = ({ auth }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ") + t("Authority") + " / " + t("Description")} InputProps={{ endAdornment: query && ( @@ -178,18 +175,20 @@ const AuthorityAllocation: React.FC = ({ auth }) => { - {tabIndex === 0 && ( + {tabIndex === 0 && ( )} - {tabIndex === 1 && ( + {tabIndex === 1 && ( = ({ auth, users }) => { const [serverError, setServerError] = useState(""); const router = useRouter(); const [tabIndex, setTabIndex] = useState(0); - const { t } = useTranslation(); + const { t } = useTranslation("group"); const errors = formProps.formState.errors; diff --git a/src/components/CreateGroup/GroupInfo.tsx b/src/components/CreateGroup/GroupInfo.tsx index d9141bc..5792050 100644 --- a/src/components/CreateGroup/GroupInfo.tsx +++ b/src/components/CreateGroup/GroupInfo.tsx @@ -51,13 +51,13 @@ const GroupInfo: React.FC = () => { Boolean(errors.name) && (errors.name?.message ? t(errors.name.message) - : t("Please input correct name")) + : t("Please input correct ") + t("Group Name")) } /> { Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) - : t("Please input correct description")) + : t("Please input correct ") + t("Description")) } /> diff --git a/src/components/CreateGroup/UserAllocation.tsx b/src/components/CreateGroup/UserAllocation.tsx index ff13c52..351303a 100644 --- a/src/components/CreateGroup/UserAllocation.tsx +++ b/src/components/CreateGroup/UserAllocation.tsx @@ -85,8 +85,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: addUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [addUser, t] ); @@ -99,8 +99,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: removeUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [removeUser, selectedUsers, t] ); @@ -116,16 +116,13 @@ const UserAllocation: React.FC = ({ users }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + const q = query.toLowerCase(); + setFilteredUsers( + initialUsers.filter((u) => ( + u.username.toLowerCase().includes(q) || + u.name.toLowerCase().includes(q) + )) + ); }, [users, query]); const resetUser = React.useCallback(() => { @@ -153,7 +150,7 @@ const UserAllocation: React.FC = ({ users }) => { > - {t("User")} + {/* {t("User")} */} @@ -163,7 +160,7 @@ const UserAllocation: React.FC = ({ users }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ") + t("Username") + " / " + t("Staff Name")} InputProps={{ endAdornment: query && ( diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 5f35919..d115f3a 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -393,6 +393,7 @@ const CreateProject: React.FC = ({ projectCategories={projectCategories} teamLeads={teamLeads} isActive={tabIndex === 0} + isEditMode={isEditMode} /> } { diff --git a/src/components/CreateProject/Milestone.tsx b/src/components/CreateProject/Milestone.tsx index c91c0fa..d854af9 100644 --- a/src/components/CreateProject/Milestone.tsx +++ b/src/components/CreateProject/Milestone.tsx @@ -4,16 +4,18 @@ import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import { useTranslation } from "react-i18next"; import Button from "@mui/material/Button"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { SyntheticEvent, useCallback, useEffect, useMemo, useState } from "react"; import CardActions from "@mui/material/CardActions"; import RestartAlt from "@mui/icons-material/RestartAlt"; import { Alert, + Autocomplete, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, + TextField, } from "@mui/material"; import { Task, TaskGroup } from "@/app/api/tasks"; import uniqBy from "lodash/uniqBy"; @@ -48,13 +50,23 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { const [currentTaskGroupId, setCurrentTaskGroupId] = useState( taskGroups[0].id, ); - const onSelectTaskGroup = useCallback( + + /*const onSelectTaskGroup = useCallback( (event: SelectChangeEvent) => { const id = event.target.value; const newTaksGroupId = typeof id === "string" ? parseInt(id) : id; setCurrentTaskGroupId(newTaksGroupId); }, [], + );*/ + + const onSelectTaskGroup = useCallback( + (event: SyntheticEvent, value: NonNullable) => { + const id = value.id; + const newTaksGroupId = typeof id === "string" ? parseInt(id) : id; + setCurrentTaskGroupId(newTaksGroupId); + }, + [], ); // handle error checking @@ -81,7 +93,7 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { } // console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) if (hasError) { - setError("milestones", {message: "milestones is not valid", type: "invalid"}) + setError("milestones", { message: "milestones is not valid", type: "invalid" }) } else { clearErrors("milestones") } @@ -92,26 +104,32 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { - {t("Task Stage")} - + renderInput={(params) => } + /> {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} {isActive && } - + {/* - + */} diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 8fb7fab..bce421a 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -39,6 +39,7 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp interface Props { isActive: boolean; isSubProject: boolean; + isEditMode: boolean; mainProjects?: MainProject[]; projectCategories: ProjectCategory[]; teamLeads: StaffResult[]; @@ -55,6 +56,7 @@ interface Props { const ProjectClientDetails: React.FC = ({ isActive, isSubProject, + isEditMode, mainProjects, projectCategories, teamLeads, @@ -110,6 +112,8 @@ const ProjectClientDetails: React.FC = ({ ); // get customer (client) contact combo + const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) + const [isMainProjectInfoLoading, setIsMainProjectInfoLoading] = useState(false) useEffect(() => { if (selectedCustomerId !== undefined) { fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => { @@ -117,8 +121,10 @@ const ProjectClientDetails: React.FC = ({ setCustomerSubsidiaryIds(subsidiaryIds); // if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0]) - // else - setValue("clientSubsidiaryId", undefined) + // else + if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } + else if (!isEditMode && isMainProjectInfoLoading) { setIsMainProjectInfoLoading(false) } + else setValue("clientSubsidiaryId", null) // if (contacts.length > 0) setValue("clientContactId", contacts[0].id) // else setValue("clientContactId", undefined) }); @@ -130,11 +136,11 @@ const ProjectClientDetails: React.FC = ({ if (Boolean(clientSubsidiaryId)) { // get subsidiary contact combo const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!! - setSubsidiaryContacts(contacts) + setSubsidiaryContacts(() => contacts) setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && Boolean(defaultValues?.clientSubsidiaryId) ? contacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? contacts[0].id : contacts[0].id) setValue("isSubsidiaryContact", true) } else if (customerContacts?.length > 0) { - setSubsidiaryContacts([]) + setSubsidiaryContacts(() => []) setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && !Boolean(defaultValues?.clientSubsidiaryId) ? customerContacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? customerContacts[0].id : customerContacts[0].id) setValue("isSubsidiaryContact", false) } @@ -153,10 +159,11 @@ const ProjectClientDetails: React.FC = ({ // Automatically update the project & client details whene select a main project const mainProjectId = watch("mainProjectId") useEffect(() => { - if (mainProjectId !== undefined && mainProjects !== undefined) { + if (mainProjectId !== undefined && mainProjects !== undefined && !isEditMode) { const mainProject = mainProjects.find(project => project.projectId === mainProjectId); if (mainProject !== undefined) { + setIsMainProjectInfoLoading(() => true) setValue("projectName", mainProject.projectName) setValue("projectCategoryId", mainProject.projectCategoryId) setValue("projectLeadId", mainProject.projectLeadId) @@ -174,7 +181,7 @@ const ProjectClientDetails: React.FC = ({ setValue("clientContactId", mainProject.clientContactId) } } - }, [getValues, mainProjectId, setValue]) + }, [getValues, mainProjectId, setValue, isEditMode]) // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( // (acc, building) => ({ ...acc, [building.id]: building.name }), @@ -202,6 +209,7 @@ const ProjectClientDetails: React.FC = ({ name="mainProjectId" label={t("Main Project")} noOptionsText={t("No Main Project")} + disabled={isEditMode} /> @@ -438,11 +446,11 @@ const ProjectClientDetails: React.FC = ({ )} - + {/* - + */} ); diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index 81d3c97..e61f995 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslation } from "react-i18next"; -import React, { useEffect, useMemo } from "react"; +import React, { SyntheticEvent, useEffect, useMemo } from "react"; import RestartAlt from "@mui/icons-material/RestartAlt"; import SearchResults, { Column } from "../SearchResults"; import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; @@ -160,8 +160,8 @@ const StaffAllocation: React.FC = ({ }, [columnFilters]); const [filters, setFilters] = React.useState(defaultFilterValues); const makeFilterSelect = React.useCallback( - (filter: keyof StaffResult) => (event: SelectChangeEvent) => { - setFilters((f) => ({ ...f, [filter]: event.target.value })); + (filter: keyof StaffResult) => (event: SyntheticEvent, value: NonNullable) => { + setFilters((f) => ({ ...f, [filter]: value })); }, [], ); @@ -239,20 +239,25 @@ const StaffAllocation: React.FC = ({ return ( - {label} - + renderInput={(params) => } + /> ); @@ -289,11 +294,11 @@ const StaffAllocation: React.FC = ({ )} - + {/* - + */} {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index fe02f37..759b05f 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -135,7 +135,7 @@ const TaskSetup: React.FC = ({ taskTemplate.id === selectedTaskTemplateId)} options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]} @@ -207,11 +207,11 @@ const TaskSetup: React.FC = ({ allItemsLabel={t("Task Pool")} selectedItemsLabel={t("Project Task List")} /> - + {/* - + */} ); diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx index 04b880c..6f267de 100644 --- a/src/components/CreateTeam/StaffAllocation.tsx +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -48,7 +48,6 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { } = useFormContext(); const initialStaffs = staff.map((s) => ({ ...s })); - // console.log(initialStaffs) const [filteredStaff, setFilteredStaff] = useState(initialStaffs); const [selectedStaff, setSelectedStaff] = useState( initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) @@ -158,15 +157,13 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, []); React.useEffect(() => { + const q = query.toLowerCase(); setFilteredStaff( - initialStaffs.filter((i) => { - const q = query.toLowerCase(); - return ( + initialStaffs.filter((i) => ( i.staffId.toLowerCase().includes(q) || i.name.toLowerCase().includes(q) || i.currentPosition.toLowerCase().includes(q) - ); - }) + )) ); }, [staff, query]); diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index c346874..83f395d 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/src/components/CustomDatagrid/CustomDatagrid.tsx @@ -15,6 +15,7 @@ interface CustomDatagridProps { dataGridHeight?: number | string; [key: string]: any; checkboxSelection?: boolean; + onRowClick?: any; onRowSelectionModelChange?: ( newSelectionModel: GridRowSelectionModel, ) => void; @@ -34,6 +35,7 @@ const CustomDatagrid: React.FC = ({ checkboxSelection, // Destructure the new prop onRowSelectionModelChange, // Destructure the new prop selectionModel, + onRowClick, columnGroupingModel, pageSize, ...props @@ -195,6 +197,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} experimentalFeatures={{ columnGrouping: true }} @@ -226,6 +229,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} experimentalFeatures={{ columnGrouping: true }} @@ -257,6 +261,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} experimentalFeatures={{ columnGrouping: true }} @@ -289,6 +294,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} style={{ marginRight: 0 }} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} diff --git a/src/components/DateHoursTable/DateHoursList.tsx b/src/components/DateHoursTable/DateHoursList.tsx index 685aef9..d54e930 100644 --- a/src/components/DateHoursTable/DateHoursList.tsx +++ b/src/components/DateHoursTable/DateHoursList.tsx @@ -17,6 +17,7 @@ import dayjs from "dayjs"; import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { + DAILY_NORMAL_MAX_HOURS, LEAVE_DAILY_MAX_HOURS, TIMESHEET_DAILY_MAX_HOURS, } from "@/app/api/timesheets/utils"; @@ -32,6 +33,7 @@ interface Props { EntryComponentProps & { date: string } >; entryComponentProps: EntryComponentProps; + errorComponent?: React.ReactNode; } function DateHoursList({ @@ -41,6 +43,7 @@ function DateHoursList({ EntryComponent, entryComponentProps, companyHolidays, + errorComponent, }: Props) { const { t, @@ -83,15 +86,22 @@ function DateHoursList({ leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; const timesheet = timesheetEntries[day]; - const timesheetHours = + const timesheetNormalHours = timesheet?.reduce( - (acc, entry) => - acc + (entry.inputHours || 0) + (entry.otHours || 0), + (acc, entry) => acc + (entry.inputHours || 0), 0, ) || 0; + const timesheetOtHours = + timesheet?.reduce( + (acc, entry) => acc + (entry.otHours || 0), + 0, + ) || 0; + const timesheetHours = timesheetNormalHours + timesheetOtHours; const dailyTotal = leaveHours + timesheetHours; + const normalHoursExceeded = + timesheetNormalHours > DAILY_NORMAL_MAX_HOURS; const leaveExceeded = leaveHours > LEAVE_DAILY_MAX_HOURS; const dailyTotalExceeded = dailyTotal > TIMESHEET_DAILY_MAX_HOURS; @@ -122,7 +132,9 @@ function DateHoursList({ sx={{ display: "flex", justifyContent: "space-between", + flexWrap: "wrap", alignItems: "baseline", + color: normalHoursExceeded ? "error.main" : undefined, }} > @@ -131,6 +143,21 @@ function DateHoursList({ {manhourFormatter.format(timesheetHours)} + {normalHoursExceeded && ( + + {t( + "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours.", + { + DAILY_NORMAL_MAX_HOURS, + }, + )} + + )} ({ variant="caption" > {t( - "The daily total hours cannot be more than {{hours}}", + "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}", { - hours: TIMESHEET_DAILY_MAX_HOURS, + TIMESHEET_DAILY_MAX_HOURS, }, )} @@ -198,6 +225,7 @@ function DateHoursList({ })} )} + {errorComponent} {isDateSelected ? ( diff --git a/src/components/EditUser/EditUserWrapper.tsx b/src/components/EditUser/EditUserWrapper.tsx index 9273d4a..f7d77eb 100644 --- a/src/components/EditUser/EditUserWrapper.tsx +++ b/src/components/EditUser/EditUserWrapper.tsx @@ -6,20 +6,23 @@ import { useSearchParams } from "next/navigation"; import { fetchTeam, fetchTeamDetail } from "@/app/api/team"; import { fetchStaff } from "@/app/api/staff"; import { fetchPwRules, fetchUser, fetchUserDetail } from "@/app/api/user"; +import { searchParamsProps } from "@/app/utils/fetchUtil"; +import { fetchUserDetails } from "@/app/api/user/actions"; +import { fetchAuth } from "@/app/api/group/actions"; interface SubComponents { Loading: typeof EditUserLoading; } -interface Props { - // id: number -} -const EditUserWrapper: React.FC & SubComponents = async ({ - // id +const EditUserWrapper: React.FC & SubComponents = async ({ + searchParams }) => { + const id = parseInt(searchParams.id as string) const pwRule = await fetchPwRules() + const user = await fetchUserDetails(id); + const auths = await fetchAuth("user", id); - return + return }; EditUserWrapper.Loading = EditUserLoading; diff --git a/src/components/EditUser/UserDetail.tsx b/src/components/EditUser/UserDetail.tsx index fc2f419..1d251c4 100644 --- a/src/components/EditUser/UserDetail.tsx +++ b/src/components/EditUser/UserDetail.tsx @@ -9,13 +9,13 @@ import { Stack, TextField, Typography, + makeStyles, } from "@mui/material"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; - - const UserDetail: React.FC = () => { + const { t } = useTranslation(); const { register, @@ -45,6 +45,30 @@ const UserDetail: React.FC = () => { label={t("password")} fullWidth {...register("password")} + // helperText={ + // Boolean(errors.password) && + // (errors.password?.message + // ? t(errors.password.message) + // : + // (<> + // - 8-20 characters + //
    + // - Uppercase letters + //
    + // - Lowercase letters + //
    + // - Numbers + //
    + // - Symbols + // ) + // ) + // } + helperText={ + Boolean(errors.password) && + (errors.password?.message + ? t(errors.password.message) + : t("Please input correct password")) + } error={Boolean(errors.password)} />
    @@ -55,3 +79,16 @@ const UserDetail: React.FC = () => { }; export default UserDetail; + + +{/* <> + - 8-20 characters +
    + - Uppercase letters +
    + - Lowercase letters +
    + - Numbers +
    + - Symbols + */} \ No newline at end of file diff --git a/src/components/EditUserGroup/AuthorityAllocation.tsx b/src/components/EditUserGroup/AuthorityAllocation.tsx index da502da..856ca3b 100644 --- a/src/components/EditUserGroup/AuthorityAllocation.tsx +++ b/src/components/EditUserGroup/AuthorityAllocation.tsx @@ -41,7 +41,6 @@ const AuthorityAllocation: React.FC = ({ auth }) => { reset, resetField, } = useFormContext(); - console.log(auth) const initialAuths = auth.map((a) => ({ ...a })).sort((a, b) => a.id - b.id); const [filteredAuths, setFilteredAuths] = useState(initialAuths); const [selectedAuths, setSelectedAuths] = useState( @@ -86,8 +85,8 @@ const AuthorityAllocation: React.FC = ({ auth }) => { onClick: addAuth, buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, // { label: t("Current Position"), name: "currentPosition" }, ], [addAuth, t] @@ -101,8 +100,8 @@ const AuthorityAllocation: React.FC = ({ auth }) => { onClick: removeAuth, buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, ], [removeAuth, selectedAuths, t] ); @@ -117,16 +116,13 @@ const AuthorityAllocation: React.FC = ({ auth }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + const q = query.toLowerCase(); + setFilteredAuths( + initialAuths.filter((a) => ( + a.authority.toLowerCase().includes(q) || + a.name.toLowerCase().includes(q) + )) + ); }, [auth, query]); const resetAuth = React.useCallback(() => { @@ -164,7 +160,7 @@ const AuthorityAllocation: React.FC = ({ auth }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ")+ t("Authority") + " / " + t("Description")} InputProps={{ endAdornment: query && ( diff --git a/src/components/EditUserGroup/GroupInfo.tsx b/src/components/EditUserGroup/GroupInfo.tsx index d9141bc..3817cec 100644 --- a/src/components/EditUserGroup/GroupInfo.tsx +++ b/src/components/EditUserGroup/GroupInfo.tsx @@ -51,13 +51,13 @@ const GroupInfo: React.FC = () => { Boolean(errors.name) && (errors.name?.message ? t(errors.name.message) - : t("Please input correct name")) + : t("Please input correct ") + t("name")) } />
    { Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) - : t("Please input correct description")) + : t("Please input correct ") + t("Description")) } /> diff --git a/src/components/EditUserGroup/UserAllocation.tsx b/src/components/EditUserGroup/UserAllocation.tsx index 14ed975..5173311 100644 --- a/src/components/EditUserGroup/UserAllocation.tsx +++ b/src/components/EditUserGroup/UserAllocation.tsx @@ -92,8 +92,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: addUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [addUser, t] ); @@ -106,8 +106,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: removeUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [removeUser, selectedUsers, t] ); @@ -123,16 +123,13 @@ const UserAllocation: React.FC = ({ users }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + const q = query.toLowerCase(); + setFilteredUsers( + initialUsers.filter((item) => ( + item.username.toLowerCase().includes(q) || + item.name.toLowerCase().includes(q) + )) + ); }, [users, query]); const resetUser = React.useCallback(() => { @@ -170,7 +167,7 @@ const UserAllocation: React.FC = ({ users }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ") + t("Username") + " / " + t("Staff Name")} InputProps={{ endAdornment: query && ( diff --git a/src/components/ErrorAlert/ErrorAlert.tsx b/src/components/ErrorAlert/ErrorAlert.tsx new file mode 100644 index 0000000..192cd1b --- /dev/null +++ b/src/components/ErrorAlert/ErrorAlert.tsx @@ -0,0 +1,28 @@ +import { Alert, AlertTitle, Box } from "@mui/material"; +import compact from "lodash/compact"; +import { useTranslation } from "react-i18next"; + +interface Props { + errors: (string | undefined)[]; +} + +const ErrorAlert: React.FC = ({ errors }) => { + const { t } = useTranslation("common"); + + if (compact(errors).length === 0) return null; + + return ( + + {t("There are some errors")} + + {errors.map((error, index) => ( + + {error} + + ))} + + + ); +}; + +export default ErrorAlert; diff --git a/src/components/ErrorAlert/index.ts b/src/components/ErrorAlert/index.ts new file mode 100644 index 0000000..a92a0d9 --- /dev/null +++ b/src/components/ErrorAlert/index.ts @@ -0,0 +1 @@ +export { default } from "./ErrorAlert"; diff --git a/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx b/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx index 072e297..a858406 100644 --- a/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx +++ b/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx @@ -3,61 +3,66 @@ import React, { useMemo } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import { ProjectResult } from "@/app/api/projects"; -import { fetchMonthlyWorkHoursReport, fetchProjectCashFlowReport } from "@/app/api/reports/actions"; +import { + fetchMonthlyWorkHoursReport, + fetchProjectCashFlowReport, +} from "@/app/api/reports/actions"; import { downloadFile } from "@/app/utils/commonUtil"; import { BASE_API_URL } from "@/config/api"; import { MonthlyWorkHoursReportFilter } from "@/app/api/reports"; import { records } from "@/app/api/staff/actions"; import { StaffResult } from "@/app/api/staff"; +import dayjs from "dayjs"; interface Props { - staffs: StaffResult[] + staffs: StaffResult[]; } type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const GenerateMonthlyWorkHoursReport: React.FC = ({ staffs }) => { - const { t } = useTranslation(); - const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`) - console.log(staffs) + const { t } = useTranslation("report"); + const staffCombo = staffs.map((staff) => `${staff.name} - ${staff.staffId}`); - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Staff"), - paramName: "staff", - type: "select", - options: staffCombo, - needAll: false - }, - { - label: t("date"), - paramName: "date", - type: "monthYear", - }, - ], - [t], - ); + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Staff"), + paramName: "staff", + type: "select", + options: staffCombo, + needAll: false, + }, + { + label: t("Date"), + paramName: "date", + type: "monthYear", + }, + ], + [t] + ); -return ( + return ( <> - { - const index = staffCombo.findIndex(staff => staff === query.staff) - if (query.staff.length > 0 && query.staff.toLocaleLowerCase() !== "all" && query.date.length > 0) { - const index = staffCombo.findIndex(staff => staff === query.staff) - const response = await fetchMonthlyWorkHoursReport({ id: staffs[index].id, yearMonth: query.date }) - if (response) { - downloadFile(new Uint8Array(response.blobValue), response.filename!!) - } - } - } - } - /> - - ) -} + const index = staffCombo.findIndex((staff) => staff === query.staff); + const response = await fetchMonthlyWorkHoursReport({ + id: staffs[index].id, + yearMonth: query.date, + }); + if (response) { + downloadFile( + new Uint8Array(response.blobValue), + response.filename!! + ); + } + }} + /> + + ); +}; -export default GenerateMonthlyWorkHoursReport \ No newline at end of file +export default GenerateMonthlyWorkHoursReport; diff --git a/src/components/GenerateProjectPandLReport/GenerateProjectPandLReport.tsx b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReport.tsx new file mode 100644 index 0000000..2fb5d63 --- /dev/null +++ b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReport.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { ProjectPandLReportFilter } from "@/app/api/reports"; +import { fetchProjectPandLReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { dateTypeCombo } from "@/app/utils/comboUtil"; +import { FormHelperText } from "@mui/material"; +import { errorDialog, errorDialogWithContent } from "../Swal/CustomAlerts"; + +interface Props { + projects: ProjectResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const GenerateProjectPandLReport: React.FC = ({ projects }) => { + const { t } = useTranslation("report"); + const projectCombo = projects.map(project => `${project.code} - ${project.name}`) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Project *"), paramName: "project", type: "select", options: projectCombo, needAll: false}, + { label: t("Start Month *"), label2: t("End Month *"), paramName: "startMonth", type: "dateRange", needMonth: true }, + + ], + [t], + ); + + return ( + <> + { + + if (query.project.length > 0 && query.project.toLocaleLowerCase() !== "all") { + const projectIndex = projectCombo.findIndex(project => project === query.project) + console.log(projects[projectIndex].id, query.startMonth, query.startMonthTo) + if(projects[projectIndex].id != null && query.startMonth != "" && query.startMonthTo != undefined){ + const response = await fetchProjectPandLReport({ projectId: projects[projectIndex].id, startMonth: query.startMonth, endMonth: query.startMonthTo }) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } + }else{ + errorDialogWithContent(t("Download Fail"), + t(`Please check the required field`), t) + .then(() => { + window.location.reload() + }) + } + } + }} + formType={"download"} + /> + + ); +}; + +export default GenerateProjectPandLReport; \ No newline at end of file diff --git a/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportLoading.tsx b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportLoading.tsx new file mode 100644 index 0000000..04826a4 --- /dev/null +++ b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportLoading.tsx @@ -0,0 +1,38 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const GenerateProjectPandLReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default GenerateProjectPandLReportLoading; \ No newline at end of file diff --git a/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportWrapper.tsx b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportWrapper.tsx new file mode 100644 index 0000000..00ca669 --- /dev/null +++ b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import GenerateProjectPandLReportLoading from "./GenerateProjectPandLReportLoading"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateProjectPandLReport from "./GenerateProjectPandLReport"; + +interface SubComponents { + Loading: typeof GenerateProjectPandLReportLoading; +} + +const GenerateProjectPandLReportWrapper: React.FC & SubComponents = async () => { + const projects = await fetchProjects(); + + return ; +}; + +GenerateProjectPandLReportWrapper.Loading = GenerateProjectPandLReportLoading; + +export default GenerateProjectPandLReportWrapper; \ No newline at end of file diff --git a/src/components/GenerateProjectPandLReport/index.ts b/src/components/GenerateProjectPandLReport/index.ts new file mode 100644 index 0000000..b56feba --- /dev/null +++ b/src/components/GenerateProjectPandLReport/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateProjectPandLReportWrapper"; \ No newline at end of file diff --git a/src/components/LeaveTable/LeaveEditModal.tsx b/src/components/LeaveTable/LeaveEditModal.tsx index 2434e7a..3bbe857 100644 --- a/src/components/LeaveTable/LeaveEditModal.tsx +++ b/src/components/LeaveTable/LeaveEditModal.tsx @@ -101,9 +101,15 @@ const LeaveEditModal: React.FC = ({ fullWidth {...register("inputHours", { setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), - validate: (value) => 0 < value && value <= LEAVE_DAILY_MAX_HOURS, + validate: (value) => + (0 < value && value <= LEAVE_DAILY_MAX_HOURS) || + t( + "Input hours should be between 0 and {{LEAVE_DAILY_MAX_HOURS}}", + { LEAVE_DAILY_MAX_HOURS }, + ), })} error={Boolean(formState.errors.inputHours)} + helperText={formState.errors.inputHours?.message} /> = ({ abilities }) => { label: "Completion Report", path: "/analytics/ProjectCompletionReport", }, - { - icon: , - label: "Completion Report with Outstanding Un-billed Hours Report", - path: "/analytics/ProjectCompletionReportWO", - }, + // { + // icon: , + // label: "Completion Report with Outstanding Un-billed Hours Report", + // path: "/analytics/ProjectCompletionReportWO", + // }, { icon: , label: "Project Claims Report", @@ -175,7 +175,7 @@ const NavigationContent: React.FC = ({ abilities }) => { { icon: , label: "Project P&L Report", - path: "/analytics/ProjectPLReport", + path: "/analytics/ProjectPandLReport", }, { icon: , diff --git a/src/components/ProgressByClient/ProgressByClient.tsx b/src/components/ProgressByClient/ProgressByClient.tsx index 3d4ecd2..8961472 100644 --- a/src/components/ProgressByClient/ProgressByClient.tsx +++ b/src/components/ProgressByClient/ProgressByClient.tsx @@ -303,130 +303,6 @@ const ProgressByClient: React.FC = () => { flex:0.1 }, ]; - const optionstest: ApexOptions = { - chart: { - height: 350, - type: "line", - }, - stroke: { - width: [0, 0, 2, 2], - }, - plotOptions: { - bar: { - horizontal: false, - distributed: false, - }, - }, - dataLabels: { - enabled: false, - }, - xaxis: { - categories: [ - "Q1", - "Q2", - "Q3", - "Q4", - "Q5", - "Q6", - "Q7", - "Q8", - "Q9", - "Q10", - "Q11", - "Q12", - ], - }, - yaxis: [ - { - title: { - text: "Monthly Income and Expenditure(HKD)", - }, - min: 0, - max: 350000, - tickAmount: 5, - labels: { - formatter: function (val) { - return val.toLocaleString() - } - } - }, - { - show: false, - seriesName: "Monthly_Expenditure", - title: { - text: "Monthly Expenditure (HKD)", - }, - min: 0, - max: 350000, - tickAmount: 5, - }, - { - seriesName: "Cumulative_Income", - opposite: true, - title: { - text: "Cumulative Income and Expenditure(HKD)", - }, - min: 0, - max: 850000, - tickAmount: 5, - labels: { - formatter: function (val) { - return val.toLocaleString() - } - } - }, - { - show: false, - seriesName: "Cumulative_Expenditure", - opposite: true, - title: { - text: "Cumulative Expenditure (HKD)", - }, - min: 0, - max: 850000, - tickAmount: 5, - }, - ], - grid: { - borderColor: "#f1f1f1", - }, - annotations: {}, - series: [ - { - name: "Monthly_Income", - type: "column", - color: "#ffde91", - data: [0, 110000, 0, 0, 185000, 0, 0, 189000, 0, 0, 300000, 0], - }, - { - name: "Monthly_Expenditure", - type: "column", - color: "#82b59a", - data: [ - 0, 160000, 120000, 120000, 55000, 55000, 55000, 55000, 55000, 70000, - 55000, 55000, - ], - }, - { - name: "Cumulative_Income", - type: "line", - color: "#EE6D7A", - data: [ - 0, 100000, 100000, 100000, 300000, 300000, 300000, 500000, 500000, - 500000, 800000, 800000, - ], - }, - { - name: "Cumulative_Expenditure", - type: "line", - color: "#7cd3f2", - data: [ - 0, 198000, 240000, 400000, 410000, 430000, 510000, 580000, 600000, - 710000, 730000, 790000, - ], - }, - ], - }; const options2: ApexOptions = { chart: { diff --git a/src/components/ProgressByTeam/ProgressByTeam.tsx b/src/components/ProgressByTeam/ProgressByTeam.tsx index fcb351a..7a573c9 100644 --- a/src/components/ProgressByTeam/ProgressByTeam.tsx +++ b/src/components/ProgressByTeam/ProgressByTeam.tsx @@ -18,9 +18,13 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByTeamSearch from "@/components/ProgressByTeamSearch"; import { Suspense } from "react"; +import { useSearchParams } from 'next/navigation'; +import { fetchAllTeamProjects} from "@/app/api/teamprojects/actions"; // const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByTeam: React.FC = () => { + const searchParams = useSearchParams(); + const teamLeadId = searchParams.get('teamLeadId'); const [activeTab, setActiveTab] = useState("financialSummary"); const [SearchCriteria, setSearchCriteria] = React.useState({}); const { t } = useTranslation("dashboard"); @@ -44,6 +48,64 @@ const ProgressByTeam: React.FC = () => { const [receiptFromDate, setReceiptFromDate] = useState(null); const [receiptToDate, setReceiptToDate] = useState(null); const [selectedRows, setSelectedRows] = useState([]); + const [chartProjectName, setChartProjectName]:any[] = useState([]); + const [chartManhourConsumptionPercentage, setChartManhourConsumptionPercentage]:any[] = useState([]); + const color = ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b", + "#f58a9b", "#8ef4d1", "#92caf9", "#a798f9", "#fad287", + "#f595a6", "#88f1cc", "#9dcff5", "#a39bf5", "#f8de83", + "#f5a0b1", "#82eec7", "#a8d4f1", "#9f9ef1", "#f6ea7f", + "#f5abb4", "#7cebca", "#b3d9ed", "#9ba1ed", "#f4f67b", + "#f5b6b7", "#76e8cd", "#bed6e9", "#97a4e9", "#f2fa77", + "#f5c1ba", "#70e5d0", "#c9d3e5", "#93a7e5", "#f0fe73", + "#f5ccbd", "#6ae2d3", "#d4d0e1", "#8faae1", "#eefe6f", + "#f5d7c0", "#64dfd6", "#dfc5dd", "#8badd5", "#ecfe6b", + "#f5e2c3", "#5edcd9", "#eabada", "#87b0c9", "#eafc67", + "#f5edc6", "#58d9dc", "#f5afd6", "#83b3bd", "#e8fc63", + "#f5f8c9", "#52d6df", "#ffacd2", "#7fb6b1", "#e6fc5f", + "#f5ffcc", "#4cd3e2", "#ffa9ce", "#7bb9a5", "#e4fc5b", + "#f2ffcf", "#46d0e5", "#ffa6ca", "#77bc99", "#e2fc57", + "#efffd2", "#40cde8", "#ffa3c6", "#73bf8d", "#e0fc53", + "#ecffd5", "#3acaeb", "#ffa0c2", "#6fc281", "#defb4f", + "#e9ffd8", "#34c7ee", "#ff9dbe", "#6bc575", "#dcfb4b", + "#e6ffdb", "#2ec4f1", "#ff9aba", "#67c869", "#dafb47", + "#e3ffde", "#28c1f4", "#ff97b6", "#63cb5d", "#d8fb43", + "#e0ffe1", "#22bef7", "#ff94b2", "#5fce51", "#d6fb3f", + "#ddfee4", "#1cbbfa", "#ff91ae", "#5bd145", "#d4fb3b", + "#dafee7", "#16b8fd", "#ff8eaa", "#57d439", "#d2fb37", + "#d7feea", "#10b5ff", "#ff8ba6", "#53d72d", "#d0fb33", + "#d4feed", "#0ab2ff", "#ff88a2", "#4fda21", "#cefb2f", + "#d1fef0", "#04afff", "#ff859e", "#4bdd15", "#ccfb2b"]; + const [teamProjectResult, setTeamProjectResult]:any[] = useState([]); + + const fetchData = async () => { + if (teamLeadId) { + try { + const clickResult = await fetchAllTeamProjects( + Number(teamLeadId)) + console.log(clickResult) + setTeamProjectResult(clickResult); + } catch (error) { + console.error('Error fetching team projects:', error); + } + } + } + + useEffect(() => { + const projectName = [] + const manhourConsumptionPercentage = [] + for (let i = 0; i < teamProjectResult.length; i++){ + teamProjectResult[i].color = color[i] + projectName.push(teamProjectResult[i].projectName) + manhourConsumptionPercentage.push(teamProjectResult[i].manhourConsumptionPercentage) + } + setChartProjectName(projectName) + setChartManhourConsumptionPercentage(manhourConsumptionPercentage) + }, [teamProjectResult]); + + useEffect(() => { + fetchData() + }, [teamLeadId]); + const rows = [ { id: 1, @@ -373,7 +435,35 @@ const ProgressByTeam: React.FC = () => { type: "bar", height: 350, }, - colors: ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b"], + series: [{ + name: "Project Resource Consumption Percentage", + data: chartManhourConsumptionPercentage, + },], + colors: ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b", + "#f58a9b", "#8ef4d1", "#92caf9", "#a798f9", "#fad287", + "#f595a6", "#88f1cc", "#9dcff5", "#a39bf5", "#f8de83", + "#f5a0b1", "#82eec7", "#a8d4f1", "#9f9ef1", "#f6ea7f", + "#f5abb4", "#7cebca", "#b3d9ed", "#9ba1ed", "#f4f67b", + "#f5b6b7", "#76e8cd", "#bed6e9", "#97a4e9", "#f2fa77", + "#f5c1ba", "#70e5d0", "#c9d3e5", "#93a7e5", "#f0fe73", + "#f5ccbd", "#6ae2d3", "#d4d0e1", "#8faae1", "#eefe6f", + "#f5d7c0", "#64dfd6", "#dfc5dd", "#8badd5", "#ecfe6b", + "#f5e2c3", "#5edcd9", "#eabada", "#87b0c9", "#eafc67", + "#f5edc6", "#58d9dc", "#f5afd6", "#83b3bd", "#e8fc63", + "#f5f8c9", "#52d6df", "#ffacd2", "#7fb6b1", "#e6fc5f", + "#f5ffcc", "#4cd3e2", "#ffa9ce", "#7bb9a5", "#e4fc5b", + "#f2ffcf", "#46d0e5", "#ffa6ca", "#77bc99", "#e2fc57", + "#efffd2", "#40cde8", "#ffa3c6", "#73bf8d", "#e0fc53", + "#ecffd5", "#3acaeb", "#ffa0c2", "#6fc281", "#defb4f", + "#e9ffd8", "#34c7ee", "#ff9dbe", "#6bc575", "#dcfb4b", + "#e6ffdb", "#2ec4f1", "#ff9aba", "#67c869", "#dafb47", + "#e3ffde", "#28c1f4", "#ff97b6", "#63cb5d", "#d8fb43", + "#e0ffe1", "#22bef7", "#ff94b2", "#5fce51", "#d6fb3f", + "#ddfee4", "#1cbbfa", "#ff91ae", "#5bd145", "#d4fb3b", + "#dafee7", "#16b8fd", "#ff8eaa", "#57d439", "#d2fb37", + "#d7feea", "#10b5ff", "#ff8ba6", "#53d72d", "#d0fb33", + "#d4feed", "#0ab2ff", "#ff88a2", "#4fda21", "#cefb2f", + "#d1fef0", "#04afff", "#ff859e", "#4bdd15", "#ccfb2b"], plotOptions: { bar: { horizontal: true, @@ -384,13 +474,7 @@ const ProgressByTeam: React.FC = () => { enabled: false, }, xaxis: { - categories: [ - "Consultancy Project 123", - "Consultancy Project 456", - "Construction Project A", - "Construction Project B", - "Construction Project C", - ], + categories: chartProjectName, }, yaxis: { title: { @@ -414,7 +498,7 @@ const ProgressByTeam: React.FC = () => { }; const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { - const selectedRowsData = rows2.filter((row) => + const selectedRowsData = teamProjectResult.filter((row:any) => newSelectionModel.includes(row.id), ); console.log(selectedRowsData); @@ -427,7 +511,7 @@ const ProgressByTeam: React.FC = () => { if (i === selectedRowsData.length && i > 0) { projectArray.push("Remained"); } else if (selectedRowsData.length > 0) { - projectArray.push(selectedRowsData[i].project); + projectArray.push(selectedRowsData[i].projectName); totalBudgetManhour += Number(selectedRowsData[i].budgetedManhour); totalSpent += Number(selectedRowsData[i].spentManhour); pieChartColorArray.push(selectedRowsData[i].color); @@ -485,7 +569,7 @@ const ProgressByTeam: React.FC = () => {
    @@ -507,7 +591,7 @@ const ProgressByTeam: React.FC = () => { style={{ display: "inline-block", width: "99%", marginLeft: 10 }} > = ({ projects }) => { const { t } = useTranslation("projects"); - + const router = useRouter(); // If project searching is done on the server-side, then no need for this. const [filteredProjects, setFilteredProjects] = useState(projects); @@ -27,13 +29,31 @@ const ProgressByClientSearch: React.FC = ({ projects }) => { [t], ); + const onTaskClick = useCallback(async (teamProjectResult: TeamProjectResult) => { + try { + console.log(teamProjectResult) + router.push( + `/dashboard/ProjectStatusByTeam?teamLeadId=${teamProjectResult.teamLeadId}` + ); + } catch (error) { + console.error('Error fetching team projects:', error); + } + }, []); + + const columns = useMemo[]>( () => [ + { + name: "id", + label: t("Details"), + onClick: onTaskClick, + buttonIcon: , + }, { name: "teamCode", label: t("Team Code") }, { name: "teamName", label: t("Team Name") }, - { name: "NoOfProjects", label: t("No. of Projects") }, + { name: "projectNo", label: t("No. of Projects") }, ], - [t], + [onTaskClick, t], ); return ( @@ -41,7 +61,13 @@ const ProgressByClientSearch: React.FC = ({ projects }) => { { - console.log(query); + setFilteredProjects( + projects.filter( + (cp) => + cp.teamCode.toLowerCase().includes(query.teamCode.toLowerCase()) && + cp.teamName.toLowerCase().includes(query.teamName.toLowerCase()) + ), + ); }} /> diff --git a/src/components/ProjectCashFlow/ProjectCashFlow.tsx b/src/components/ProjectCashFlow/ProjectCashFlow.tsx index ef81aff..4a78c62 100644 --- a/src/components/ProjectCashFlow/ProjectCashFlow.tsx +++ b/src/components/ProjectCashFlow/ProjectCashFlow.tsx @@ -19,17 +19,34 @@ import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; import ProgressCashFlowSearch from "@/components/ProgressCashFlowSearch"; +import { fetchProjectsCashFlow} from "@/app/api/cashflow"; import { Input, Label } from "reactstrap"; +import { CashFlow } from "@/app/api/cashflow"; + +interface Props { + projects: CashFlow[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; const ProjectCashFlow: React.FC = () => { + const { t } = useTranslation("projects"); const todayDate = new Date(); const [selectionModel, setSelectionModel]: any[] = React.useState([]); + const [projectData, setProjectData]: any[] = React.useState([]); const [cashFlowYear, setCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); const [anticipateCashFlowYear, setAnticipateCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); + const fetchData = async () => { + const cashFlowProject = await fetchProjectsCashFlow(); + setProjectData(cashFlowProject) + } + useEffect(() => { + fetchData() + }, []); const columns = [ { id: "projectCode", @@ -50,8 +67,8 @@ const ProjectCashFlow: React.FC = () => { flex: 1, }, { - id: "teamLeader", - field: "teamLeader", + id: "teamLead", + field: "teamLead", headerName: "Team Leader", flex: 1, }, @@ -530,8 +547,6 @@ const ProjectCashFlow: React.FC = () => { remarks: "Monthly Manpower Expenditure", }, ]; - - const [projectData, setProjectData]: any[] = React.useState(rows); const [ledgerData, setLedgerData]: any[] = React.useState(ledgerRows); const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { const selectedRowsData = projectData.filter((row: any) => @@ -540,11 +555,31 @@ const ProjectCashFlow: React.FC = () => { console.log(selectedRowsData); }; + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "Project Code", paramName: "projectCode", type: "text" }, + { label: "Project Name", paramName: "projectName", type: "text" }, + { + label: "Start Date From", + label2: "Start Date To", + paramName: "startDateFrom", + type: "dateRange", + }, + ], + [t], + ); + return ( <> - }> + {/* }> - + */} + { + console.log(query); + }} + /> >; +type SearchParamNames = keyof SearchQuery; + +const ProjectCompletionReport: React.FC = ( + { + // team, + // customer + } +) => { + const { t } = useTranslation("report"); + const [error, setError] = useState(""); + const outstandingList = ["Regular", "Outstanding Accounts Receivable"] + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("startDate"), + label2: t("endDate"), + paramName: "startDate", + type: "dateRange", + }, + { + label: t("Type"), + paramName: "outstanding", + type: "select", + needAll: false, + options: outstandingList + }, + ], + [t] + ); + + return ( + <> + { + console.log(query); + let postData: ProjectCompletionReportRequest = { + startDate: "", + endDate: dayjs().format(INPUT_DATE_FORMAT).toString(), + outstanding: false + }; + if (query.endDate && query.endDate.length > 0) { + postData.endDate = query.endDate; + } + + // check if start date exist + if (query.startDate.length === 0) { + setError(t("Start Date cant be empty")); + } else { + postData.startDate = query.startDate; + if (query.outstanding && query.outstanding === "Outstanding Accounts Receivable") { + // outstanding report + postData.outstanding = true + } + console.log(postData) + const response = + await fetchProjectCompletionReport( + postData + ); + // normal report + + if (response) { + downloadFile( + new Uint8Array(response.blobValue), + response.filename!! + ); + } + } + }} + /> + + ); +}; + +export default ProjectCompletionReport; diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx b/src/components/ProjectCompletionReport/ProjectCompletionReportLoading.tsx similarity index 89% rename from src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx rename to src/components/ProjectCompletionReport/ProjectCompletionReportLoading.tsx index 9ae7417..1c5355c 100644 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx +++ b/src/components/ProjectCompletionReport/ProjectCompletionReportLoading.tsx @@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack"; import React from "react"; // Can make this nicer -export const ResourceOvercomsumptionReportGenLoading: React.FC = () => { +export const ProjectCompletionReportLoading: React.FC = () => { return ( <> @@ -38,4 +38,4 @@ export const ResourceOvercomsumptionReportGenLoading: React.FC = () => { ); }; -export default ResourceOvercomsumptionReportGenLoading; +export default ProjectCompletionReportLoading; diff --git a/src/components/ProjectCompletionReport/ProjectCompletionReportWrapper.tsx b/src/components/ProjectCompletionReport/ProjectCompletionReportWrapper.tsx new file mode 100644 index 0000000..0dd10cb --- /dev/null +++ b/src/components/ProjectCompletionReport/ProjectCompletionReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { fetchAllCustomers } from "@/app/api/customer"; +import { fetchTeam } from "@/app/api/team"; +import ProjectCompletionReportLoading from "./ProjectCompletionReportLoading"; +import ProjectCompletionReport from "./ProjectCompletionReport"; + +interface SubComponents { + Loading: typeof ProjectCompletionReportLoading; +} + +const ProjectCompletionReportWrapper: React.FC & SubComponents = async () => { + + return +}; + +ProjectCompletionReportWrapper.Loading = ProjectCompletionReportLoading; + +export default ProjectCompletionReportWrapper; \ No newline at end of file diff --git a/src/components/ProjectCompletionReport/index.ts b/src/components/ProjectCompletionReport/index.ts new file mode 100644 index 0000000..8555938 --- /dev/null +++ b/src/components/ProjectCompletionReport/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectCompletionReportWrapper"; \ No newline at end of file diff --git a/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx b/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx index 25ff90e..369d10c 100644 --- a/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx +++ b/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx @@ -20,14 +20,14 @@ import { Suspense } from "react"; interface Props { Title: string; - TotalActiveProjectNumber: string; - TotalFees: string; - TotalBudget: string; - TotalCumulative: string; - TotalInvoicedAmount: string; - TotalReceivedAmount: string; + TotalActiveProjectNumber: number; + TotalFees: number; + TotalBudget: number; + TotalCumulative: number; + TotalInvoicedAmount: number; + TotalReceivedAmount: number; CashFlowStatus: string; - CostPerformanceIndex: string; + CostPerformanceIndex: number; ClickedIndex: number; Index: number; } @@ -53,8 +53,6 @@ const ProjectFinancialCard: React.FC = ({ : "border-green-200 border-solid"; const selectedBackgroundColor = ClickedIndex === Index ? "rgb(235 235 235)" : "rgb(255 255 255)"; - console.log(ClickedIndex); - console.log(Index); return ( = ({ Total Active Project
    - {TotalActiveProjectNumber} + {TotalActiveProjectNumber.toLocaleString()}

    Total Fees
    - {TotalFees} + {TotalFees.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    Total Budget
    - {TotalBudget} + {TotalBudget.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    Total Cumulative Expenditure
    - {TotalCumulative} + {TotalCumulative.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    Total Invoiced Amount
    - {TotalInvoicedAmount} + {TotalInvoicedAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    Total Received Amount
    - {TotalReceivedAmount} + {TotalReceivedAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

    diff --git a/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx b/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx index e0a2764..c803b59 100644 --- a/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx +++ b/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx @@ -18,86 +18,33 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; +import { fetchFinancialSummaryCard } from "@/app/api/financialsummary"; +import { searchFinancialSummaryByClient,searchFinancialSummaryByProject } from "@/app/api/financialsummary/actions"; import ProjectFinancialCard from "./ProjectFinancialCard"; +import VisibilityIcon from '@mui/icons-material/Visibility'; const ProjectFinancialSummary: React.FC = () => { const [SearchCriteria, setSearchCriteria] = React.useState({}); const { t } = useTranslation("dashboard"); const [selectionModel, setSelectionModel]: any[] = React.useState([]); - const projectFinancialData = [ - { - id: 1, - title: "All Teams", - activeProject: "147", - fees: "22,800,000.00", - budget: "18,240,000.00", - cumulativeExpenditure: "17,950,000.00", - invoicedAmount: "18,240,000.00", - receivedAmount: "10,900,000.00", - cashFlowStatus: "Negative", - CPI: "0.69", - }, - { - id: 2, - title: "XXX Team", - activeProject: "25", - fees: "1,500,000.00", - budget: "1,200,000.00", - cumulativeExpenditure: "1,250,000.00", - invoicedAmount: "900,000.00", - receivedAmount: "650,000.00", - cashFlowStatus: "Negative", - CPI: "0.72", - }, - { - id: 3, - title: "YYY Team", - activeProject: "35", - fees: "5,000,000.00", - budget: "4,000,000.00", - cumulativeExpenditure: "3,200,000.00", - invoicedAmount: "3,500,000.00", - receivedAmount: "3,500,000.00", - cashFlowStatus: "Positive", - CPI: "1.09", - }, - { - id: 4, - title: "ZZZ Team", - activeProject: "50", - fees: "3,500,000.00", - budget: "2,800,000.00", - cumulativeExpenditure: "5,600,000.00", - invoicedAmount: "2,500,000.00", - receivedAmount: "2,200,000.00", - cashFlowStatus: "Negative", - CPI: "0.45", - }, - { - id: 5, - title: "AAA Team", - activeProject: "15", - fees: "4,800,000.00", - budget: "3,840,000.00", - cumulativeExpenditure: "2,500,000.00", - invoicedAmount: "1,500,000.00", - receivedAmount: "750,000.00", - cashFlowStatus: "Negative", - CPI: "0.60", - }, - { - id: 6, - title: "BBB Team", - activeProject: "22", - fees: "8,000,000.00", - budget: "6,400,000.00", - cumulativeExpenditure: "5,400,000.00", - invoicedAmount: "4,000,000.00", - receivedAmount: "3,800,000.00", - cashFlowStatus: "Negative", - CPI: "0.74", - }, - ]; + const [projectFinancialData, setProjectFinancialData]: any[] = React.useState([]); + const [clientFinancialRows, setClientFinancialRows]: any[] = React.useState([]); + const [projectFinancialRows, setProjectFinancialRows]: any[] = React.useState([]); + const fetchData = async () => { + const financialSummaryCard = await fetchFinancialSummaryCard(); + setProjectFinancialData(financialSummaryCard) + } + const fetchTableData = async (teamId?:any) => { + const financialSummaryByClient = await searchFinancialSummaryByClient(teamId); + + console.log(financialSummaryByClient) + // console.log(financialSummaryByProject) + setClientFinancialRows(financialSummaryByClient) + } + useEffect(() => { + fetchData() + fetchTableData(undefined) + }, []); const rows0 = [{id: 1,projectCode:"M1201",projectName:"Consultancy Project C", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "01/05/2024", client:"Client A", subsidiary:"N/A"}, {id: 2,projectCode:"M1321",projectName:"Consultancy Project CCC", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "20/01/2024", client:"Client E", subsidiary:"Subsidiary B"}, @@ -115,45 +62,39 @@ const ProjectFinancialSummary: React.FC = () => { {id: 5,projectCode:"M1354",projectName:"Consultancy Project BBB", team:"YYY", teamLeader:"YYY", startDate:"01/02/2023", targetEndDate: "31/01/2024", client:"Client D", subsidiary:"Subsidiary C"} ] - const projectFinancialRows = [{id: 1,projectCode:"M1354",projectName:"Consultanct Project BBB",clientName:"Client D",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} - ] + // const projectFinancialRows = [{id: 1,projectCode:"M1354",projectName:"Consultanct Project BBB",clientName:"Client D",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} + // ] - const clientFinancialRows =[{id: 1,clientCode:"Cust-02",clientName:"Client B",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, - {id: 2,clientCode:"Cust-03",clientName:"Client C",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, - {id: 3,clientCode:"Cust-04",clientName:"Client D",totalProjectInvolved:"4",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} - ] + // const clientFinancialRows =[{id: 1,clientCode:"Cust-02",clientName:"Client B",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, + // {id: 2,clientCode:"Cust-03",clientName:"Client C",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, + // {id: 3,clientCode:"Cust-04",clientName:"Client D",totalProjectInvolved:"4",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} + // ] const [isCardClickedIndex, setIsCardClickedIndex] = React.useState(0); const [selectedTeamData, setSelectedTeamData]: any[] = React.useState(rows0); - const handleCardClick = (r: any) => { - setIsCardClickedIndex(r); - if (r === 0) { - setSelectedTeamData(rows0); - } else if (r === 1) { - setSelectedTeamData(rows1); - } else if (r === 2) { - setSelectedTeamData(rows2); - } + const handleCardClick = (r: any, index:any) => { + fetchTableData(r.teamId) + setIsCardClickedIndex(index) }; const columns = [ { - id: 'clientCode', - field: 'clientCode', + id: 'customerCode', + field: 'customerCode', headerName: "Client Code", flex: 0.7, }, { - id: 'clientName', - field: 'clientName', + id: 'customerName', + field: 'customerName', headerName: "Client Name", flex: 1, }, { - id: 'totalProjectInvolved', - field: 'totalProjectInvolved', + id: 'projectNo', + field: 'projectNo', headerName: "Total Project Involved", flex: 1, }, @@ -163,6 +104,7 @@ const ProjectFinancialSummary: React.FC = () => { headerName: "Cash Flow Status", flex: 1, renderCell: (params:any) => { + console.log(params.row) if (params.row.cashFlowStatus === "Positive") { return ( {params.row.cashFlowStatus} @@ -192,13 +134,13 @@ const ProjectFinancialSummary: React.FC = () => { }, }, { - id: 'totalFees', - field: 'totalFees', + id: 'totalFee', + field: 'totalFee', headerName: "Total Fees (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalFees} + ${params.row.totalFee} ) }, }, @@ -214,46 +156,46 @@ const ProjectFinancialSummary: React.FC = () => { }, }, { - id: 'totalCumulativeExpenditure', - field: 'totalCumulativeExpenditure', + id: 'cumulativeExpenditure', + field: 'cumulativeExpenditure', headerName: "Total Cumulative Expenditure (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalCumulativeExpenditure} + ${params.row.cumulativeExpenditure} ) }, }, { - id: 'totalInvoicedAmount', - field: 'totalInvoicedAmount', + id: 'totalInvoiced', + field: 'totalInvoiced', headerName: "Total Invoiced Amount (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalInvoicedAmount} + ${params.row.totalInvoiced} ) }, }, { - id: 'totalUnInvoicedAmount', - field: 'totalUnInvoicedAmount', + id: 'totalUnInvoiced', + field: 'totalUnInvoiced', headerName: "Total Un-invoiced Amount (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalUnInvoicedAmount} + ${params.row.totalUninvoiced} ) }, }, { - id: 'totalReceivedAmount', - field: 'totalReceivedAmount', + id: 'totalReceived', + field: 'totalReceived', headerName: "Total Received Amount (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalReceivedAmount} + ${params.row.totalReceived} ) }, }, @@ -322,8 +264,8 @@ const columns2 = [ flex: 1, }, { - id: 'clientName', - field: 'clientName', + id: 'customerName', + field: 'customerName', headerName: "Client Name", flex: 1, }, @@ -365,7 +307,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalFees} + ${params.row.totalFee} ) }, }, @@ -387,7 +329,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalCumulativeExpenditure} + ${params.row.cumulativeExpenditure} ) }, }, @@ -398,7 +340,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalInvoicedAmount} + ${params.row.totalInvoiced} ) }, }, @@ -409,7 +351,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalUnInvoicedAmount} + ${params.row.totalUninvoiced} ) }, }, @@ -420,7 +362,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalReceivedAmount} + ${params.row.totalReceived} ) }, }, @@ -432,15 +374,25 @@ const columns2 = [ ); console.log(selectedRowsData); }; + + const fetchProjectTableData = async (teamId?:any,customerId?:any) => { + const financialSummaryByProject = await searchFinancialSummaryByProject(teamId,customerId); + setProjectFinancialRows(financialSummaryByProject) + } + + const handleRowClick = (params:any) => { + console.log(params.row.teamId); + fetchProjectTableData(params.row.teamId,params.row.cid) + }; return (
    - {projectFinancialData.map((record, index) => ( -
    handleCardClick(index)}> - + {projectFinancialData.map((record:any, index:any) => ( +
    handleCardClick(record,index)}> +
    ))}
    @@ -449,7 +401,7 @@ const columns2 = [
    {/* */} - +
    diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index 79ee51b..6456902 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -20,7 +20,6 @@ type SearchParamNames = keyof SearchQuery; const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const router = useRouter(); const { t } = useTranslation("projects"); - console.log(projects) const [filteredProjects, setFilteredProjects] = useState(projects); @@ -62,7 +61,9 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const onProjectClick = useCallback( (project: ProjectResult) => { - router.push(`/projects/edit?id=${project.id}`); + if (Boolean(project.mainProject)) { + router.push(`/projects/editSub?id=${project.id}`); + } else router.push(`/projects/edit?id=${project.id}`); }, [router], ); diff --git a/src/components/Report/ReportSearchBox3/SearchBox3.tsx b/src/components/Report/ReportSearchBox3/SearchBox3.tsx deleted file mode 100644 index aafa7d0..0000000 --- a/src/components/Report/ReportSearchBox3/SearchBox3.tsx +++ /dev/null @@ -1,313 +0,0 @@ -//src\components\ReportSearchBox3\SearchBox3.tsx -"use client"; - -import Grid from "@mui/material/Grid"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import Typography from "@mui/material/Typography"; -import React, { useCallback, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import TextField from "@mui/material/TextField"; -import FormControl from "@mui/material/FormControl"; -import InputLabel from "@mui/material/InputLabel"; -import Select, { SelectChangeEvent } from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import CardActions from "@mui/material/CardActions"; -import Button from "@mui/material/Button"; -import RestartAlt from "@mui/icons-material/RestartAlt"; -import Search from "@mui/icons-material/Search"; -import dayjs from "dayjs"; -import "dayjs/locale/zh-hk"; -import { DatePicker } from "@mui/x-date-pickers/DatePicker"; -import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; -import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import { Box } from "@mui/material"; -import * as XLSX from 'xlsx-js-style'; -//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; - -interface BaseCriterion { - label: string; - label2?: string; - paramName: T; - paramName2?: T; -} - -interface TextCriterion extends BaseCriterion { - type: "text"; -} - -interface SelectCriterion extends BaseCriterion { - type: "select"; - options: string[]; -} - -interface DateRangeCriterion extends BaseCriterion { - type: "dateRange"; -} - -export type Criterion = - | TextCriterion - | SelectCriterion - | DateRangeCriterion; - -interface Props { - criteria: Criterion[]; - onSearch: (inputs: Record) => void; - onReset?: () => void; -} - -function SearchBox({ - criteria, - onSearch, - onReset, -}: Props) { - const { t } = useTranslation("common"); - const defaultInputs = useMemo( - () => - criteria.reduce>( - (acc, c) => { - return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; - }, - {} as Record, - ), - [criteria], - ); - const [inputs, setInputs] = useState(defaultInputs); - - const makeInputChangeHandler = useCallback( - (paramName: T): React.ChangeEventHandler => { - return (e) => { - setInputs((i) => ({ ...i, [paramName]: e.target.value })); - }; - }, - [], - ); - - const makeSelectChangeHandler = useCallback((paramName: T) => { - return (e: SelectChangeEvent) => { - setInputs((i) => ({ ...i, [paramName]: e.target.value })); - }; - }, []); - - const makeDateChangeHandler = useCallback((paramName: T) => { - return (e: any) => { - setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); - }; - }, []); - - const makeDateToChangeHandler = useCallback((paramName: T) => { - return (e: any) => { - setInputs((i) => ({ - ...i, - [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), - })); - }; - }, []); - - const handleReset = () => { - setInputs(defaultInputs); - onReset?.(); - }; - - const handleSearch = () => { - onSearch(inputs); - - }; - - const handleDownload = async () => { - //setIsLoading(true); - - try { - const response = await fetch('/temp/AR03_Resource Overconsumption.xlsx', { - headers: { - 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - }, - }); - if (!response.ok) throw new Error('Network response was not ok.'); - - const data = await response.blob(); - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target && e.target.result) { - const ab = e.target.result as ArrayBuffer; - const workbook = XLSX.read(ab, { type: 'array' }); - const firstSheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheetName]; - - // Add the current date to cell C2 - const cellAddress = 'C2'; - const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD - const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD - XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - - // Style for cell A1: Font size 16 and bold - if (worksheet['A1']) { - worksheet['A1'].s = { - font: { - bold: true, - sz: 16, // Font size 16 - //name: 'Times New Roman' // Specify font - } - }; - } - - // Apply styles from A2 to A4 (bold) - ['A2', 'A3', 'A4'].forEach(cell => { - if (worksheet[cell]) { - worksheet[cell].s = { font: { bold: true } }; - } - }); - - // Formatting from A6 to L6 - // Apply styles from A6 to L6 (bold, bottom border, center alignment) - for (let col = 0; col < 12; col++) { // Columns A to K - const cellRef = XLSX.utils.encode_col(col) + '6'; - if (worksheet[cellRef]) { - worksheet[cellRef].s = { - font: { bold: true }, - alignment: { horizontal: 'center' }, - border: { - bottom: { style: 'thin', color: { auto: 1 } } - } - }; - } - } - - const firstTableData = [ - ['Column1', 'Column2', 'Column3'], // Row 1 - ['Data1', 'Data2', 'Data3'], // Row 2 - // ... more rows as needed - ]; - // Find the last row of the first table - let lastRowOfFirstTable = 6; // Starting row for data in the first table - while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { - lastRowOfFirstTable++; - } - - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - - // Format filename with date - const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD - const filename = `AR03_Resource_Overconsumption_${today}.xlsx`; // Append formatted date to the filename - - // Convert workbook back to XLSX file - XLSX.writeFile(workbook, filename); - } else { - throw new Error('Failed to load file'); - } - }; - reader.readAsArrayBuffer(data); - } catch (error) { - console.error('Error downloading the file: ', error); - } - - //setIsLoading(false); - }; - return ( - - - {t("Search Criteria")} - - {criteria.map((c) => { - return ( - - {c.type === "text" && ( - - )} - {c.type === "select" && ( - - {c.label} - - - )} - {c.type === "dateRange" && ( - - - - - - - {"-"} - - - - - - - )} - - ); - })} - - - - - - - - ); -} - -export default SearchBox; diff --git a/src/components/Report/ReportSearchBox3/index.ts b/src/components/Report/ReportSearchBox3/index.ts deleted file mode 100644 index d481fbd..0000000 --- a/src/components/Report/ReportSearchBox3/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -//src\components\SearchBox\index.ts -export { default } from "./SearchBox3"; -export type { Criterion } from "./SearchBox3"; diff --git a/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx deleted file mode 100644 index 345b2f2..0000000 --- a/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx +++ /dev/null @@ -1,17 +0,0 @@ -//src\components\DelayReport\DelayReport.tsx -"use client"; -import * as React from "react"; -import "../../../app/global.css"; -import { Suspense } from "react"; -import ResourceOverconsumptionReportGen from "@/components/Report/ResourceOverconsumptionReportGen"; - -const ResourceOverconsumptionReport: React.FC = () => { - - return ( - }> - - - ); -}; - -export default ResourceOverconsumptionReport; \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReport/index.ts b/src/components/Report/ResourceOverconsumptionReport/index.ts deleted file mode 100644 index ce20324..0000000 --- a/src/components/Report/ResourceOverconsumptionReport/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//src\components\LateStartReport\index.ts -export { default } from "./ResourceOverconsumptionReport"; diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx deleted file mode 100644 index a6ec216..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx +++ /dev/null @@ -1,45 +0,0 @@ -//src\components\LateStartReportGen\LateStartReportGen.tsx -"use client"; -import React, { useMemo, useState } from "react"; -import SearchBox, { Criterion } from "../ReportSearchBox3"; -import { useTranslation } from "react-i18next"; -import { ResourceOverconsumption } from "@/app/api/report3"; - -interface Props { - projects: ResourceOverconsumption[]; -} -type SearchQuery = Partial>; -type SearchParamNames = keyof SearchQuery; - -const ProgressByClientSearch: React.FC = ({ projects }) => { - const { t } = useTranslation("projects"); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { label: "Team", paramName: "team", type: "select", options: ["AAA", "BBB", "CCC"] }, - { label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] }, - { label: "Status", paramName: "status", type: "select", options: ["Overconsumption", "Potential Overconsumption"] }, - // { - // label: "Status", - // label2: "Remained Date To", - // paramName: "targetEndDate", - // type: "dateRange", - // }, - ], - [t], - ); - - return ( - <> - { - console.log(query); - }} - /> - {/* */} - - ); -}; - -export default ProgressByClientSearch; diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx deleted file mode 100644 index a93f64b..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx +++ /dev/null @@ -1,19 +0,0 @@ -//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx -import { fetchProjectsResourceOverconsumption } from "@/app/api/report3"; -import React from "react"; -import ResourceOvercomsumptionReportGen from "./ResourceOverconsumptionReportGen"; -import ResourceOvercomsumptionReportGenLoading from "./ResourceOverconsumptionReportGenLoading"; - -interface SubComponents { - Loading: typeof ResourceOvercomsumptionReportGenLoading; -} - -const ResourceOvercomsumptionReportGenWrapper: React.FC & SubComponents = async () => { - const clentprojects = await fetchProjectsResourceOverconsumption(); - - return ; -}; - -ResourceOvercomsumptionReportGenWrapper.Loading = ResourceOvercomsumptionReportGenLoading; - -export default ResourceOvercomsumptionReportGenWrapper; \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReportGen/index.ts b/src/components/Report/ResourceOverconsumptionReportGen/index.ts deleted file mode 100644 index 82fb633..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//src\components\DelayReportGen\index.ts -export { default } from "./ResourceOverconsumptionReportGenWrapper"; diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx new file mode 100644 index 0000000..fb49dba --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx @@ -0,0 +1,96 @@ +"use client"; +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { fetchMonthlyWorkHoursReport, fetchProjectCashFlowReport, fetchProjectResourceOverconsumptionReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { BASE_API_URL } from "@/config/api"; +import { ProjectResourceOverconsumptionReportFilter, ProjectResourceOverconsumptionReportRequest } from "@/app/api/reports"; +import { StaffResult } from "@/app/api/staff"; +import { TeamResult } from "@/app/api/team"; +import { Customer } from "@/app/api/customer"; + +interface Props { + team: TeamResult[] + customer: Customer[] +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ResourceOverconsumptionReport: React.FC = ({ team, customer }) => { + const { t } = useTranslation("report"); + const teamCombo = team.map(t => `${t.name} - ${t.code}`) + const custCombo = customer.map(c => `${c.name} - ${c.code}`) + const statusCombo = ["Overconsumption", "Potential Overconsumption"] + // const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`) + // console.log(staffs) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team"), + paramName: "team", + type: "select", + options: teamCombo, + needAll: true + }, + { + label: t("Client"), + paramName: "customer", + type: "select", + options: custCombo, + needAll: true + }, + { + label: t("Status"), + paramName: "status", + type: "select", + options: statusCombo, + needAll: true + }, + { + label: t("lowerLimit"), + paramName: "lowerLimit", + type: "number", + }, + ], + [t], + ); + +return ( + <> + { + let index = 0 + let postData: ProjectResourceOverconsumptionReportRequest = { + status: "All", + lowerLimit: 0.9 + } + if (query.team.length > 0 && query.team.toLocaleLowerCase() !== "all") { + index = teamCombo.findIndex(team => team === query.team) + postData.teamId = team[index].id + } + if (query.customer.length > 0 && query.customer.toLocaleLowerCase() !== "all") { + index = custCombo.findIndex(customer => customer === query.customer) + postData.custId = customer[index].id + } + if (Boolean(query.lowerLimit)) { + postData.lowerLimit = query.lowerLimit/100 + } + postData.status = query.status + console.log(postData) + const response = await fetchProjectResourceOverconsumptionReport(postData) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } + } + } + /> + + ) +} + +export default ResourceOverconsumptionReport \ No newline at end of file diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx new file mode 100644 index 0000000..945d13c --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx @@ -0,0 +1,41 @@ +//src\components\LateStartReportGen\LateStartReportGenLoading.tsx +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const ResourceOvercomsumptionReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ResourceOvercomsumptionReportLoading; diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx new file mode 100644 index 0000000..1ab9d24 --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import ResourceOvercomsumptionReportLoading from "./ResourceOverconsumptionReportLoading"; +import ResourceOverconsumptionReport from "./ResourceOverconsumptionReport"; +import { fetchAllCustomers } from "@/app/api/customer"; +import { fetchTeam } from "@/app/api/team"; + +interface SubComponents { + Loading: typeof ResourceOvercomsumptionReportLoading; +} + +const ResourceOvercomsumptionReportWrapper: React.FC & SubComponents = async () => { + const customers = await fetchAllCustomers() + const teams = await fetchTeam () + + return ; +}; + +ResourceOvercomsumptionReportWrapper.Loading = ResourceOvercomsumptionReportLoading; + +export default ResourceOvercomsumptionReportWrapper; \ No newline at end of file diff --git a/src/components/ResourceOverconsumptionReport/index.ts b/src/components/ResourceOverconsumptionReport/index.ts new file mode 100644 index 0000000..b5f20e2 --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/index.ts @@ -0,0 +1 @@ +export { default } from "./ResourceOverconsumptionReportWrapper"; \ No newline at end of file diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index cd0de38..3f849cd 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -4,7 +4,7 @@ import Grid from "@mui/material/Grid"; import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import Typography from "@mui/material/Typography"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { FocusEvent, KeyboardEvent, PointerEvent, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import TextField from "@mui/material/TextField"; import FormControl from "@mui/material/FormControl"; @@ -15,18 +15,29 @@ import CardActions from "@mui/material/CardActions"; import Button from "@mui/material/Button"; import RestartAlt from "@mui/icons-material/RestartAlt"; import Search from "@mui/icons-material/Search"; -import FileDownload from '@mui/icons-material/FileDownload'; +import FileDownload from "@mui/icons-material/FileDownload"; import dayjs from "dayjs"; import "dayjs/locale/zh-hk"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; -import { Box } from "@mui/material"; +import { Box, FormHelperText } from "@mui/material"; import { DateCalendar } from "@mui/x-date-pickers"; import { fetchLateStartReport } from "@/app/api/reports/actions"; import { LateStartReportRequest } from "@/app/api/reports"; import { fetchTeamCombo } from "@/app/api/team/actions"; import { downloadFile } from "@/app/utils/commonUtil"; +import { + Unstable_NumberInput as BaseNumberInput, + NumberInputProps, + numberInputClasses, +} from "@mui/base/Unstable_NumberInput"; +import { + StyledButton, + StyledInputElement, + StyledInputRoot, +} from "@/theme/colorConst"; +import { InputAdornment, NumberInput } from "../utils/numberInput"; interface BaseCriterion { label: string; @@ -47,23 +58,29 @@ interface SelectCriterion extends BaseCriterion { interface DateRangeCriterion extends BaseCriterion { type: "dateRange"; + needMonth?: boolean; } interface MonthYearCriterion extends BaseCriterion { type: "monthYear"; } +interface NumberCriterion extends BaseCriterion { + type: "number"; +} + export type Criterion = | TextCriterion | SelectCriterion | DateRangeCriterion - | MonthYearCriterion; + | MonthYearCriterion + | NumberCriterion; interface Props { criteria: Criterion[]; onSearch: (inputs: Record) => void; onReset?: () => void; - formType?: String, + formType?: String; } function SearchBox({ @@ -79,10 +96,14 @@ function SearchBox({ (acc, c) => { return { ...acc, - [c.paramName]: c.type === "select" ? - !(c.needAll === false) ? "All" : - c.options.length > 0 ? c.options[0] : "" - : "" + [c.paramName]: + c.type === "select" + ? !(c.needAll === false) + ? "All" + : c.options.length > 0 + ? c.options[0] + : "" + : "", }; }, {} as Record @@ -90,7 +111,7 @@ function SearchBox({ [criteria] ); const [inputs, setInputs] = useState(defaultInputs); - + const makeInputChangeHandler = useCallback( (paramName: T): React.ChangeEventHandler => { return (e) => { @@ -99,6 +120,15 @@ function SearchBox({ }, [] ); + + const makeNumberChangeHandler = useCallback( + (paramName: T): (event: FocusEvent | PointerEvent | KeyboardEvent, value: number | null) => void => { + return (event, value) => { + setInputs((i) => ({ ...i, [paramName]: value })); + }; + }, + [] + ); const makeSelectChangeHandler = useCallback((paramName: T) => { return (e: SelectChangeEvent) => { @@ -106,25 +136,37 @@ function SearchBox({ }; }, []); - const makeDateChangeHandler = useCallback((paramName: T) => { + const makeDateChangeHandler = useCallback((paramName: T, needMonth?: boolean) => { return (e: any) => { - setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + if(needMonth){ + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") })); + }else{ + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + } + }; }, []); const makeMonthYearChangeHandler = useCallback((paramName: T) => { return (e: any) => { - console.log(dayjs(e).format("YYYY-MM")) + console.log(dayjs(e).format("YYYY-MM")); setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") })); }; }, []); - const makeDateToChangeHandler = useCallback((paramName: T) => { + const makeDateToChangeHandler = useCallback((paramName: T, needMonth?: boolean) => { return (e: any) => { + if(needMonth){ + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM"), + })); + }else{ setInputs((i) => ({ ...i, [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), })); + } }; }, []); @@ -172,6 +214,15 @@ function SearchBox({ )} + {c.type === "number" && ( + %} + /> + )} {c.type === "monthYear" && ( ({ adapterLocale="zh-hk" > - + + + )} @@ -202,12 +255,13 @@ function SearchBox({ ({ @@ -246,7 +301,9 @@ function SearchBox({