| @@ -12,6 +12,7 @@ | |||||
| "@emotion/cache": "^11.11.0", | "@emotion/cache": "^11.11.0", | ||||
| "@emotion/react": "^11.11.1", | "@emotion/react": "^11.11.1", | ||||
| "@emotion/styled": "^11.11.0", | "@emotion/styled": "^11.11.0", | ||||
| "@faker-js/faker": "^8.4.1", | |||||
| "@fontsource/inter": "^5.0.16", | "@fontsource/inter": "^5.0.16", | ||||
| "@fontsource/plus-jakarta-sans": "^5.0.18", | "@fontsource/plus-jakarta-sans": "^5.0.18", | ||||
| "@mui/icons-material": "^5.15.0", | "@mui/icons-material": "^5.15.0", | ||||
| @@ -38,7 +39,8 @@ | |||||
| "react-select": "^5.8.0", | "react-select": "^5.8.0", | ||||
| "reactstrap": "^9.2.2", | "reactstrap": "^9.2.2", | ||||
| "styled-components": "^6.1.8", | "styled-components": "^6.1.8", | ||||
| "sweetalert2": "^11.10.3" | |||||
| "sweetalert2": "^11.10.3", | |||||
| "xlsx-js-style": "^1.2.0" | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@types/lodash": "^4.14.202", | "@types/lodash": "^4.14.202", | ||||
| @@ -0,0 +1,24 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| import { Metadata } from "next"; | |||||
| import { I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Project Status by Client", | |||||
| }; | |||||
| const ProjectLateReport: React.FC = () => { | |||||
| return ( | |||||
| <I18nProvider namespaces={["analytics"]}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| Cost and Expense Report | |||||
| </Typography> | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||||
| <ProgressCashFlowSearch/> | |||||
| </Suspense> */} | |||||
| <LateStartReportComponent /> | |||||
| </I18nProvider> | |||||
| ); | |||||
| }; | |||||
| export default ProjectLateReport; | |||||
| @@ -0,0 +1,24 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| import { Metadata } from "next"; | |||||
| import { I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Project Status by Client", | |||||
| }; | |||||
| const ProjectLateReport: React.FC = () => { | |||||
| return ( | |||||
| <I18nProvider namespaces={["analytics"]}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| Delay Report | |||||
| </Typography> | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||||
| <ProgressCashFlowSearch/> | |||||
| </Suspense> */} | |||||
| <LateStartReportComponent /> | |||||
| </I18nProvider> | |||||
| ); | |||||
| }; | |||||
| export default ProjectLateReport; | |||||
| @@ -0,0 +1,24 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| import { Metadata } from "next"; | |||||
| import { I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Project Status by Client", | |||||
| }; | |||||
| const ProjectLateReport: React.FC = () => { | |||||
| return ( | |||||
| <I18nProvider namespaces={["analytics"]}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| Late Start Report | |||||
| </Typography> | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||||
| <ProgressCashFlowSearch/> | |||||
| </Suspense> */} | |||||
| <LateStartReportComponent /> | |||||
| </I18nProvider> | |||||
| ); | |||||
| }; | |||||
| export default ProjectLateReport; | |||||
| @@ -0,0 +1,24 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| import { Metadata } from "next"; | |||||
| import { I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Project Status by Client", | |||||
| }; | |||||
| const ProjectLateReport: React.FC = () => { | |||||
| return ( | |||||
| <I18nProvider namespaces={["analytics"]}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| Project Completion Report | |||||
| </Typography> | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||||
| <ProgressCashFlowSearch/> | |||||
| </Suspense> */} | |||||
| <LateStartReportComponent /> | |||||
| </I18nProvider> | |||||
| ); | |||||
| }; | |||||
| export default ProjectLateReport; | |||||
| @@ -0,0 +1,24 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| import { Metadata } from "next"; | |||||
| import { I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Project Status by Client", | |||||
| }; | |||||
| const ProjectLateReport: React.FC = () => { | |||||
| return ( | |||||
| <I18nProvider namespaces={["analytics"]}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| Project Completion Report with Outstanding Un-billed Hours | |||||
| </Typography> | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||||
| <ProgressCashFlowSearch/> | |||||
| </Suspense> */} | |||||
| <LateStartReportComponent /> | |||||
| </I18nProvider> | |||||
| ); | |||||
| }; | |||||
| export default ProjectLateReport; | |||||
| @@ -0,0 +1,24 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| import { Metadata } from "next"; | |||||
| import { I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Project Status by Client", | |||||
| }; | |||||
| const ProjectLateReport: React.FC = () => { | |||||
| return ( | |||||
| <I18nProvider namespaces={["analytics"]}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| Resource Overconsumption Report | |||||
| </Typography> | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||||
| <ProgressCashFlowSearch/> | |||||
| </Suspense> */} | |||||
| <LateStartReportComponent /> | |||||
| </I18nProvider> | |||||
| ); | |||||
| }; | |||||
| export default ProjectLateReport; | |||||
| @@ -1,3 +1,4 @@ | |||||
| //src\app\(main)\analytics\page.tsx | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| @@ -5,7 +6,8 @@ export const metadata: Metadata = { | |||||
| }; | }; | ||||
| const Analytics: React.FC = async () => { | const Analytics: React.FC = async () => { | ||||
| return "Analytics"; | |||||
| //return "Analytics"; | |||||
| return <div>Analytics</div>; | |||||
| }; | }; | ||||
| export default Analytics; | export default Analytics; | ||||
| @@ -17,7 +17,7 @@ const ProjectFinancialSummary: React.FC = () => { | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["dashboard"]}> | <I18nProvider namespaces={["dashboard"]}> | ||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| Project Financial Summary | |||||
| Financial Summary | |||||
| </Typography> | </Typography> | ||||
| <ProjectFinancialSummaryComponents /> | <ProjectFinancialSummaryComponents /> | ||||
| </I18nProvider> | </I18nProvider> | ||||
| @@ -6,7 +6,7 @@ import Typography from "@mui/material/Typography"; | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Create Customer", | |||||
| title: "Create Client", | |||||
| }; | }; | ||||
| const CreateCustomer: React.FC = async () => { | const CreateCustomer: React.FC = async () => { | ||||
| @@ -0,0 +1,44 @@ | |||||
| //src\app\api\report\index.ts | |||||
| import { cache } from "react"; | |||||
| export interface LateStart { | |||||
| id: number; | |||||
| projectCode: string; | |||||
| projectName: string; | |||||
| team: string; | |||||
| teamLeader: string; | |||||
| startDate: string; | |||||
| startDateFrom: string; | |||||
| startDateTo: string; | |||||
| targetEndDate: string; | |||||
| client: string; | |||||
| subsidiary: string; | |||||
| nextstage: string; | |||||
| nextstageenddate: string; | |||||
| } | |||||
| export const preloadProjects = () => { | |||||
| fetchProjectsCashFlow(); | |||||
| }; | |||||
| export const fetchProjectsCashFlow = cache(async () => { | |||||
| return mockProjects; | |||||
| }); | |||||
| const mockProjects: LateStart[] = [ | |||||
| { | |||||
| 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", | |||||
| nextstage:"s1", | |||||
| nextstageenddate:"30/2/2024", | |||||
| }, | |||||
| ]; | |||||
| @@ -19,7 +19,7 @@ export interface CreateStaffInputs { | |||||
| companyId: number; | companyId: number; | ||||
| gradeId: number; | gradeId: number; | ||||
| teamId: number; | teamId: number; | ||||
| salaryEffId: number; | |||||
| salaryId: number; | |||||
| email: string; | email: string; | ||||
| phone1: string; | phone1: string; | ||||
| phone2: string; | phone2: string; | ||||
| @@ -179,7 +179,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| label: t("Team"), | label: t("Team"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: teamCombo, | options: teamCombo, | ||||
| required: true, | |||||
| required: false, | |||||
| }, | }, | ||||
| { | { | ||||
| id: "departmentId", | id: "departmentId", | ||||
| @@ -193,14 +193,14 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| label: t("Grade"), | label: t("Grade"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: gradeCombo, | options: gradeCombo, | ||||
| required: true, | |||||
| required: false, | |||||
| }, | }, | ||||
| { | { | ||||
| id: "skillSetId", | id: "skillSetId", | ||||
| label: t("Skillset"), | label: t("Skillset"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: skillCombo, | options: skillCombo, | ||||
| required: true, | |||||
| required: false, | |||||
| }, | }, | ||||
| { | { | ||||
| id: "currentPositionId", | id: "currentPositionId", | ||||
| @@ -210,19 +210,19 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | { | ||||
| id: "salaryEffId", | |||||
| id: "salaryId", | |||||
| label: t("Salary Point"), | label: t("Salary Point"), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: salaryCombo, | options: salaryCombo, | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | |||||
| id: "hourlyRate", | |||||
| label: t("Hourly Rate"), | |||||
| type: "numeric-testing", | |||||
| value: "", | |||||
| required: true, | |||||
| }, | |||||
| // { | |||||
| // id: "hourlyRate", | |||||
| // label: t("Hourly Rate"), | |||||
| // type: "numeric-testing", | |||||
| // value: "", | |||||
| // required: false, | |||||
| // }, | |||||
| { | { | ||||
| id: "employType", | id: "employType", | ||||
| label: t("Employ Type"), | label: t("Employ Type"), | ||||
| @@ -245,7 +245,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| label: t("Phone1"), | label: t("Phone1"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | value: "", | ||||
| pattern: "^\\d{8}$", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| @@ -254,9 +254,9 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| label: t("Phone2"), | label: t("Phone2"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | value: "", | ||||
| pattern: "^\\d{8}$", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| required: true, | |||||
| required: false, | |||||
| }, | }, | ||||
| ], | ], | ||||
| [ | [ | ||||
| @@ -272,7 +272,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => { | |||||
| label: t("Emergency Contact Phone"), | label: t("Emergency Contact Phone"), | ||||
| type: "text", | type: "text", | ||||
| value: "", | value: "", | ||||
| pattern: "^\\d{8}$", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| @@ -274,7 +274,7 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||||
| fullWidth | fullWidth | ||||
| {...register(field.id, { | {...register(field.id, { | ||||
| pattern: | pattern: | ||||
| /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/, | |||||
| /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/, | |||||
| })} | })} | ||||
| defaultValue={!field.value ? `${field.value}` : ""} | defaultValue={!field.value ? `${field.value}` : ""} | ||||
| required={field.required ?? false} | required={field.required ?? false} | ||||
| @@ -231,7 +231,7 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| if (errorRows.length > 0) { | if (errorRows.length > 0) { | ||||
| setError("addContacts", { message: "Contact details include empty fields", type: "required" }) | setError("addContacts", { message: "Contact details include empty fields", type: "required" }) | ||||
| } else { | } else { | ||||
| const errorRows_EmailFormat = rows.filter(row => !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(String(row.email))) | |||||
| const errorRows_EmailFormat = rows.filter(row => !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email))) | |||||
| if (errorRows_EmailFormat.length > 0) { | if (errorRows_EmailFormat.length > 0) { | ||||
| setError("addContacts", { message: "Contact details include empty fields", type: "email_format" }) | setError("addContacts", { message: "Contact details include empty fields", type: "email_format" }) | ||||
| } else { | } else { | ||||
| @@ -75,7 +75,9 @@ const CustomerDetail: React.FC<Props> = ({ | |||||
| const customer = await fetchCustomer(parseInt(id)) | const customer = await fetchCustomer(parseInt(id)) | ||||
| if (customer !== null && Object.keys(customer).length > 0) { | |||||
| console.log(customer) | |||||
| if (customer !== null && Object.keys(customer).length > 0 && !Object.values(customer.customer).every(x => x === null)) { | |||||
| const tempCustomerInput = { | const tempCustomerInput = { | ||||
| id: customer.customer.id, | id: customer.customer.id, | ||||
| code: customer.customer.code ?? "", | code: customer.customer.code ?? "", | ||||
| @@ -165,7 +167,7 @@ const CustomerDetail: React.FC<Props> = ({ | |||||
| formProps.setError("code", { message: "Code is empty", type: "required" }) | formProps.setError("code", { message: "Code is empty", type: "required" }) | ||||
| } | } | ||||
| // if (data.email && data.email?.length > 0 && !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(data.email)) { | |||||
| // if (data.email && data.email?.length > 0 && !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(data.email)) { | |||||
| // haveError = true | // haveError = true | ||||
| // formProps.setError("email", { message: "Email format is not valid", type: "custom" }) | // formProps.setError("email", { message: "Email format is not valid", type: "custom" }) | ||||
| // } | // } | ||||
| @@ -180,7 +182,7 @@ const CustomerDetail: React.FC<Props> = ({ | |||||
| formProps.setError("addContacts", { message: "Contact info includes empty fields", type: "required" }) | formProps.setError("addContacts", { message: "Contact info includes empty fields", type: "required" }) | ||||
| } | } | ||||
| if (data.addContacts.length > 0 && data.addContacts.filter(row => !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(String(row.email))).length > 0) { | |||||
| if (data.addContacts.length > 0 && data.addContacts.filter(row => !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email))).length > 0) { | |||||
| haveError = true | haveError = true | ||||
| formProps.setError("addContacts", { message: "Contact info includes invalid email", type: "email_format" }) | formProps.setError("addContacts", { message: "Contact info includes invalid email", type: "email_format" }) | ||||
| } | } | ||||
| @@ -107,7 +107,7 @@ const CustomerInfo: React.FC<Props> = ({ | |||||
| label={t("Customer Email")} | label={t("Customer Email")} | ||||
| fullWidth | fullWidth | ||||
| {...register("email", { | {...register("email", { | ||||
| pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/, | |||||
| pattern: /^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/, | |||||
| })} | })} | ||||
| error={Boolean(errors.email)} | error={Boolean(errors.email)} | ||||
| helperText={Boolean(errors.email) && t("Please input correct customer email")} | helperText={Boolean(errors.email) && t("Please input correct customer email")} | ||||
| @@ -31,9 +31,9 @@ const DashboardPage: React.FC = () => { | |||||
| return ( | return ( | ||||
| <ThemeProvider theme={theme}> | <ThemeProvider theme={theme}> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
| <Tab label="Project Financial Summary" /> | |||||
| <Tab label="Financial Summary" /> | |||||
| <Tab label="Project Cash Flow" /> | <Tab label="Project Cash Flow" /> | ||||
| <Tab label="Project Progress by Client" /> | |||||
| <Tab label="Project Resource Consumption by Client" /> | |||||
| <Tab label="Project Resource Utilization" /> | <Tab label="Project Resource Utilization" /> | ||||
| <Tab label="Staff Utilization" /> | <Tab label="Staff Utilization" /> | ||||
| </Tabs> | </Tabs> | ||||
| @@ -16,7 +16,7 @@ const DashboardTabButton: React.FC = () => { | |||||
| const renderContent = () => { | const renderContent = () => { | ||||
| switch (activeTab) { | switch (activeTab) { | ||||
| case "financialSummary": | case "financialSummary": | ||||
| return <div>Project Financial Summary</div>; | |||||
| return <div>Financial Summary</div>; | |||||
| case "cashFlow": | case "cashFlow": | ||||
| return <div>Project Cash Flow</div>; | return <div>Project Cash Flow</div>; | ||||
| case "progressByClient": | case "progressByClient": | ||||
| @@ -26,7 +26,7 @@ const DashboardTabButton: React.FC = () => { | |||||
| case "staffUtilization": | case "staffUtilization": | ||||
| return <div>Staff Utilization</div>; | return <div>Staff Utilization</div>; | ||||
| default: | default: | ||||
| return <div>Project Financial Summary</div>; | |||||
| return <div>Financial Summary</div>; | |||||
| } | } | ||||
| }; | }; | ||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| @@ -40,16 +40,16 @@ const DashboardTabButton: React.FC = () => { | |||||
| // <Grid item sm> | // <Grid item sm> | ||||
| // <div style={{marginLeft:20}}> | // <div style={{marginLeft:20}}> | ||||
| // {activeTab !== 'financialSummary' ? | // {activeTab !== 'financialSummary' ? | ||||
| // <button onClick={() => setActiveTab('financialSummary')}className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Project Financial Summary</button> : | |||||
| // <button onClick={() => setActiveTab('financialSummary')}className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Project Financial Summary</button> | |||||
| // <button onClick={() => setActiveTab('financialSummary')}className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Financial Summary</button> : | |||||
| // <button onClick={() => setActiveTab('financialSummary')}className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:40,width:250,fontSize:18}}>Financial Summary</button> | |||||
| // } | // } | ||||
| // {activeTab !== 'cashFlow' ? | // {activeTab !== 'cashFlow' ? | ||||
| // <button onClick={() => setActiveTab('cashFlow')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button> : | // <button onClick={() => setActiveTab('cashFlow')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button> : | ||||
| // <button onClick={() => setActiveTab('cashFlow')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button> | // <button onClick={() => setActiveTab('cashFlow')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Cash Flow</button> | ||||
| // } | // } | ||||
| // {activeTab !== 'progressByClient' ? | // {activeTab !== 'progressByClient' ? | ||||
| // <button onClick={() => setActiveTab('progressByClient')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Progress by Client</button> : | |||||
| // <button onClick={() => setActiveTab('progressByClient')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Progress by Client</button> | |||||
| // <button onClick={() => setActiveTab('progressByClient')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Consumption by Client</button> : | |||||
| // <button onClick={() => setActiveTab('progressByClient')} className="rounded-lg bg-sky-100 border-cyan-500 border-solid text-cyan-500 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Consumption by Client</button> | |||||
| // } | // } | ||||
| // {activeTab !== 'resourceUtilization' ? | // {activeTab !== 'resourceUtilization' ? | ||||
| // <button onClick={() => setActiveTab('resourceUtilization')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Utilization</button> : | // <button onClick={() => setActiveTab('resourceUtilization')} className="hover:bg-sky-100 hover:cursor-pointer rounded-lg bg-transparent border-slate-400 border-solid text-slate-400 ml-0.5 mt-0.5" style={{height:39,width:250,fontSize:18}}>Project Resource Utilization</button> : | ||||
| @@ -65,9 +65,9 @@ const DashboardTabButton: React.FC = () => { | |||||
| // </div> | // </div> | ||||
| // </Grid> | // </Grid> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
| <Tab label="Project Financial Summary" /> | |||||
| <Tab label="Financial Summary" /> | |||||
| <Tab label="Project Cash Flow" /> | <Tab label="Project Cash Flow" /> | ||||
| <Tab label="Project Progress by Client" /> | |||||
| <Tab label="Project Resource Consumption by Client" /> | |||||
| <Tab label="Project Resource Utilization" /> | <Tab label="Project Resource Utilization" /> | ||||
| <Tab label="Staff Utilization" /> | <Tab label="Staff Utilization" /> | ||||
| </Tabs> | </Tabs> | ||||
| @@ -298,7 +298,7 @@ const ProgressByClient: React.FC = () => { | |||||
| const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [ | const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [ | ||||
| { | { | ||||
| name: "Current Stage Completion Percentage", | |||||
| name: "Project Resource Consumption Percentage", | |||||
| data: [80, 55, 40, 65, 70], | data: [80, 55, 40, 65, 70], | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -372,7 +372,7 @@ const ProgressByClient: React.FC = () => { | |||||
| }, | }, | ||||
| }, | }, | ||||
| title: { | title: { | ||||
| text: "Current Stage Completion Percentage", | |||||
| text: "Project Resource Consumption Percentage", | |||||
| align: "center", | align: "center", | ||||
| }, | }, | ||||
| grid: { | grid: { | ||||
| @@ -426,7 +426,7 @@ const ProgressByClient: React.FC = () => { | |||||
| <div style={{ display: "inline-block", width: "70%" }}> | <div style={{ display: "inline-block", width: "70%" }}> | ||||
| <Grid item xs={12} md={12} lg={12}> | <Grid item xs={12} md={12} lg={12}> | ||||
| <Card> | <Card> | ||||
| <CardHeader className="text-slate-500" title="Project Progress" /> | |||||
| <CardHeader className="text-slate-500" title="Project Resource Consumption" /> | |||||
| <div style={{ display: "inline-block", width: "99%" }}> | <div style={{ display: "inline-block", width: "99%" }}> | ||||
| <ReactApexChart | <ReactApexChart | ||||
| options={options} | options={options} | ||||
| @@ -77,7 +77,7 @@ const EditStaff: React.FC = async () => { | |||||
| "grade", | "grade", | ||||
| "skill", | "skill", | ||||
| "currentPosition", | "currentPosition", | ||||
| "salaryEffective", | |||||
| "salary", | |||||
| "hourlyRate", | "hourlyRate", | ||||
| "employType", | "employType", | ||||
| "email", | "email", | ||||
| @@ -140,6 +140,7 @@ const EditStaff: React.FC = async () => { | |||||
| label: t(`Staff ID`), | label: t(`Staff ID`), | ||||
| type: "text", | type: "text", | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | |||||
| }; | }; | ||||
| case "name": | case "name": | ||||
| return { | return { | ||||
| @@ -147,6 +148,7 @@ const EditStaff: React.FC = async () => { | |||||
| label: t(`Staff Name`), | label: t(`Staff Name`), | ||||
| type: "text", | type: "text", | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | |||||
| }; | }; | ||||
| case "company": | case "company": | ||||
| return { | return { | ||||
| @@ -155,6 +157,7 @@ const EditStaff: React.FC = async () => { | |||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: companyCombo, | options: companyCombo, | ||||
| value: data[key].id ?? "", | value: data[key].id ?? "", | ||||
| required: true, | |||||
| }; | }; | ||||
| case "team": | case "team": | ||||
| return { | return { | ||||
| @@ -171,6 +174,7 @@ const EditStaff: React.FC = async () => { | |||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: departmentCombo, | options: departmentCombo, | ||||
| value: data[key]?.id ?? "", | value: data[key]?.id ?? "", | ||||
| required: true, | |||||
| // later check | // later check | ||||
| }; | }; | ||||
| case "grade": | case "grade": | ||||
| @@ -179,7 +183,7 @@ const EditStaff: React.FC = async () => { | |||||
| label: t(`Grade`), | label: t(`Grade`), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: gradeCombo, | options: gradeCombo, | ||||
| value: data[key].id ?? "", | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| }; | }; | ||||
| case "skill": | case "skill": | ||||
| return { | return { | ||||
| @@ -187,7 +191,7 @@ const EditStaff: React.FC = async () => { | |||||
| label: t(`Skillset`), | label: t(`Skillset`), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: skillCombo, | options: skillCombo, | ||||
| value: data[key].id ?? "", | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| }; | }; | ||||
| case "currentPosition": | case "currentPosition": | ||||
| return { | return { | ||||
| @@ -196,24 +200,26 @@ const EditStaff: React.FC = async () => { | |||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: positionCombo, | options: positionCombo, | ||||
| value: data[key].id ?? "", | value: data[key].id ?? "", | ||||
| required: true, | |||||
| }; | }; | ||||
| case "salaryEffective": | |||||
| case "salary": | |||||
| return { | return { | ||||
| id: `salaryEffId`, | |||||
| id: `salaryId`, | |||||
| label: t(`Salary Point`), | label: t(`Salary Point`), | ||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: salaryCombo, | options: salaryCombo, | ||||
| value: data[key].salary.id ?? "", | |||||
| }; | |||||
| case "hourlyRate": | |||||
| return { | |||||
| id: `${key}`, | |||||
| label: t(`hourlyRate`), | |||||
| type: "text", | |||||
| value: "", | |||||
| // value: data[key], | |||||
| readOnly: true, | |||||
| value: data[key] !== null ? data[key].id ?? "" : "", | |||||
| required: true, | |||||
| }; | }; | ||||
| // case "hourlyRate": | |||||
| // return { | |||||
| // id: `${key}`, | |||||
| // label: t(`hourlyRate`), | |||||
| // type: "text", | |||||
| // value: "", | |||||
| // // value: data[key], | |||||
| // readOnly: true, | |||||
| // }; | |||||
| case "employType": | case "employType": | ||||
| return { | return { | ||||
| id: `${key}`, | id: `${key}`, | ||||
| @@ -221,6 +227,7 @@ const EditStaff: React.FC = async () => { | |||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: employTypeCombo, | options: employTypeCombo, | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | |||||
| }; | }; | ||||
| case "email": | case "email": | ||||
| return { | return { | ||||
| @@ -230,22 +237,24 @@ const EditStaff: React.FC = async () => { | |||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | ||||
| message: t("input matching format"), | message: t("input matching format"), | ||||
| required: true, | |||||
| }; | }; | ||||
| case "phone1": | case "phone1": | ||||
| return { | return { | ||||
| id: `${key}`, | id: `${key}`, | ||||
| label: t(`Phone1`), | label: t(`Phone1`), | ||||
| type: "text", | type: "text", | ||||
| pattern: "^\\d{8}$", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | |||||
| }; | }; | ||||
| case "phone2": | case "phone2": | ||||
| return { | return { | ||||
| id: `${key}`, | id: `${key}`, | ||||
| label: t(`Phone2`), | label: t(`Phone2`), | ||||
| type: "text", | type: "text", | ||||
| pattern: "^\\d{8}$", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| } as Field; | } as Field; | ||||
| @@ -263,15 +272,17 @@ const EditStaff: React.FC = async () => { | |||||
| label: t(`Emergency Contact Name`), | label: t(`Emergency Contact Name`), | ||||
| type: "text", | type: "text", | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | |||||
| } as Field; | } as Field; | ||||
| case "emergContactPhone": | case "emergContactPhone": | ||||
| return { | return { | ||||
| id: `${key}`, | id: `${key}`, | ||||
| label: t(`Emergency Contact Phonee`), | label: t(`Emergency Contact Phonee`), | ||||
| type: "text", | type: "text", | ||||
| pattern: "^\\d{8}$", | |||||
| // pattern: "^\\d{8}$", | |||||
| message: t("input correct phone no."), | message: t("input correct phone no."), | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | |||||
| } as Field; | } as Field; | ||||
| case "joinDate": | case "joinDate": | ||||
| return { | return { | ||||
| @@ -279,6 +290,7 @@ const EditStaff: React.FC = async () => { | |||||
| label: t(`Join Date`), | label: t(`Join Date`), | ||||
| type: "multiDate", | type: "multiDate", | ||||
| value: data[key] ?? "", | value: data[key] ?? "", | ||||
| required: true, | |||||
| } as Field; | } as Field; | ||||
| case "joinPosition": | case "joinPosition": | ||||
| return { | return { | ||||
| @@ -287,6 +299,7 @@ const EditStaff: React.FC = async () => { | |||||
| type: "combo-Obj", | type: "combo-Obj", | ||||
| options: positionCombo, | options: positionCombo, | ||||
| value: data[key].id ?? "", | value: data[key].id ?? "", | ||||
| required: true, | |||||
| } as Field; | } as Field; | ||||
| case "departDate": | case "departDate": | ||||
| return { | return { | ||||
| @@ -0,0 +1,17 @@ | |||||
| //src\components\LateStartReport\LateStartReport.tsx | |||||
| "use client"; | |||||
| import * as React from "react"; | |||||
| import "../../app/global.css"; | |||||
| import { Suspense } from "react"; | |||||
| import LateStartReportGen from "@/components/LateStartReportGen"; | |||||
| const LateStartReport: React.FC = () => { | |||||
| return ( | |||||
| <Suspense fallback={<LateStartReportGen.Loading />}> | |||||
| <LateStartReportGen /> | |||||
| </Suspense> | |||||
| ); | |||||
| }; | |||||
| export default LateStartReport; | |||||
| @@ -0,0 +1,2 @@ | |||||
| //src\components\LateStartReport\index.ts | |||||
| export { default } from "./LateStartReport"; | |||||
| @@ -0,0 +1,40 @@ | |||||
| // DownloadReportButton.tsx | |||||
| // import React, { useState } from 'react'; | |||||
| // import { generateFakeData } from '../utils/generateFakeData'; | |||||
| // import { downloadExcel } from '../utils/downloadExcel'; | |||||
| // export const DownloadReportButton: React.FC = () => { | |||||
| // const [isLoading, setIsLoading] = useState(false); | |||||
| // const handleDownload = async () => { | |||||
| // setIsLoading(true); | |||||
| // const data = generateFakeData(10); | |||||
| // downloadExcel(data); | |||||
| // setIsLoading(false); | |||||
| // }; | |||||
| // return ( | |||||
| // <button onClick={handleDownload} disabled={isLoading}> | |||||
| // {isLoading ? 'Generating...' : 'Download Report'} | |||||
| // </button> | |||||
| // ); | |||||
| // }; | |||||
| import React from 'react'; | |||||
| export const DownloadReportButton: React.FC = () => { | |||||
| const handleDownload = () => { | |||||
| const link = document.createElement('a'); | |||||
| link.href = '/temp/AR01_Late Start Report.xlsx'; // Adjust the path as necessary | |||||
| link.download = 'AR01_Late Start Report.xlsx'; | |||||
| document.body.appendChild(link); | |||||
| link.click(); | |||||
| document.body.removeChild(link); | |||||
| }; | |||||
| return ( | |||||
| <button onClick={handleDownload}> | |||||
| Download Report | |||||
| </button> | |||||
| ); | |||||
| }; | |||||
| @@ -0,0 +1,44 @@ | |||||
| //src\components\LateStartReportGen\LateStartReportGen.tsx | |||||
| "use client"; | |||||
| import React, { useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { CashFlow } from "@/app/api/cashflow"; | |||||
| import { DownloadReportButton } from './DownloadReportButton'; | |||||
| interface Props { | |||||
| projects: CashFlow[]; | |||||
| } | |||||
| type SearchQuery = Partial<Omit<CashFlow, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const ProgressByClientSearch: React.FC<Props> = ({ projects }) => { | |||||
| const { t } = useTranslation("projects"); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: "Team", paramName: "team", type: "text" }, | |||||
| { label: "Client", paramName: "client", type: "text" }, | |||||
| { | |||||
| label: "Remained Date From", | |||||
| label2: "Remained Date To", | |||||
| paramName: "targetEndDate", | |||||
| type: "dateRange", | |||||
| }, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| console.log(query); | |||||
| }} | |||||
| /> | |||||
| <DownloadReportButton /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default ProgressByClientSearch; | |||||
| @@ -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 LateStartReportGenLoading: React.FC = () => { | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default LateStartReportGenLoading; | |||||
| @@ -0,0 +1,19 @@ | |||||
| //src\components\LateStartReportGen\LateStartReportGenWrapper.tsx | |||||
| import { fetchProjectsCashFlow } from "@/app/api/cashflow"; | |||||
| import React from "react"; | |||||
| import LateStartReportGen from "./LateStartReportGen"; | |||||
| import LateStartReportGenLoading from "./LateStartReportGenLoading"; | |||||
| interface SubComponents { | |||||
| Loading: typeof LateStartReportGenLoading; | |||||
| } | |||||
| const LateStartReportGenWrapper: React.FC & SubComponents = async () => { | |||||
| const clentprojects = await fetchProjectsCashFlow(); | |||||
| return <LateStartReportGen projects={clentprojects} />; | |||||
| }; | |||||
| LateStartReportGenWrapper.Loading = LateStartReportGenLoading; | |||||
| export default LateStartReportGenWrapper; | |||||
| @@ -0,0 +1,2 @@ | |||||
| //src\components\LateStartReportGen\index.ts | |||||
| export { default } from "./LateStartReportGenWrapper"; | |||||
| @@ -47,7 +47,7 @@ const navigationItems: NavigationItem[] = [ | |||||
| children: [ | children: [ | ||||
| { | { | ||||
| icon: <SummarizeIcon />, | icon: <SummarizeIcon />, | ||||
| label: "Project Financial Summary", | |||||
| label: "Financial Summary", | |||||
| path: "/dashboard/ProjectFinancialSummary", | path: "/dashboard/ProjectFinancialSummary", | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -84,12 +84,12 @@ const navigationItems: NavigationItem[] = [ | |||||
| children: [ | children: [ | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "ClaimApproval", | |||||
| label: "Claim Approval", | |||||
| path: "/staffReimbursement/ClaimApproval", | path: "/staffReimbursement/ClaimApproval", | ||||
| }, | }, | ||||
| { | { | ||||
| icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
| label: "ClaimSummary", | |||||
| label: "Claim Summary", | |||||
| path: "/staffReimbursement/ClaimSummary", | path: "/staffReimbursement/ClaimSummary", | ||||
| }, | }, | ||||
| ], | ], | ||||
| @@ -333,7 +333,7 @@ const ProgressByClient: React.FC = () => { | |||||
| const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [ | const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [ | ||||
| { | { | ||||
| name: "Current Stage Completion Percentage", | |||||
| name: "Project Resource Consumption Percentage", | |||||
| data: [80, 55, 40, 65, 70], | data: [80, 55, 40, 65, 70], | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -426,7 +426,7 @@ const ProgressByClient: React.FC = () => { | |||||
| }, | }, | ||||
| }, | }, | ||||
| title: { | title: { | ||||
| text: "Current Stage Completion Percentage", | |||||
| text: "Project Resource Consumption Percentage", | |||||
| align: "center", | align: "center", | ||||
| }, | }, | ||||
| grid: { | grid: { | ||||
| @@ -503,7 +503,7 @@ const ProgressByClient: React.FC = () => { | |||||
| <div style={{ display: "inline-block", width: "70%" }}> | <div style={{ display: "inline-block", width: "70%" }}> | ||||
| <Grid item xs={12} md={12} lg={12}> | <Grid item xs={12} md={12} lg={12}> | ||||
| <Card> | <Card> | ||||
| <CardHeader className="text-slate-500" title="Project Progress" /> | |||||
| <CardHeader className="text-slate-500" title="Project Resource Consumption" /> | |||||
| <div style={{ display: "inline-block", width: "99%" }}> | <div style={{ display: "inline-block", width: "99%" }}> | ||||
| <ReactApexChart | <ReactApexChart | ||||
| options={options} | options={options} | ||||
| @@ -310,7 +310,7 @@ const ProgressByTeam: React.FC = () => { | |||||
| const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [ | const series: ApexAxisChartSeries | ApexNonAxisChartSeries = [ | ||||
| { | { | ||||
| name: "Current Stage Completion Percentage", | |||||
| name: "Project Resource Consumption Percentage", | |||||
| data: [80, 55, 40, 65, 70], | data: [80, 55, 40, 65, 70], | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -403,7 +403,7 @@ const ProgressByTeam: React.FC = () => { | |||||
| }, | }, | ||||
| }, | }, | ||||
| title: { | title: { | ||||
| text: "Current Stage Completion Percentage", | |||||
| text: "Project Resource Consumption Percentage", | |||||
| align: "center", | align: "center", | ||||
| }, | }, | ||||
| grid: { | grid: { | ||||
| @@ -480,7 +480,7 @@ const ProgressByTeam: React.FC = () => { | |||||
| <div style={{ display: "inline-block", width: "70%" }}> | <div style={{ display: "inline-block", width: "70%" }}> | ||||
| <Grid item xs={12} md={12} lg={12}> | <Grid item xs={12} md={12} lg={12}> | ||||
| <Card> | <Card> | ||||
| <CardHeader className="text-slate-500" title="Project Progress" /> | |||||
| <CardHeader className="text-slate-500" title="Project Resource Consumption" /> | |||||
| <div style={{ display: "inline-block", width: "99%" }}> | <div style={{ display: "inline-block", width: "99%" }}> | ||||
| <ReactApexChart | <ReactApexChart | ||||
| options={options} | options={options} | ||||
| @@ -515,7 +515,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| className="text-sm font-medium ml-5 mt-2" | className="text-sm font-medium ml-5 mt-2" | ||||
| style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
| > | > | ||||
| Total A. Receivable | |||||
| Total Invoiced Amount | |||||
| </div> | </div> | ||||
| <div | <div | ||||
| className="text-lg font-medium ml-5" | className="text-lg font-medium ml-5" | ||||
| @@ -528,7 +528,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| className="text-sm font-medium ml-5" | className="text-sm font-medium ml-5" | ||||
| style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
| > | > | ||||
| Amount Received | |||||
| Total Received Amount | |||||
| </div> | </div> | ||||
| <div | <div | ||||
| className="text-lg font-medium ml-5" | className="text-lg font-medium ml-5" | ||||
| @@ -541,7 +541,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| className="text-sm font-medium ml-5" | className="text-sm font-medium ml-5" | ||||
| style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
| > | > | ||||
| Remaining Balance | |||||
| Accounts Receivable | |||||
| </div> | </div> | ||||
| <div | <div | ||||
| className="text-lg font-medium ml-5 mb-2" | className="text-lg font-medium ml-5 mb-2" | ||||
| @@ -577,7 +577,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| className="text-sm font-medium ml-5 mt-2" | className="text-sm font-medium ml-5 mt-2" | ||||
| style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
| > | > | ||||
| Budgeted Expenditure | |||||
| Total Budget | |||||
| </div> | </div> | ||||
| <div | <div | ||||
| className="text-lg font-medium ml-5" | className="text-lg font-medium ml-5" | ||||
| @@ -590,7 +590,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| className="text-sm font-medium ml-5" | className="text-sm font-medium ml-5" | ||||
| style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
| > | > | ||||
| Actual Expenditure | |||||
| Total Cumulative Expenditure | |||||
| </div> | </div> | ||||
| <div | <div | ||||
| className="text-lg font-medium ml-5" | className="text-lg font-medium ml-5" | ||||
| @@ -603,7 +603,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| className="text-sm font-medium ml-5" | className="text-sm font-medium ml-5" | ||||
| style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
| > | > | ||||
| Remaining Balance | |||||
| Accounts Receivable | |||||
| </div> | </div> | ||||
| <div | <div | ||||
| className="text-lg font-medium ml-5 mb-2" | className="text-lg font-medium ml-5 mb-2" | ||||
| @@ -0,0 +1,201 @@ | |||||
| "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"; | |||||
| interface BaseCriterion<T extends string> { | |||||
| label: string; | |||||
| label2?: string; | |||||
| paramName: T; | |||||
| paramName2?: T; | |||||
| } | |||||
| interface TextCriterion<T extends string> extends BaseCriterion<T> { | |||||
| type: "text"; | |||||
| } | |||||
| interface SelectCriterion<T extends string> extends BaseCriterion<T> { | |||||
| type: "select"; | |||||
| options: string[]; | |||||
| } | |||||
| interface DateRangeCriterion<T extends string> extends BaseCriterion<T> { | |||||
| type: "dateRange"; | |||||
| } | |||||
| export type Criterion<T extends string> = | |||||
| | TextCriterion<T> | |||||
| | SelectCriterion<T> | |||||
| | DateRangeCriterion<T>; | |||||
| interface Props<T extends string> { | |||||
| criteria: Criterion<T>[]; | |||||
| onSearch: (inputs: Record<T, string>) => void; | |||||
| onReset?: () => void; | |||||
| } | |||||
| function SearchBox<T extends string>({ | |||||
| criteria, | |||||
| onSearch, | |||||
| onReset, | |||||
| }: Props<T>) { | |||||
| const { t } = useTranslation("common"); | |||||
| const defaultInputs = useMemo( | |||||
| () => | |||||
| criteria.reduce<Record<T, string>>( | |||||
| (acc, c) => { | |||||
| return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; | |||||
| }, | |||||
| {} as Record<T, string>, | |||||
| ), | |||||
| [criteria], | |||||
| ); | |||||
| const [inputs, setInputs] = useState(defaultInputs); | |||||
| const makeInputChangeHandler = useCallback( | |||||
| (paramName: T): React.ChangeEventHandler<HTMLInputElement> => { | |||||
| 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); | |||||
| }; | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
| <Typography variant="overline">{t("Search Criteria")}</Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| {criteria.map((c) => { | |||||
| return ( | |||||
| <Grid key={c.paramName} item xs={6}> | |||||
| {c.type === "text" && ( | |||||
| <TextField | |||||
| label={c.label} | |||||
| fullWidth | |||||
| onChange={makeInputChangeHandler(c.paramName)} | |||||
| value={inputs[c.paramName]} | |||||
| /> | |||||
| )} | |||||
| {c.type === "select" && ( | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{c.label}</InputLabel> | |||||
| <Select | |||||
| label={c.label} | |||||
| onChange={makeSelectChangeHandler(c.paramName)} | |||||
| value={inputs[c.paramName]} | |||||
| > | |||||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | |||||
| {c.options.map((option, index) => ( | |||||
| <MenuItem key={`${option}-${index}`} value={option}> | |||||
| {option} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | |||||
| </FormControl> | |||||
| )} | |||||
| {c.type === "dateRange" && ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD | |||||
| adapterLocale="zh-hk" | |||||
| > | |||||
| <Box display="flex"> | |||||
| <FormControl fullWidth> | |||||
| <DatePicker | |||||
| label={c.label} | |||||
| onChange={makeDateChangeHandler(c.paramName)} | |||||
| /> | |||||
| </FormControl> | |||||
| <Box | |||||
| display="flex" | |||||
| alignItems="center" | |||||
| justifyContent="center" | |||||
| marginInline={2} | |||||
| > | |||||
| {"-"} | |||||
| </Box> | |||||
| <FormControl fullWidth> | |||||
| <DatePicker | |||||
| label={c.label2} | |||||
| onChange={makeDateToChangeHandler(c.paramName)} | |||||
| /> | |||||
| </FormControl> | |||||
| </Box> | |||||
| </LocalizationProvider> | |||||
| )} | |||||
| </Grid> | |||||
| ); | |||||
| })} | |||||
| </Grid> | |||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button | |||||
| variant="text" | |||||
| startIcon={<RestartAlt />} | |||||
| onClick={handleReset} | |||||
| > | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Search />} | |||||
| onClick={handleSearch} | |||||
| > | |||||
| {t("Search")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| } | |||||
| export default SearchBox; | |||||
| @@ -0,0 +1,3 @@ | |||||
| //src\components\SearchBox\index.ts | |||||
| export { default } from "./SearchBox"; | |||||
| export type { Criterion } from "./SearchBox"; | |||||
| @@ -231,7 +231,7 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| if (errorRows.length > 0) { | if (errorRows.length > 0) { | ||||
| setError("addContacts", { message: "Contact details include empty fields", type: "required" }) | setError("addContacts", { message: "Contact details include empty fields", type: "required" }) | ||||
| } else { | } else { | ||||
| const errorRows_EmailFormat = rows.filter(row => !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(String(row.email))) | |||||
| const errorRows_EmailFormat = rows.filter(row => !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email))) | |||||
| if (errorRows_EmailFormat.length > 0) { | if (errorRows_EmailFormat.length > 0) { | ||||
| setError("addContacts", { message: "Contact details include empty fields", type: "email_format" }) | setError("addContacts", { message: "Contact details include empty fields", type: "email_format" }) | ||||
| @@ -75,8 +75,7 @@ const SubsidiaryDetail: React.FC<Props> = ({ | |||||
| const subsidiary = await fetchSubsidiary(parseInt(id)) | const subsidiary = await fetchSubsidiary(parseInt(id)) | ||||
| if (subsidiary !== null && Object.keys(subsidiary).length > 0) { | |||||
| console.log(subsidiary) | |||||
| if (subsidiary !== null && Object.keys(subsidiary).length > 0 && !Object.values(subsidiary.subsidiary).every(x => x === null)) { | |||||
| const tempSubsidiaryInput = { | const tempSubsidiaryInput = { | ||||
| id: subsidiary.subsidiary.id, | id: subsidiary.subsidiary.id, | ||||
| code: subsidiary.subsidiary.code ?? "", | code: subsidiary.subsidiary.code ?? "", | ||||
| @@ -158,7 +157,7 @@ const SubsidiaryDetail: React.FC<Props> = ({ | |||||
| formProps.setError("addContacts", { message: "Contact info includes empty fields", type: "required" }) | formProps.setError("addContacts", { message: "Contact info includes empty fields", type: "required" }) | ||||
| } | } | ||||
| if (data.addContacts.length > 0 && data.addContacts.filter(row => !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/.test(String(row.email))).length > 0) { | |||||
| if (data.addContacts.length > 0 && data.addContacts.filter(row => !/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/.test(String(row.email))).length > 0) { | |||||
| haveError = true | haveError = true | ||||
| formProps.setError("addContacts", { message: "Contact info includes invalid email", type: "email_format" }) | formProps.setError("addContacts", { message: "Contact info includes invalid email", type: "email_format" }) | ||||
| } | } | ||||
| @@ -0,0 +1,9 @@ | |||||
| // downloadExcel.ts | |||||
| import * as XLSX from 'xlsx-js-style'; | |||||
| export const downloadExcel = (data: any[]) => { | |||||
| const worksheet = XLSX.utils.json_to_sheet(data); | |||||
| const workbook = XLSX.utils.book_new(); | |||||
| XLSX.utils.book_append_sheet(workbook, worksheet, 'Report'); | |||||
| XLSX.writeFile(workbook, 'Report.xlsx'); | |||||
| }; | |||||
| @@ -0,0 +1,40 @@ | |||||
| // generateFakeData.ts | |||||
| import { faker } from '@faker-js/faker'; | |||||
| interface ProjectData { | |||||
| id: number; | |||||
| projectCode: string; | |||||
| projectName: string; | |||||
| team: string; | |||||
| teamLeader: string; | |||||
| startDate: string; | |||||
| startDateFrom: string; | |||||
| startDateTo: string; | |||||
| targetEndDate: string; | |||||
| client: string; | |||||
| subsidiary: string; | |||||
| nextstage: string; | |||||
| nextstageenddate: string; | |||||
| } | |||||
| export const generateFakeData = (numEntries: number): ProjectData[] => { | |||||
| const data: ProjectData[] = []; | |||||
| for (let i = 0; i < numEntries; i++) { | |||||
| data.push({ | |||||
| id: i + 1, | |||||
| projectCode: faker.datatype.uuid(), | |||||
| projectName: faker.commerce.productName(), | |||||
| team: faker.commerce.department(), | |||||
| teamLeader: faker.name.fullName(), // Corrected from findName to fullName | |||||
| startDate: faker.date.recent(90).toISOString().split('T')[0], | |||||
| startDateFrom: faker.date.past(1).toISOString().split('T')[0], | |||||
| startDateTo: faker.date.future(1).toISOString().split('T')[0], | |||||
| targetEndDate: faker.date.future(1).toISOString().split('T')[0], | |||||
| client: faker.company.name(), // Corrected from companyName to name | |||||
| subsidiary: faker.company.name(), // Corrected from companyName to name | |||||
| nextstage: "Design", | |||||
| nextstageenddate: faker.date.future(2).toISOString().split('T')[0], | |||||
| }); | |||||
| } | |||||
| return data; | |||||
| }; | |||||
| @@ -1,6 +1,6 @@ | |||||
| { | { | ||||
| "Overview": "Overview", | "Overview": "Overview", | ||||
| "customer": "Customer", | |||||
| "Create Customer": "Create Customer" | |||||
| "customer": "Client", | |||||
| "Create Customer": "Create Client" | |||||
| } | } | ||||
| @@ -15,8 +15,8 @@ | |||||
| "Customer Type": "Client Type", | "Customer Type": "Client Type", | ||||
| "Customer Allocation": "Client Allocation", | "Customer Allocation": "Client Allocation", | ||||
| "Search by customer code, name or br no.": "Search by client code, name or br no.", | "Search by customer code, name or br no.": "Search by client code, name or br no.", | ||||
| "Client Pool": "Client Pool", | |||||
| "Allocated Client": "Allocated Client", | |||||
| "Customer Pool": "Client Pool", | |||||
| "Allocated Customer": "Allocated Client", | |||||
| "Please input correct subsidiary code": "Please input correct client code", | "Please input correct subsidiary code": "Please input correct client code", | ||||
| "Please input correct subsidiary name": "Please input correct client name", | "Please input correct subsidiary name": "Please input correct client name", | ||||