@@ -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", | ||||