| @@ -1,14 +1,14 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| //src\app\(main)\analytics\ProjectCompletionReport\page.tsx | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| import ProjectCompletionReportComponent from "@/components/Report/ProjectCompletionReport"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Project Status by Client", | title: "Project Status by Client", | ||||
| }; | }; | ||||
| const ProjectLateReport: React.FC = () => { | |||||
| const ProjectCompletionReport: React.FC = () => { | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["analytics"]}> | <I18nProvider namespaces={["analytics"]}> | ||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| @@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => { | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | ||||
| <ProgressCashFlowSearch/> | <ProgressCashFlowSearch/> | ||||
| </Suspense> */} | </Suspense> */} | ||||
| <LateStartReportComponent /> | |||||
| <ProjectCompletionReportComponent /> | |||||
| </I18nProvider> | </I18nProvider> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default ProjectLateReport; | |||||
| export default ProjectCompletionReport; | |||||
| @@ -1,14 +1,14 @@ | |||||
| //src\app\(main)\analytics\LateStartReport\page.tsx | |||||
| //src\app\(main)\analytics\ProjectCompletionReport\page.tsx | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import LateStartReportComponent from "@/components/LateStartReport"; | |||||
| import ProjectCompletionReportWOComponent from "@/components/Report/ProjectCompletionReportWO"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Project Status by Client", | title: "Project Status by Client", | ||||
| }; | }; | ||||
| const ProjectLateReport: React.FC = () => { | |||||
| const ProjectCompletionReportWO: React.FC = () => { | |||||
| return ( | return ( | ||||
| <I18nProvider namespaces={["analytics"]}> | <I18nProvider namespaces={["analytics"]}> | ||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| @@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => { | |||||
| {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | {/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | ||||
| <ProgressCashFlowSearch/> | <ProgressCashFlowSearch/> | ||||
| </Suspense> */} | </Suspense> */} | ||||
| <LateStartReportComponent /> | |||||
| <ProjectCompletionReportWOComponent /> | |||||
| </I18nProvider> | </I18nProvider> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default ProjectLateReport; | |||||
| export default ProjectCompletionReportWO; | |||||
| @@ -1,3 +1,5 @@ | |||||
| import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
| import { fetchGrades } from "@/app/api/grades"; | |||||
| import { | import { | ||||
| fetchProjectBuildingTypes, | fetchProjectBuildingTypes, | ||||
| fetchProjectCategories, | fetchProjectCategories, | ||||
| @@ -31,6 +33,9 @@ const Projects: React.FC = async () => { | |||||
| fetchProjectServiceTypes(); | fetchProjectServiceTypes(); | ||||
| fetchProjectBuildingTypes(); | fetchProjectBuildingTypes(); | ||||
| fetchProjectWorkNatures(); | fetchProjectWorkNatures(); | ||||
| fetchAllCustomers(); | |||||
| fetchAllSubsidiaries(); | |||||
| fetchGrades(); | |||||
| preloadTeamLeads(); | preloadTeamLeads(); | ||||
| preloadStaff(); | preloadStaff(); | ||||
| @@ -0,0 +1,45 @@ | |||||
| // 'use client'; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import CustomInputForm from "@/components/CustomInputForm"; | |||||
| import Check from "@mui/icons-material/Check"; | |||||
| import Close from "@mui/icons-material/Close"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Tab from "@mui/material/Tab"; | |||||
| import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import React, { useCallback, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { Task, TaskTemplate } from "@/app/api/tasks"; | |||||
| import { | |||||
| FieldErrors, | |||||
| FormProvider, | |||||
| SubmitErrorHandler, | |||||
| SubmitHandler, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; | |||||
| import { Error } from "@mui/icons-material"; | |||||
| import { ProjectCategory } from "@/app/api/projects"; | |||||
| import { Grid, Typography } from "@mui/material"; | |||||
| import CreateStaffForm from "@/components/CreateStaff/CreateStaff"; | |||||
| import CreateTeam from "@/components/CreateTeam"; | |||||
| const CreateTeamPage: React.FC = async () => { | |||||
| const { t } = await getServerI18n("team"); | |||||
| const title = ['', t('Additional Info')] | |||||
| // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") | |||||
| // console.log(regex) | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Create Team")}</Typography> | |||||
| <I18nProvider namespaces={["Team"]}> | |||||
| <CreateTeam/> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreateTeamPage; | |||||
| @@ -0,0 +1,53 @@ | |||||
| import { preloadClaims } from "@/app/api/claims"; | |||||
| import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||||
| import StaffSearch from "@/components/StaffSearch"; | |||||
| import TeamSearch from "@/components/TeamSearch"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import Add from "@mui/icons-material/Add"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { Metadata } from "next"; | |||||
| import Link from "next/link"; | |||||
| import { Suspense } from "react"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Team", | |||||
| }; | |||||
| const Team: React.FC = async () => { | |||||
| const { t } = await getServerI18n("Team"); | |||||
| // preloadTeamLeads(); | |||||
| // preloadStaff(); | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Team")} | |||||
| </Typography> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/settings/team/create" | |||||
| > | |||||
| {t("Create Team")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <I18nProvider namespaces={["Team", "common"]}> | |||||
| <Suspense fallback={<TeamSearch.Loading />}> | |||||
| <TeamSearch /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Team; | |||||
| @@ -1,5 +1,5 @@ | |||||
| import CreateClaim from "@/components/CreateClaim"; | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import ClaimDetail from "@/components/ClaimDetail"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| @@ -7,15 +7,17 @@ export const metadata: Metadata = { | |||||
| title: "Create Claim", | title: "Create Claim", | ||||
| }; | }; | ||||
| const CreateClaims: React.FC = async () => { | |||||
| const { t } = await getServerI18n("claims"); | |||||
| const ClaimDetails: React.FC = async () => { | |||||
| const { t } = await getServerI18n("claim"); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Typography variant="h4">{t("Create Claim")}</Typography> | <Typography variant="h4">{t("Create Claim")}</Typography> | ||||
| <CreateClaim /> | |||||
| <I18nProvider namespaces={["claim", "common"]}> | |||||
| <ClaimDetail /> | |||||
| </I18nProvider> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default CreateClaims; | |||||
| export default ClaimDetails; | |||||
| @@ -1,6 +1,6 @@ | |||||
| import { preloadClaims } from "@/app/api/claims"; | import { preloadClaims } from "@/app/api/claims"; | ||||
| import ClaimSearch from "@/components/ClaimSearch"; | import ClaimSearch from "@/components/ClaimSearch"; | ||||
| import { getServerI18n } from "@/i18n"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| @@ -14,7 +14,7 @@ export const metadata: Metadata = { | |||||
| }; | }; | ||||
| const StaffReimbursement: React.FC = async () => { | const StaffReimbursement: React.FC = async () => { | ||||
| const { t } = await getServerI18n("claims"); | |||||
| const { t } = await getServerI18n("claim"); | |||||
| preloadClaims(); | preloadClaims(); | ||||
| return ( | return ( | ||||
| @@ -37,9 +37,11 @@ const StaffReimbursement: React.FC = async () => { | |||||
| {t("Create Claim")} | {t("Create Claim")} | ||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| <Suspense fallback={<ClaimSearch.Loading />}> | |||||
| <ClaimSearch /> | |||||
| </Suspense> | |||||
| <I18nProvider namespaces={["claim", "common"]}> | |||||
| <Suspense fallback={<ClaimSearch.Loading />}> | |||||
| <ClaimSearch /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -0,0 +1,48 @@ | |||||
| "use server"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { Claim, ProjectCombo, SupportingDocument } from "."; | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { revalidateTag } from "next/cache"; | |||||
| export interface ClaimInputFormByStaff { | |||||
| id: number | null; | |||||
| code: string | null; | |||||
| expenseType: string; | |||||
| status: string; | |||||
| addClaimDetails: ClaimDetailTable[] | |||||
| } | |||||
| export interface ClaimDetailTable { | |||||
| id: number; | |||||
| invoiceDate: Date; | |||||
| description: string; | |||||
| project: ProjectCombo; | |||||
| amount: number; | |||||
| supportingDocumentName: string; | |||||
| oldSupportingDocument: FileList[]; | |||||
| newSupportingDocument: SupportingDocument; | |||||
| isNew: boolean; | |||||
| } | |||||
| export interface SaveClaimResponse { | |||||
| claim: Claim; | |||||
| message: string; | |||||
| } | |||||
| export const saveClaim = async (data: ClaimInputFormByStaff) => { | |||||
| console.log(data) | |||||
| const saveCustomer = await serverFetchJson<SaveClaimResponse>( | |||||
| `${BASE_API_URL}/claim/save`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| revalidateTag("claims"); | |||||
| return saveCustomer; | |||||
| }; | |||||
| @@ -1,7 +1,9 @@ | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| import "server-only"; | import "server-only"; | ||||
| export interface ClaimResult { | |||||
| export interface Claim { | |||||
| id: number; | id: number; | ||||
| created: string; | created: string; | ||||
| name: string; | name: string; | ||||
| @@ -11,18 +13,52 @@ export interface ClaimResult { | |||||
| remarks: string; | remarks: string; | ||||
| } | } | ||||
| export interface ClaimSearchForm { | |||||
| id: number; | |||||
| created: string; | |||||
| createdTo: string; | |||||
| name: string; | |||||
| cost: number; | |||||
| type: "Expense" | "Petty Cash"; | |||||
| status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected"; | |||||
| remarks: string; | |||||
| } | |||||
| export interface ProjectCombo { | |||||
| id: number; | |||||
| name: string; | |||||
| code: string; | |||||
| } | |||||
| export interface SupportingDocument { | |||||
| id: number; | |||||
| skey: string; | |||||
| filename: string; | |||||
| } | |||||
| export const preloadClaims = () => { | export const preloadClaims = () => { | ||||
| fetchClaims(); | fetchClaims(); | ||||
| }; | }; | ||||
| export const fetchClaims = cache(async () => { | export const fetchClaims = cache(async () => { | ||||
| return mockClaims; | return mockClaims; | ||||
| // return serverFetchJson<Claim[]>(`${BASE_API_URL}/claim`); | |||||
| }); | |||||
| export const fetchProjectCombo = cache(async () => { | |||||
| return serverFetchJson<ProjectCombo[]>(`${BASE_API_URL}/projects`, { | |||||
| next: { tags: ["projects"] }, | |||||
| }); | |||||
| }); | }); | ||||
| const mockClaims: ClaimResult[] = [ | |||||
| // export const fetchAllCustomers = cache(async () => { | |||||
| // return serverFetchJson<Customer[]>(`${BASE_API_URL}/customer`); | |||||
| // }); | |||||
| const mockClaims: Claim[] = [ | |||||
| { | { | ||||
| id: 1, | id: 1, | ||||
| created: "2023-11-22", | |||||
| created: "2023/11/22", | |||||
| name: "Consultancy Project A", | name: "Consultancy Project A", | ||||
| cost: 121.0, | cost: 121.0, | ||||
| type: "Expense", | type: "Expense", | ||||
| @@ -31,7 +67,7 @@ const mockClaims: ClaimResult[] = [ | |||||
| }, | }, | ||||
| { | { | ||||
| id: 2, | id: 2, | ||||
| created: "2023-11-30", | |||||
| created: "2023/11/30", | |||||
| name: "Consultancy Project A", | name: "Consultancy Project A", | ||||
| cost: 4300.0, | cost: 4300.0, | ||||
| type: "Expense", | type: "Expense", | ||||
| @@ -40,7 +76,7 @@ const mockClaims: ClaimResult[] = [ | |||||
| }, | }, | ||||
| { | { | ||||
| id: 3, | id: 3, | ||||
| created: "2023-12-12", | |||||
| created: "2023/12/12", | |||||
| name: "Construction Project C", | name: "Construction Project C", | ||||
| cost: 3675.0, | cost: 3675.0, | ||||
| type: "Petty Cash", | type: "Petty Cash", | ||||
| @@ -1,5 +1,15 @@ | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { cache } from "react"; | |||||
| export interface Grade { | export interface Grade { | ||||
| name: string; | name: string; | ||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| } | } | ||||
| export const fetchGrades = cache(async () => { | |||||
| return serverFetchJson<Grade[]>(`${BASE_API_URL}/grades`, { | |||||
| next: { tags: ["grades"] }, | |||||
| }); | |||||
| }); | |||||
| @@ -4,18 +4,18 @@ import { cache } from "react"; | |||||
| import "server-only"; | import "server-only"; | ||||
| export interface PositionResult { | export interface PositionResult { | ||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description: string; | |||||
| id: number; | |||||
| code: string; | |||||
| name: string; | |||||
| description: string; | |||||
| } | } | ||||
| export const preloadPositions = () => { | export const preloadPositions = () => { | ||||
| fetchPositions(); | |||||
| fetchPositions(); | |||||
| }; | }; | ||||
| export const fetchPositions = cache(async () => { | export const fetchPositions = cache(async () => { | ||||
| return serverFetchJson<PositionResult[]>(`${BASE_API_URL}/positions`, { | |||||
| next: { tags: ["positions"] }, | |||||
| }); | |||||
| }); | |||||
| return serverFetchJson<PositionResult[]>(`${BASE_API_URL}/positions`, { | |||||
| next: { tags: ["positions"] }, | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,42 @@ | |||||
| //src\app\api\report\index.ts | |||||
| import { cache } from "react"; | |||||
| export interface ProjectCompletion { | |||||
| id: number; | |||||
| projectCode: string; | |||||
| projectName: string; | |||||
| team: string; | |||||
| teamLeader: string; | |||||
| startDate: string; | |||||
| startDateFrom: string; | |||||
| startDateTo: string; | |||||
| targetEndDate: string; | |||||
| client: string; | |||||
| subsidiary: string; | |||||
| completeDate: string; | |||||
| } | |||||
| export const preloadProjects = () => { | |||||
| fetchProjectsProjectCompletion(); | |||||
| }; | |||||
| export const fetchProjectsProjectCompletion = cache(async () => { | |||||
| return mockProjects; | |||||
| }); | |||||
| const mockProjects: ProjectCompletion[] = [ | |||||
| { | |||||
| 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", | |||||
| completeDate:"30/2/2024", | |||||
| }, | |||||
| ]; | |||||
| @@ -0,0 +1,42 @@ | |||||
| //src\app\api\report\index.ts | |||||
| import { cache } from "react"; | |||||
| export interface ProjectClaims { | |||||
| id: number; | |||||
| projectCode: string; | |||||
| projectName: string; | |||||
| team: string; | |||||
| teamLeader: string; | |||||
| startDate: string; | |||||
| startDateFrom: string; | |||||
| startDateTo: string; | |||||
| targetEndDate: string; | |||||
| client: string; | |||||
| subsidiary: string; | |||||
| completeDate: string; | |||||
| } | |||||
| export const preloadProjects = () => { | |||||
| fetchProjectsProjectClaims(); | |||||
| }; | |||||
| export const fetchProjectsProjectClaims = cache(async () => { | |||||
| return mockProjects; | |||||
| }); | |||||
| const mockProjects: ProjectClaims[] = [ | |||||
| { | |||||
| 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", | |||||
| completeDate:"30/2/2024", | |||||
| }, | |||||
| ]; | |||||
| @@ -0,0 +1,20 @@ | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { cache } from "react"; | |||||
| import "server-only"; | |||||
| export interface TeamResult { | |||||
| action: any; | |||||
| id: number; | |||||
| name: string; | |||||
| code: string; | |||||
| description: string; | |||||
| } | |||||
| export const fetchTeam = cache(async () => { | |||||
| return serverFetchJson<TeamResult[]>(`${BASE_API_URL}/team`, { | |||||
| next: { tags: ["team"] }, | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,28 @@ | |||||
| import { useIntl } from "react-intl"; | |||||
| const intl = useIntl() | |||||
| export const TEAM_COMBO = [ | |||||
| { id: 1, key: 1, label: 'AAA', value: "AAA" }, | |||||
| { id: 2, key: 2, label: 'BBB', value: "BBB" }, | |||||
| { id: 3, key: 3, label: 'CCC', value: "CCC" }, | |||||
| ]; | |||||
| export const CLIENT_COMBO = [ | |||||
| { id: 1, key: 1, label: 'Cust A', value: "Cust A" }, | |||||
| { id: 2, key: 2, label: 'Cust B', value: "Cust B" }, | |||||
| { id: 3, key: 3, label: 'Cust C', value: "Cust C" }, | |||||
| ]; | |||||
| export function LOCALE_COMBO() { | |||||
| return ([ | |||||
| {id: 1,label: intl.formatMessage({ id: "en" }),value: "en",}, | |||||
| {id: 2,label: intl.formatMessage({ id: "zh-HK" }),value: "zh-HK",}, | |||||
| {id: 3,label: intl.formatMessage({ id: "zh-CN" }),value: "zh-CN",}, | |||||
| ]) | |||||
| } | |||||
| export function OVERCONSUMPTION_COMBO() { | |||||
| return ([ | |||||
| {id: 1,label: intl.formatMessage({ id: "Overconsumption" }),value: "Overconsumption",}, | |||||
| {id: 2,label: intl.formatMessage({ id: "Potential Overconsumption" }),value: "Potential Overconsumption",}, | |||||
| ]) | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| export const expenseTypeCombo = [ | |||||
| "Petty Cash", | |||||
| "Expense" | |||||
| ] | |||||
| export const claimStatusCombo = [ | |||||
| "Not Submitted", | |||||
| "Waiting for Approval", | |||||
| "Approved", | |||||
| "Rejected" | |||||
| ] | |||||
| @@ -0,0 +1,23 @@ | |||||
| export const dateInRange = (currentDate: string, startDate: string, endDate: string) => { | |||||
| if (currentDate === undefined) { | |||||
| return false // can be changed to true if necessary | |||||
| } | |||||
| const currentDateTime = new Date(currentDate).getTime() | |||||
| const startDateTime = startDate === undefined || startDate.length === 0 ? undefined : new Date(startDate).getTime() | |||||
| const endDateTime = endDate === undefined || startDate.length === 0 ? undefined : new Date(endDate).getTime() | |||||
| // console.log(currentDateTime, startDateTime, endDateTime) | |||||
| if (startDateTime === undefined && endDateTime !== undefined) { | |||||
| return currentDateTime <= endDateTime | |||||
| } else if (startDateTime !== undefined && endDateTime === undefined) { | |||||
| return currentDateTime >= startDateTime | |||||
| } else { | |||||
| if (startDateTime !== undefined && endDateTime !== undefined) { | |||||
| return currentDateTime >= startDateTime && currentDateTime <= endDateTime | |||||
| } else { | |||||
| return true | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,3 +1,5 @@ | |||||
| import dayjs from "dayjs"; | |||||
| export const manhourFormatter = new Intl.NumberFormat("en-HK", { | export const manhourFormatter = new Intl.NumberFormat("en-HK", { | ||||
| minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
| maximumFractionDigits: 2, | maximumFractionDigits: 2, | ||||
| @@ -15,6 +17,12 @@ export const percentFormatter = new Intl.NumberFormat("en-HK", { | |||||
| export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; | export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; | ||||
| export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD"; | |||||
| export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FORMAT) => { | |||||
| return dayjs(date).format(format) | |||||
| } | |||||
| const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | ||||
| weekday: "short", | weekday: "short", | ||||
| year: "numeric", | year: "numeric", | ||||
| @@ -0,0 +1,33 @@ | |||||
| import useJwt from 'auth/jwt/coreUseJwt' | |||||
| /** | |||||
| * Return if user is logged in | |||||
| * This is completely up to you and how you want to store the token in your frontend application | |||||
| * e.g. If you are using cookies to store the application please update this function | |||||
| */ | |||||
| // eslint-disable-next-line arrow-body-style | |||||
| export const hostname = process.env.REACT_APP_BACKEND_HOST | |||||
| const hostPort = process.env.REACT_APP_BACKEND_PORT | |||||
| export const hostPath = `${process.env.REACT_APP_BACKEND_PROTOCOL}://${hostname}:${hostPort}` | |||||
| export const apiPath = `${hostPath}/api` | |||||
| export const isUserLoggedIn = () => { | |||||
| return localStorage.getItem('userData') && localStorage.getItem(useJwt.jwtConfig.storageTokenKeyName) | |||||
| } | |||||
| export const getUserData = () => JSON.parse(localStorage.getItem('userData')) | |||||
| /** | |||||
| * This function is used for demo purpose route navigation | |||||
| * In real app you won't need this function because your app will navigate to same route for each users regardless of ability | |||||
| * Please note role field is just for showing purpose it's not used by anything in frontend | |||||
| * We are checking role just for ease | |||||
| * NOTE: If you have different pages to navigate based on user ability then this function can be useful. However, you need to update it. | |||||
| * @param {String} userRole Role of user | |||||
| */ | |||||
| export const getHomeRouteForLoggedInUser = userRole => { | |||||
| if (userRole === 'admin') return '/' | |||||
| if (userRole === 'user') return '/' | |||||
| if (userRole === 'client') return {name: 'access-control'} | |||||
| return {name: 'auth-login'} | |||||
| } | |||||
| @@ -0,0 +1,105 @@ | |||||
| "use client"; | |||||
| import Check from "@mui/icons-material/Check"; | |||||
| import Close from "@mui/icons-material/Close"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import React, { useCallback, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import ClaimFormInfo from "./ClaimFormInfo"; | |||||
| import { ProjectCombo } from "@/app/api/claims"; | |||||
| import { Typography } from "@mui/material"; | |||||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||||
| import { ClaimInputFormByStaff, saveClaim } from "@/app/api/claims/actions"; | |||||
| import { DoneAll } from "@mui/icons-material"; | |||||
| import { expenseTypeCombo } from "@/app/utils/comboUtil"; | |||||
| export interface Props { | |||||
| projectCombo: ProjectCombo[] | |||||
| } | |||||
| const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
| const { t } = useTranslation("common"); | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const router = useRouter(); | |||||
| const formProps = useForm<ClaimInputFormByStaff>({ | |||||
| defaultValues: { | |||||
| id: null, | |||||
| expenseType: expenseTypeCombo[0], | |||||
| addClaimDetails: [] | |||||
| }, | |||||
| }); | |||||
| const handleCancel = () => { | |||||
| router.back(); | |||||
| }; | |||||
| const onSubmit = useCallback<SubmitHandler<ClaimInputFormByStaff>>( | |||||
| async (data, event) => { | |||||
| try { | |||||
| console.log(data); | |||||
| console.log((event?.nativeEvent as any).submitter.name); | |||||
| const buttonName = (event?.nativeEvent as any).submitter.name | |||||
| console.log(JSON.stringify(data)) | |||||
| // const formData = new FormData() | |||||
| // formData.append("expenseType", data.expenseType) | |||||
| // formData.append("claimDetails", data.addClaimDetails) | |||||
| if (buttonName === "submit") { | |||||
| data.status = "Not Submitted" | |||||
| } else if (buttonName === "save") { | |||||
| data.status = "Waiting for Approval" | |||||
| } | |||||
| // for (let i = 0; i < data.addClaimDetails.length; i++) { | |||||
| // // const formData = new FormData(); | |||||
| // // formData.append("newSupportingDocument", data.addClaimDetails[i].oldSupportingDocument); | |||||
| // data.addClaimDetails[i].oldSupportingDocument = new Blob([data.addClaimDetails[i].oldSupportingDocument], {type: data.addClaimDetails[i].oldSupportingDocument.type}) | |||||
| // } | |||||
| console.log(data); | |||||
| await saveClaim(data) | |||||
| setServerError(""); | |||||
| // await saveProject(data); | |||||
| // router.replace("/projects"); | |||||
| } catch (e) { | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| }, | |||||
| [router, t], | |||||
| ); | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<ClaimInputFormByStaff>>( | |||||
| (errors) => { | |||||
| // Set the tab so that the focus will go there | |||||
| console.log(errors) | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| return ( | |||||
| <FormProvider {...formProps}> | |||||
| <Stack spacing={2} component={"form"} onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}> | |||||
| <ClaimFormInfo projectCombo={projectCombo} /> | |||||
| {serverError && ( | |||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
| {serverError} | |||||
| </Typography> | |||||
| )} | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button variant="text" startIcon={<Close />} onClick={handleCancel}> | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button variant="outlined" name="save" startIcon={<Check />} type="submit"> | |||||
| {t("Save")} | |||||
| </Button> | |||||
| <Button variant="contained" name="submit" startIcon={<DoneAll />} type="submit"> | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| ); | |||||
| }; | |||||
| export default ClaimDetail; | |||||
| @@ -0,0 +1,20 @@ | |||||
| import React from "react"; | |||||
| import ClaimDetail from "./ClaimDetail"; | |||||
| import { fetchProjectCombo } from "@/app/api/claims"; | |||||
| // import TaskSetup from "./TaskSetup"; | |||||
| // import StaffAllocation from "./StaffAllocation"; | |||||
| // import ResourceMilestone from "./ResourceMilestone"; | |||||
| const ClaimDetailWrapper: React.FC = async () => { | |||||
| const [projectCombo] = | |||||
| await Promise.all([ | |||||
| fetchProjectCombo() | |||||
| ]); | |||||
| return ( | |||||
| <ClaimDetail projectCombo={projectCombo}/> | |||||
| ); | |||||
| }; | |||||
| export default ClaimDetailWrapper; | |||||
| @@ -0,0 +1,76 @@ | |||||
| "use client"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import FormControl from "@mui/material/FormControl"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import InputLabel from "@mui/material/InputLabel"; | |||||
| import MenuItem from "@mui/material/MenuItem"; | |||||
| import Select from "@mui/material/Select"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import ClaimFormInputGrid from "./ClaimFormInputGrid"; | |||||
| import { expenseTypeCombo } from "@/app/utils/comboUtil"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | |||||
| import { ClaimInputFormByStaff } from "@/app/api/claims/actions"; | |||||
| import { ProjectCombo } from "@/app/api/claims"; | |||||
| import { TextField } from "@mui/material"; | |||||
| interface Props { | |||||
| projectCombo: ProjectCombo[] | |||||
| } | |||||
| const ClaimFormInfo: React.FC<Props> = ({ projectCombo }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| control, | |||||
| register, | |||||
| } = useFormContext<ClaimInputFormByStaff>(); | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t("Claim Code")} | |||||
| {...register("code")} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{t("Expense Type")}</InputLabel> | |||||
| <Controller | |||||
| // defaultValue={expenseTypeCombo[0].value} | |||||
| control={control} | |||||
| name="expenseType" | |||||
| render={({ field }) => ( | |||||
| <Select label={t("Expense Type")} {...field}> | |||||
| { | |||||
| expenseTypeCombo.map((type, index) => ( | |||||
| <MenuItem key={`${type}-${index}`} value={type}> | |||||
| {t(type)} | |||||
| </MenuItem> | |||||
| )) | |||||
| } | |||||
| </Select> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| <Card> | |||||
| <ClaimFormInputGrid projectCombo={projectCombo} /> | |||||
| </Card> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default ClaimFormInfo; | |||||
| @@ -8,16 +8,9 @@ import { Suspense } from "react"; | |||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import { t } from "i18next"; | |||||
| import { | import { | ||||
| Box, | |||||
| Container, | |||||
| Modal, | |||||
| Select, | |||||
| SelectChangeEvent, | |||||
| Typography, | |||||
| Box, Card, Typography, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { Close } from "@mui/icons-material"; | |||||
| import AddIcon from "@mui/icons-material/Add"; | import AddIcon from "@mui/icons-material/Add"; | ||||
| import EditIcon from "@mui/icons-material/Edit"; | import EditIcon from "@mui/icons-material/Edit"; | ||||
| import DeleteIcon from "@mui/icons-material/DeleteOutlined"; | import DeleteIcon from "@mui/icons-material/DeleteOutlined"; | ||||
| @@ -25,35 +18,30 @@ import SaveIcon from "@mui/icons-material/Save"; | |||||
| import CancelIcon from "@mui/icons-material/Close"; | import CancelIcon from "@mui/icons-material/Close"; | ||||
| import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; | import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; | ||||
| import ImageNotSupportedOutlinedIcon from "@mui/icons-material/ImageNotSupportedOutlined"; | import ImageNotSupportedOutlinedIcon from "@mui/icons-material/ImageNotSupportedOutlined"; | ||||
| import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; | |||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||||
| import Swal from "sweetalert2"; | |||||
| import { msg } from "../Swal/CustomAlerts"; | |||||
| import React from "react"; | import React from "react"; | ||||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||||
| import { | import { | ||||
| GridRowsProp, | GridRowsProp, | ||||
| GridRowModesModel, | GridRowModesModel, | ||||
| GridRowModes, | GridRowModes, | ||||
| DataGrid, | DataGrid, | ||||
| GridColDef, | GridColDef, | ||||
| GridToolbarContainer, | |||||
| GridFooterContainer, | |||||
| GridActionsCellItem, | GridActionsCellItem, | ||||
| GridEventListener, | GridEventListener, | ||||
| GridRowId, | GridRowId, | ||||
| GridRowModel, | GridRowModel, | ||||
| GridRowEditStopReasons, | GridRowEditStopReasons, | ||||
| GridEditInputCell, | GridEditInputCell, | ||||
| GridValueSetterParams, | |||||
| GridTreeNodeWithRender, | |||||
| GridRenderCellParams, | |||||
| } from "@mui/x-data-grid"; | } from "@mui/x-data-grid"; | ||||
| import { LocalizationProvider } from "@mui/x-date-pickers"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { Props } from "react-intl/src/components/relative"; | import { Props } from "react-intl/src/components/relative"; | ||||
| import palette from "@/theme/devias-material-kit/palette"; | import palette from "@/theme/devias-material-kit/palette"; | ||||
| const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; | |||||
| import { ProjectCombo } from "@/app/api/claims"; | |||||
| import { ClaimDetailTable, ClaimInputFormByStaff } from "@/app/api/claims/actions"; | |||||
| import { useFieldArray, useFormContext } from "react-hook-form"; | |||||
| import { GridRenderEditCellParams } from "@mui/x-data-grid"; | |||||
| import { convertDateToString } from "@/app/utils/formatUtil"; | |||||
| interface BottomBarProps { | interface BottomBarProps { | ||||
| getCostTotal: () => number; | getCostTotal: () => number; | ||||
| @@ -63,15 +51,6 @@ interface BottomBarProps { | |||||
| ) => void; | ) => void; | ||||
| } | } | ||||
| interface EditToolbarProps { | |||||
| // setDay: (newDay : dayjs.Dayjs) => void; | |||||
| setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; | |||||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
| setRowModesModel: ( | |||||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
| ) => void; | |||||
| } | |||||
| interface EditFooterProps { | interface EditFooterProps { | ||||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | ||||
| setRowModesModel: ( | setRowModesModel: ( | ||||
| @@ -80,17 +59,17 @@ interface EditFooterProps { | |||||
| } | } | ||||
| const BottomBar = (props: BottomBarProps) => { | const BottomBar = (props: BottomBarProps) => { | ||||
| const { t } = useTranslation("claim") | |||||
| const { setRows, setRowModesModel, getCostTotal } = props; | const { setRows, setRowModesModel, getCostTotal } = props; | ||||
| // const getCostTotal = props.getCostTotal; | // const getCostTotal = props.getCostTotal; | ||||
| const [newId, setNewId] = useState(-1); | const [newId, setNewId] = useState(-1); | ||||
| const [invalidDays, setInvalidDays] = useState(0); | |||||
| const handleAddClick = () => { | const handleAddClick = () => { | ||||
| const id = newId; | const id = newId; | ||||
| setNewId(newId - 1); | setNewId(newId - 1); | ||||
| setRows((oldRows) => [ | setRows((oldRows) => [ | ||||
| ...oldRows, | ...oldRows, | ||||
| { id, projectCode: "", task: "", isNew: true }, | |||||
| { id, invoiceDate: new Date(), project: null, description: null, amount: null, newSupportingDocument: null, supportingDocumentName: null, isNew: true }, | |||||
| ]); | ]); | ||||
| setRowModesModel((oldModel) => ({ | setRowModesModel((oldModel) => ({ | ||||
| ...oldModel, | ...oldModel, | ||||
| @@ -98,11 +77,6 @@ const BottomBar = (props: BottomBarProps) => { | |||||
| })); | })); | ||||
| }; | }; | ||||
| const totalColDef = { | |||||
| flex: 1, | |||||
| // style: {color:getCostTotal('mon')>24?"red":"black"} | |||||
| }; | |||||
| const TotalCell = ({ value }: Props) => { | const TotalCell = ({ value }: Props) => { | ||||
| const [invalid, setInvalid] = useState(false); | const [invalid, setInvalid] = useState(false); | ||||
| @@ -122,7 +96,7 @@ const BottomBar = (props: BottomBarProps) => { | |||||
| <div> | <div> | ||||
| <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | ||||
| <Box flex={1.5} textAlign={"right"} marginRight="4rem"> | <Box flex={1.5} textAlign={"right"} marginRight="4rem"> | ||||
| <b>Total:</b> | |||||
| <b>{t("Total")}:</b> | |||||
| </Box> | </Box> | ||||
| <TotalCell value={getCostTotal()} /> | <TotalCell value={getCostTotal()} /> | ||||
| </div> | </div> | ||||
| @@ -133,7 +107,7 @@ const BottomBar = (props: BottomBarProps) => { | |||||
| onClick={handleAddClick} | onClick={handleAddClick} | ||||
| sx={{ margin: "20px" }} | sx={{ margin: "20px" }} | ||||
| > | > | ||||
| Add record | |||||
| {t("Add Record")} | |||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -150,40 +124,51 @@ const EditFooter = (props: EditFooterProps) => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| interface ClaimInputGridProps { | |||||
| onClose?: () => void; | |||||
| interface ClaimFormInputGridProps { | |||||
| // onClose?: () => void; | |||||
| projectCombo: ProjectCombo[] | |||||
| } | } | ||||
| const initialRows: GridRowsProp = [ | const initialRows: GridRowsProp = [ | ||||
| { | { | ||||
| id: 1, | id: 1, | ||||
| date: new Date(), | |||||
| invoiceDate: new Date(), | |||||
| description: "Taxi to client office", | description: "Taxi to client office", | ||||
| cost: 169.5, | |||||
| document: "taxi_receipt.jpg", | |||||
| amount: 169.5, | |||||
| supportingDocumentName: "taxi_receipt.jpg", | |||||
| }, | }, | ||||
| { | { | ||||
| id: 2, | id: 2, | ||||
| date: dayjs().add(-14, "days").toDate(), | |||||
| invoiceDate: dayjs().add(-14, "days").toDate(), | |||||
| description: "MTR fee to Kowloon Bay Office", | description: "MTR fee to Kowloon Bay Office", | ||||
| cost: 15.5, | |||||
| document: "octopus_invoice.jpg", | |||||
| amount: 15.5, | |||||
| supportingDocumentName: "octopus_invoice.jpg", | |||||
| }, | }, | ||||
| { | { | ||||
| id: 3, | id: 3, | ||||
| date: dayjs().add(-44, "days").toDate(), | |||||
| invoiceDate: dayjs().add(-44, "days").toDate(), | |||||
| description: "Starbucks", | description: "Starbucks", | ||||
| cost: 504, | |||||
| amount: 504, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||||
| const [rows, setRows] = useState(initialRows); | |||||
| const [day, setDay] = useState(dayjs()); | |||||
| const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||||
| // onClose, | |||||
| projectCombo, | |||||
| }) => { | |||||
| const { t } = useTranslation() | |||||
| const { control, setValue, getValues, formState: { errors } } = useFormContext<ClaimInputFormByStaff>(); | |||||
| const { fields } = useFieldArray({ | |||||
| control, | |||||
| name: "addClaimDetails" | |||||
| }) | |||||
| const [rows, setRows] = useState<GridRowsProp>([]); | |||||
| const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>( | const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>( | ||||
| {}, | {}, | ||||
| ); | ); | ||||
| // Row function | |||||
| const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | ||||
| params, | params, | ||||
| event, | event, | ||||
| @@ -217,20 +202,77 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||||
| } | } | ||||
| }; | }; | ||||
| const processRowUpdate = (newRow: GridRowModel) => { | |||||
| const updatedRow = { ...newRow, isNew: false }; | |||||
| setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); | |||||
| return updatedRow; | |||||
| }; | |||||
| const processRowUpdate = React.useCallback((newRow: GridRowModel) => { | |||||
| const updatedRow = { ...newRow }; | |||||
| const updatedRows = rows.map((row) => (row.id === newRow.id ? { ...updatedRow, supportingDocumentName: row.supportingDocumentName } : row)) | |||||
| setRows(updatedRows); | |||||
| setValue("addClaimDetails", updatedRows as ClaimDetailTable[]) | |||||
| return updatedRows.find((row) => row.id === newRow.id) as GridRowModel; | |||||
| }, [rows, rowModesModel, t]); | |||||
| const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { | const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { | ||||
| setRowModesModel(newRowModesModel); | setRowModesModel(newRowModesModel); | ||||
| }; | }; | ||||
| // File Upload function | |||||
| const fileInputRef: React.RefObject<Record<string, HTMLInputElement | null>> = React.useRef({}) | |||||
| const setFileInputRefs = (ele: HTMLInputElement | null, key: string) => { | |||||
| if (fileInputRef.current !== null) { | |||||
| fileInputRef.current[key] = ele | |||||
| } | |||||
| } | |||||
| useEffect(() => { | |||||
| }, []) | |||||
| const handleFileSelect = (key: string) => { | |||||
| if (fileInputRef !== null && fileInputRef.current !== null && fileInputRef.current[key] !== null) { | |||||
| fileInputRef.current[key]?.click() | |||||
| } | |||||
| } | |||||
| const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>, params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => { | |||||
| const file = event.target.files?.[0] ?? null | |||||
| if (file !== null) { | |||||
| console.log(file) | |||||
| console.log(typeof file) | |||||
| const updatedRows = rows.map((row) => (row.id === params.row.id ? { ...row, supportingDocumentName: file.name, newSupportingDocument: file } : row)) | |||||
| setRows(updatedRows); | |||||
| setValue("addClaimDetails", updatedRows as ClaimDetailTable[]) | |||||
| // const url = URL.createObjectURL(new Blob([file])); | |||||
| // const link = document.createElement("a"); | |||||
| // link.href = url; | |||||
| // link.setAttribute("download", file.name); | |||||
| // link.click(); | |||||
| } | |||||
| } | |||||
| const handleFileDelete = (id: number) => { | |||||
| const updatedRows = rows.map((row) => (row.id === id ? { ...row, supportingDocumentName: null, newSupportingDocument: null } : row)) | |||||
| setRows(updatedRows); | |||||
| setValue("addClaimDetails", updatedRows as ClaimDetailTable[]) | |||||
| } | |||||
| const handleLinkClick = (params: GridRenderEditCellParams<any, any, any, GridTreeNodeWithRender>) => { | |||||
| const url = URL.createObjectURL(new Blob([params.row.newSupportingDocument])); | |||||
| const link = document.createElement("a"); | |||||
| link.href = url; | |||||
| link.setAttribute("download", params.row.supportingDocumentName); | |||||
| link.click(); | |||||
| // console.log(params) | |||||
| // console.log(rows) | |||||
| } | |||||
| // columns | |||||
| const getCostTotal = () => { | const getCostTotal = () => { | ||||
| let sum = 0; | let sum = 0; | ||||
| rows.forEach((row) => { | rows.forEach((row) => { | ||||
| sum += row["cost"] ?? 0; | |||||
| sum += row["amount"] ?? 0; | |||||
| }); | }); | ||||
| return sum; | return sum; | ||||
| }; | }; | ||||
| @@ -256,11 +298,11 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||||
| ), | ), | ||||
| }; | }; | ||||
| const columns: GridColDef[] = [ | |||||
| const columns: GridColDef[] = React.useMemo(() => [ | |||||
| { | { | ||||
| field: "actions", | field: "actions", | ||||
| type: "actions", | type: "actions", | ||||
| headerName: "Actions", | |||||
| headerName: t("Actions"), | |||||
| width: 100, | width: 100, | ||||
| cellClassName: "actions", | cellClassName: "actions", | ||||
| getActions: ({ id }) => { | getActions: ({ id }) => { | ||||
| @@ -312,24 +354,50 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| field: "date", | |||||
| headerName: "Invoice Date", | |||||
| field: "invoiceDate", | |||||
| headerName: t("Invoice Date"), | |||||
| // width: 220, | // width: 220, | ||||
| flex: 1, | flex: 1, | ||||
| editable: true, | editable: true, | ||||
| type: "date", | type: "date", | ||||
| renderCell: (params: GridRenderCellParams<any, Date>) => { | |||||
| return convertDateToString(params.value!!) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "project", | |||||
| headerName: t("Project"), | |||||
| // width: 220, | |||||
| flex: 1, | |||||
| editable: true, | |||||
| type: "singleSelect", | |||||
| getOptionLabel: (value: any) => { | |||||
| return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; | |||||
| }, | |||||
| getOptionValue: (value: any) => value, | |||||
| valueOptions: () => { | |||||
| const options = projectCombo ?? [] | |||||
| if (options.length === 0) { | |||||
| options.push({ id: -1, code: "", name: "No Projects" }) | |||||
| } | |||||
| return options; | |||||
| }, | |||||
| valueGetter: (params) => { | |||||
| return params.value ?? projectCombo[0].id ?? -1 | |||||
| }, | |||||
| }, | }, | ||||
| { | { | ||||
| field: "description", | field: "description", | ||||
| headerName: "Description", | |||||
| headerName: t("Description"), | |||||
| // width: 220, | // width: 220, | ||||
| flex: 2, | flex: 2, | ||||
| editable: true, | editable: true, | ||||
| type: "string", | type: "string", | ||||
| }, | }, | ||||
| { | { | ||||
| field: "cost", | |||||
| headerName: "Cost (HKD)", | |||||
| field: "amount", | |||||
| headerName: t("Amount (HKD)"), | |||||
| editable: true, | editable: true, | ||||
| type: "number", | type: "number", | ||||
| valueFormatter: (params) => { | valueFormatter: (params) => { | ||||
| @@ -337,31 +405,34 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| field: "document", | |||||
| headerName: "Supporting Document", | |||||
| type: "string", | |||||
| field: "supportingDocumentName", | |||||
| headerName: t("Supporting Document"), | |||||
| // type: "string", | |||||
| editable: true, | editable: true, | ||||
| flex: 2, | flex: 2, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| return params.value ? ( | return params.value ? ( | ||||
| <span> | <span> | ||||
| <a href="" target="_blank" rel="noopener noreferrer"> | |||||
| <Link onClick={() => handleLinkClick(params)} href="#">{params.value}</Link> | |||||
| {/* <a href="" target="_blank" rel="noopener noreferrer"> | |||||
| {params.value} | {params.value} | ||||
| </a> | |||||
| </a> */} | |||||
| </span> | </span> | ||||
| ) : ( | ) : ( | ||||
| <span style={{ color: palette.text.disabled }}>No Documents</span> | <span style={{ color: palette.text.disabled }}>No Documents</span> | ||||
| ); | ); | ||||
| }, | }, | ||||
| renderEditCell: (params) => { | renderEditCell: (params) => { | ||||
| return params.value ? ( | |||||
| const currentRow = rows.find(row => row.id === params.row.id); | |||||
| return params.formattedValue ? ( | |||||
| <span> | <span> | ||||
| <a href="" target="_blank" rel="noopener noreferrer"> | |||||
| {params.value} | |||||
| </a> | |||||
| <Link onClick={() => handleLinkClick(params)} href="#">{params.formattedValue}</Link> | |||||
| {/* <a href="" target="_blank" rel="noopener noreferrer"> | |||||
| {params.formattedValue} | |||||
| </a> */} | |||||
| <Button | <Button | ||||
| title="Remove Document" | title="Remove Document" | ||||
| onClick={(event) => console.log(event)} | |||||
| onClick={() => handleFileDelete(params.row.id)} | |||||
| > | > | ||||
| <ImageNotSupportedOutlinedIcon | <ImageNotSupportedOutlinedIcon | ||||
| sx={{ fontSize: "25px", color: "red" }} | sx={{ fontSize: "25px", color: "red" }} | ||||
| @@ -369,15 +440,24 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||||
| </Button> | </Button> | ||||
| </span> | </span> | ||||
| ) : ( | ) : ( | ||||
| <Button title="Add Document"> | |||||
| <AddPhotoAlternateOutlinedIcon | |||||
| sx={{ fontSize: "25px", color: "green" }} | |||||
| <div> | |||||
| <input | |||||
| type="file" | |||||
| ref={ele => setFileInputRefs(ele, params.row.id)} | |||||
| accept="image/jpg, image/jpeg, image/png, .doc, .docx, .pdf" | |||||
| style={{ display: 'none' }} | |||||
| onChange={(event) => handleFileChange(event, params)} | |||||
| /> | /> | ||||
| </Button> | |||||
| <Button title="Add Document" onClick={() => handleFileSelect(params.row.id)}> | |||||
| <AddPhotoAlternateOutlinedIcon | |||||
| sx={{ fontSize: "25px", color: "green" }} | |||||
| /> | |||||
| </Button> | |||||
| </div> | |||||
| ); | ); | ||||
| }, | }, | ||||
| }, | }, | ||||
| ]; | |||||
| ], [rows, rowModesModel, t],); | |||||
| return ( | return ( | ||||
| <Box | <Box | ||||
| @@ -402,41 +482,48 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||||
| }, | }, | ||||
| }} | }} | ||||
| > | > | ||||
| <DataGrid | |||||
| sx={{ flex: 1 }} | |||||
| rows={rows} | |||||
| columns={columns} | |||||
| editMode="row" | |||||
| rowModesModel={rowModesModel} | |||||
| onRowModesModelChange={handleRowModesModelChange} | |||||
| onRowEditStop={handleRowEditStop} | |||||
| processRowUpdate={processRowUpdate} | |||||
| disableRowSelectionOnClick={true} | |||||
| disableColumnMenu={true} | |||||
| hideFooterPagination={true} | |||||
| slots={ | |||||
| { | |||||
| // footer: EditFooter, | |||||
| {Boolean(errors.addClaimDetails?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | |||||
| {t("Please ensure at least one row is created, and all the fields are inputted and saved")} | |||||
| </Typography>} | |||||
| {Boolean(errors.addClaimDetails?.type === "format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | |||||
| {t("Please ensure the date formats are correct")} | |||||
| </Typography>} | |||||
| <div style={{ height: 400, width: "100%" }}> | |||||
| <DataGrid | |||||
| sx={{ flex: 1 }} | |||||
| rows={rows} | |||||
| columns={columns} | |||||
| editMode="row" | |||||
| rowModesModel={rowModesModel} | |||||
| onRowModesModelChange={handleRowModesModelChange} | |||||
| onRowEditStop={handleRowEditStop} | |||||
| processRowUpdate={processRowUpdate} | |||||
| disableRowSelectionOnClick={true} | |||||
| disableColumnMenu={true} | |||||
| // hideFooterPagination={true} | |||||
| slots={ | |||||
| { | |||||
| // footer: EditFooter, | |||||
| } | |||||
| } | } | ||||
| } | |||||
| slotProps={ | |||||
| { | |||||
| // footer: { setDay, setRows, setRowModesModel }, | |||||
| slotProps={ | |||||
| { | |||||
| // footer: { setDay, setRows, setRowModesModel }, | |||||
| } | |||||
| } | } | ||||
| } | |||||
| initialState={{ | |||||
| pagination: { paginationModel: { pageSize: 100 } }, | |||||
| }} | |||||
| /> | |||||
| initialState={{ | |||||
| pagination: { paginationModel: { pageSize: 5 } }, | |||||
| }} | |||||
| /> | |||||
| </div> | |||||
| <BottomBar | <BottomBar | ||||
| getCostTotal={getCostTotal} | getCostTotal={getCostTotal} | ||||
| setRows={setRows} | setRows={setRows} | ||||
| setRowModesModel={setRowModesModel} | setRowModesModel={setRowModesModel} | ||||
| // sx={{flex:2}} | |||||
| // sx={{flex:2}} | |||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default ClaimInputGrid; | |||||
| export default ClaimFormInputGrid; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./ClaimDetailWrapper"; | |||||
| @@ -1,65 +1,52 @@ | |||||
| "use client"; | "use client"; | ||||
| import { ClaimResult } from "@/app/api/claims"; | |||||
| import { Claim, ClaimSearchForm } from "@/app/api/claims"; | |||||
| import React, { useCallback, useMemo, useState } from "react"; | import React, { useCallback, useMemo, useState } from "react"; | ||||
| import SearchBox, { Criterion } from "../SearchBox/index"; | import SearchBox, { Criterion } from "../SearchBox/index"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchResults, { Column } from "../SearchResults/index"; | import SearchResults, { Column } from "../SearchResults/index"; | ||||
| import EditNote from "@mui/icons-material/EditNote"; | import EditNote from "@mui/icons-material/EditNote"; | ||||
| import { dateInRange } from "@/app/utils/commonUtil"; | |||||
| import { claimStatusCombo, expenseTypeCombo } from "@/app/utils/comboUtil"; | |||||
| interface Props { | interface Props { | ||||
| claims: ClaimResult[]; | |||||
| claims: Claim[]; | |||||
| } | } | ||||
| type SearchQuery = Partial<Omit<ClaimResult, "id">>; | |||||
| type SearchQuery = Partial<Omit<ClaimSearchForm, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| const ClaimSearch: React.FC<Props> = ({ claims }) => { | const ClaimSearch: React.FC<Props> = ({ claims }) => { | ||||
| const { t } = useTranslation("claims"); | |||||
| const { t } = useTranslation(); | |||||
| // If claim searching is done on the server-side, then no need for this. | // If claim searching is done on the server-side, then no need for this. | ||||
| const [filteredClaims, setFilteredClaims] = useState(claims); | const [filteredClaims, setFilteredClaims] = useState(claims); | ||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| { label: t("Creation Date"), paramName: "created", type: "dateRange" }, | |||||
| { label: t("Creation Date From"), label2: t("Creation Date To"), paramName: "created", type: "dateRange" }, | |||||
| { label: t("Related Project Name"), paramName: "name", type: "text" }, | { label: t("Related Project Name"), paramName: "name", type: "text" }, | ||||
| { | |||||
| label: t("Cost (HKD)"), | |||||
| paramName: "cost", | |||||
| type: "text", | |||||
| }, | |||||
| { | { | ||||
| label: t("Expense Type"), | label: t("Expense Type"), | ||||
| paramName: "type", | paramName: "type", | ||||
| type: "select", | type: "select", | ||||
| options: ["Expense", "Petty Cash"], | |||||
| options: expenseTypeCombo, | |||||
| }, | }, | ||||
| { | { | ||||
| label: t("Status"), | label: t("Status"), | ||||
| paramName: "status", | paramName: "status", | ||||
| type: "select", | type: "select", | ||||
| options: [ | |||||
| "Not Submitted", | |||||
| "Waiting for Approval", | |||||
| "Approved", | |||||
| "Rejected", | |||||
| ], | |||||
| }, | |||||
| { | |||||
| label: t("Remarks"), | |||||
| paramName: "remarks", | |||||
| type: "text", | |||||
| options: claimStatusCombo, | |||||
| }, | }, | ||||
| ], | ], | ||||
| [t], | [t], | ||||
| ); | ); | ||||
| const onClaimClick = useCallback((claim: ClaimResult) => { | |||||
| const onClaimClick = useCallback((claim: Claim) => { | |||||
| console.log(claim); | console.log(claim); | ||||
| }, []); | }, []); | ||||
| const columns = useMemo<Column<ClaimResult>[]>( | |||||
| const columns = useMemo<Column<Claim>[]>( | |||||
| () => [ | () => [ | ||||
| // { | // { | ||||
| // name: "action", | // name: "action", | ||||
| @@ -69,9 +56,9 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||||
| // }, | // }, | ||||
| { name: "created", label: t("Creation Date") }, | { name: "created", label: t("Creation Date") }, | ||||
| { name: "name", label: t("Related Project Name") }, | { name: "name", label: t("Related Project Name") }, | ||||
| { name: "cost", label: t("Cost (HKD)") }, | |||||
| { name: "type", label: t("Expense Type") }, | |||||
| { name: "status", label: t("Status") }, | |||||
| { name: "cost", label: t("Amount (HKD)") }, | |||||
| { name: "type", label: t("Expense Type"), needTranslation: true }, | |||||
| { name: "status", label: t("Status"), needTranslation: true }, | |||||
| { name: "remarks", label: t("Remarks") }, | { name: "remarks", label: t("Remarks") }, | ||||
| ], | ], | ||||
| [t, onClaimClick], | [t, onClaimClick], | ||||
| @@ -82,10 +69,18 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| console.log(query); | |||||
| setFilteredClaims( | |||||
| claims.filter( | |||||
| (claim) => | |||||
| dateInRange(claim.created, query.created, query.createdTo ?? undefined) && | |||||
| claim.name.toLowerCase().includes(query.name.toLowerCase()) && | |||||
| (claim.type.toLowerCase().includes(query.type.toLowerCase()) || query.type.toLowerCase() === "all") && | |||||
| (claim.status.toLowerCase().includes(query.status.toLowerCase()) || query.status.toLowerCase() === "all") | |||||
| ), | |||||
| ); | |||||
| }} | }} | ||||
| /> | /> | ||||
| <SearchResults<ClaimResult> items={filteredClaims} columns={columns} /> | |||||
| <SearchResults<Claim> items={filteredClaims} columns={columns} /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -1,67 +0,0 @@ | |||||
| "use client"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import FormControl from "@mui/material/FormControl"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import InputLabel from "@mui/material/InputLabel"; | |||||
| import MenuItem from "@mui/material/MenuItem"; | |||||
| import Select from "@mui/material/Select"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import CardActions from "@mui/material/CardActions"; | |||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import ClaimInputGrid from "./ClaimInputGrid"; | |||||
| const ClaimDetails: React.FC = () => { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| {/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Related Project")} | |||||
| </Typography> */} | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{t("Related Project")}</InputLabel> | |||||
| <Select label={t("Project Category")}> | |||||
| <MenuItem value={"M1001"}>{t("M1001")}</MenuItem> | |||||
| <MenuItem value={"M1301"}>{t("M1301")}</MenuItem> | |||||
| <MenuItem value={"M1354"}>{t("M1354")}</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{t("Expense Type")}</InputLabel> | |||||
| <Select label={t("Team Lead")}> | |||||
| <MenuItem value={"Petty Cash"}>{"Petty Cash"}</MenuItem> | |||||
| <MenuItem value={"Expense"}>{"Expense"}</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| <Card> | |||||
| <ClaimInputGrid /> | |||||
| </Card> | |||||
| {/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button variant="text" startIcon={<RestartAlt />}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> */} | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default ClaimDetails; | |||||
| @@ -1,48 +0,0 @@ | |||||
| "use client"; | |||||
| import Check from "@mui/icons-material/Check"; | |||||
| import Close from "@mui/icons-material/Close"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Tab from "@mui/material/Tab"; | |||||
| import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import React, { useCallback, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import ClaimProjectDetails from "./ClaimDetails"; | |||||
| // import TaskSetup from "./TaskSetup"; | |||||
| // import StaffAllocation from "./StaffAllocation"; | |||||
| // import ResourceMilestone from "./ResourceMilestone"; | |||||
| const CreateProject: React.FC = () => { | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const { t } = useTranslation(); | |||||
| const router = useRouter(); | |||||
| const handleCancel = () => { | |||||
| router.back(); | |||||
| }; | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <ClaimProjectDetails /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button variant="contained" startIcon={<Check />}> | |||||
| {t("Confirm")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreateProject; | |||||
| @@ -1 +0,0 @@ | |||||
| export { default } from "./CreateClaim"; | |||||
| @@ -35,7 +35,7 @@ import { | |||||
| import { StaffResult } from "@/app/api/staff"; | import { StaffResult } from "@/app/api/staff"; | ||||
| import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
| import { Grade } from "@/app/api/grades"; | import { Grade } from "@/app/api/grades"; | ||||
| import { Customer } from "@/app/api/customer"; | |||||
| import { Customer, Subsidiary } from "@/app/api/customer"; | |||||
| export interface Props { | export interface Props { | ||||
| allTasks: Task[]; | allTasks: Task[]; | ||||
| @@ -43,6 +43,7 @@ export interface Props { | |||||
| taskTemplates: TaskTemplate[]; | taskTemplates: TaskTemplate[]; | ||||
| teamLeads: StaffResult[]; | teamLeads: StaffResult[]; | ||||
| allCustomers: Customer[]; | allCustomers: Customer[]; | ||||
| allSubsidiaries: Subsidiary[]; | |||||
| fundingTypes: FundingType[]; | fundingTypes: FundingType[]; | ||||
| serviceTypes: ServiceType[]; | serviceTypes: ServiceType[]; | ||||
| contractTypes: ContractType[]; | contractTypes: ContractType[]; | ||||
| @@ -50,8 +51,6 @@ export interface Props { | |||||
| buildingTypes: BuildingType[]; | buildingTypes: BuildingType[]; | ||||
| workNatures: WorkNature[]; | workNatures: WorkNature[]; | ||||
| allStaffs: StaffResult[]; | allStaffs: StaffResult[]; | ||||
| // Mocked | |||||
| grades: Grade[]; | grades: Grade[]; | ||||
| } | } | ||||
| @@ -76,6 +75,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
| teamLeads, | teamLeads, | ||||
| grades, | grades, | ||||
| allCustomers, | allCustomers, | ||||
| allSubsidiaries, | |||||
| contractTypes, | contractTypes, | ||||
| fundingTypes, | fundingTypes, | ||||
| locationTypes, | locationTypes, | ||||
| @@ -171,6 +171,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
| locationTypes={locationTypes} | locationTypes={locationTypes} | ||||
| serviceTypes={serviceTypes} | serviceTypes={serviceTypes} | ||||
| allCustomers={allCustomers} | allCustomers={allCustomers} | ||||
| allSubsidiaries={allSubsidiaries} | |||||
| projectCategories={projectCategories} | projectCategories={projectCategories} | ||||
| teamLeads={teamLeads} | teamLeads={teamLeads} | ||||
| isActive={tabIndex === 0} | isActive={tabIndex === 0} | ||||
| @@ -10,7 +10,8 @@ import { | |||||
| fetchProjectWorkNatures, | fetchProjectWorkNatures, | ||||
| } from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
| import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | ||||
| import { fetchAllCustomers } from "@/app/api/customer"; | |||||
| import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
| import { fetchGrades } from "@/app/api/grades"; | |||||
| const CreateProjectWrapper: React.FC = async () => { | const CreateProjectWrapper: React.FC = async () => { | ||||
| const [ | const [ | ||||
| @@ -19,6 +20,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||||
| projectCategories, | projectCategories, | ||||
| teamLeads, | teamLeads, | ||||
| allCustomers, | allCustomers, | ||||
| allSubsidiaries, | |||||
| contractTypes, | contractTypes, | ||||
| fundingTypes, | fundingTypes, | ||||
| locationTypes, | locationTypes, | ||||
| @@ -26,12 +28,14 @@ const CreateProjectWrapper: React.FC = async () => { | |||||
| buildingTypes, | buildingTypes, | ||||
| workNatures, | workNatures, | ||||
| allStaffs, | allStaffs, | ||||
| grades, | |||||
| ] = await Promise.all([ | ] = await Promise.all([ | ||||
| fetchAllTasks(), | fetchAllTasks(), | ||||
| fetchTaskTemplates(), | fetchTaskTemplates(), | ||||
| fetchProjectCategories(), | fetchProjectCategories(), | ||||
| fetchTeamLeads(), | fetchTeamLeads(), | ||||
| fetchAllCustomers(), | fetchAllCustomers(), | ||||
| fetchAllSubsidiaries(), | |||||
| fetchProjectContractTypes(), | fetchProjectContractTypes(), | ||||
| fetchProjectFundingTypes(), | fetchProjectFundingTypes(), | ||||
| fetchProjectLocationTypes(), | fetchProjectLocationTypes(), | ||||
| @@ -39,6 +43,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||||
| fetchProjectBuildingTypes(), | fetchProjectBuildingTypes(), | ||||
| fetchProjectWorkNatures(), | fetchProjectWorkNatures(), | ||||
| fetchStaff(), | fetchStaff(), | ||||
| fetchGrades(), | |||||
| ]); | ]); | ||||
| return ( | return ( | ||||
| @@ -47,6 +52,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||||
| projectCategories={projectCategories} | projectCategories={projectCategories} | ||||
| taskTemplates={taskTemplates} | taskTemplates={taskTemplates} | ||||
| teamLeads={teamLeads} | teamLeads={teamLeads} | ||||
| allSubsidiaries={allSubsidiaries} | |||||
| allCustomers={allCustomers} | allCustomers={allCustomers} | ||||
| contractTypes={contractTypes} | contractTypes={contractTypes} | ||||
| fundingTypes={fundingTypes} | fundingTypes={fundingTypes} | ||||
| @@ -55,14 +61,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||||
| buildingTypes={buildingTypes} | buildingTypes={buildingTypes} | ||||
| workNatures={workNatures} | workNatures={workNatures} | ||||
| allStaffs={allStaffs} | allStaffs={allStaffs} | ||||
| // Mocks | |||||
| grades={[ | |||||
| { name: "Grade 1", id: 1, code: "1" }, | |||||
| { name: "Grade 2", id: 2, code: "2" }, | |||||
| { name: "Grade 3", id: 3, code: "3" }, | |||||
| { name: "Grade 4", id: 4, code: "4" }, | |||||
| { name: "Grade 5", id: 5, code: "5" }, | |||||
| ]} | |||||
| grades={grades} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -43,7 +43,8 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
| t, | t, | ||||
| i18n: { language }, | i18n: { language }, | ||||
| } = useTranslation(); | } = useTranslation(); | ||||
| const { getValues, setValue } = useFormContext<CreateProjectInputs>(); | |||||
| const { getValues, setValue, formState } = | |||||
| useFormContext<CreateProjectInputs>(); | |||||
| const [payments, setPayments] = useState<PaymentRow[]>( | const [payments, setPayments] = useState<PaymentRow[]>( | ||||
| getValues("milestones")[taskGroupId]?.payments || [], | getValues("milestones")[taskGroupId]?.payments || [], | ||||
| ); | ); | ||||
| @@ -223,6 +224,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
| </Button> | </Button> | ||||
| ); | ); | ||||
| const startDate = getValues("milestones")[taskGroupId]?.startDate; | |||||
| const endDate = getValues("milestones")[taskGroupId]?.endDate; | |||||
| return ( | return ( | ||||
| <Stack gap={1}> | <Stack gap={1}> | ||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
| @@ -237,7 +241,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <DatePicker | <DatePicker | ||||
| label={t("Stage Start Date")} | label={t("Stage Start Date")} | ||||
| value={dayjs(getValues("milestones")[taskGroupId]?.startDate)} | |||||
| value={startDate ? dayjs(startDate) : null} | |||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (!date) return; | if (!date) return; | ||||
| const milestones = getValues("milestones"); | const milestones = getValues("milestones"); | ||||
| @@ -256,7 +260,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <DatePicker | <DatePicker | ||||
| label={t("Stage End Date")} | label={t("Stage End Date")} | ||||
| value={dayjs(getValues("milestones")[taskGroupId]?.endDate)} | |||||
| value={endDate ? dayjs(endDate) : null} | |||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (!date) return; | if (!date) return; | ||||
| const milestones = getValues("milestones"); | const milestones = getValues("milestones"); | ||||
| @@ -27,7 +27,7 @@ import { | |||||
| WorkNature, | WorkNature, | ||||
| } from "@/app/api/projects"; | } from "@/app/api/projects"; | ||||
| import { StaffResult } from "@/app/api/staff"; | import { StaffResult } from "@/app/api/staff"; | ||||
| import { Contact, Customer } from "@/app/api/customer"; | |||||
| import { Contact, Customer, Subsidiary } from "@/app/api/customer"; | |||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import React, { useEffect, useMemo, useState } from "react"; | import React, { useEffect, useMemo, useState } from "react"; | ||||
| import { fetchCustomer } from "@/app/api/customer/actions"; | import { fetchCustomer } from "@/app/api/customer/actions"; | ||||
| @@ -39,6 +39,7 @@ interface Props { | |||||
| projectCategories: ProjectCategory[]; | projectCategories: ProjectCategory[]; | ||||
| teamLeads: StaffResult[]; | teamLeads: StaffResult[]; | ||||
| allCustomers: Customer[]; | allCustomers: Customer[]; | ||||
| allSubsidiaries: Subsidiary[]; | |||||
| serviceTypes: ServiceType[]; | serviceTypes: ServiceType[]; | ||||
| contractTypes: ContractType[]; | contractTypes: ContractType[]; | ||||
| fundingTypes: FundingType[]; | fundingTypes: FundingType[]; | ||||
| @@ -52,6 +53,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| projectCategories, | projectCategories, | ||||
| teamLeads, | teamLeads, | ||||
| allCustomers, | allCustomers, | ||||
| allSubsidiaries, | |||||
| serviceTypes, | serviceTypes, | ||||
| contractTypes, | contractTypes, | ||||
| fundingTypes, | fundingTypes, | ||||
| @@ -69,6 +71,13 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| getValues, | getValues, | ||||
| } = useFormContext<CreateProjectInputs>(); | } = useFormContext<CreateProjectInputs>(); | ||||
| const subsidiaryMap = useMemo<{ | |||||
| [id: Subsidiary["id"]]: Subsidiary; | |||||
| }>( | |||||
| () => allSubsidiaries.reduce((acc, sub) => ({ ...acc, [sub.id]: sub }), {}), | |||||
| [allSubsidiaries], | |||||
| ); | |||||
| const selectedCustomerId = watch("clientId"); | const selectedCustomerId = watch("clientId"); | ||||
| const selectedCustomer = useMemo( | const selectedCustomer = useMemo( | ||||
| () => allCustomers.find((c) => c.id === selectedCustomerId), | () => allCustomers.find((c) => c.id === selectedCustomerId), | ||||
| @@ -482,14 +491,20 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| name="clientSubsidiaryId" | name="clientSubsidiaryId" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <Select label={t("Client Lead")} {...field}> | <Select label={t("Client Lead")} {...field}> | ||||
| {customerSubsidiaryIds.map((subsidiaryId, index) => ( | |||||
| <MenuItem | |||||
| key={`${subsidiaryId}-${index}`} | |||||
| value={subsidiaryId} | |||||
| > | |||||
| {subsidiaryId} | |||||
| </MenuItem> | |||||
| ))} | |||||
| {customerSubsidiaryIds | |||||
| .filter((subId) => subsidiaryMap[subId]) | |||||
| .map((subsidiaryId, index) => { | |||||
| const subsidiary = subsidiaryMap[subsidiaryId]; | |||||
| return ( | |||||
| <MenuItem | |||||
| key={`${subsidiaryId}-${index}`} | |||||
| value={subsidiaryId} | |||||
| > | |||||
| {`${subsidiary.code} - ${subsidiary.name}`} | |||||
| </MenuItem> | |||||
| ); | |||||
| })} | |||||
| </Select> | </Select> | ||||
| )} | )} | ||||
| /> | /> | ||||
| @@ -54,7 +54,7 @@ export interface Props { | |||||
| } | } | ||||
| const StaffAllocation: React.FC<Props> = ({ | const StaffAllocation: React.FC<Props> = ({ | ||||
| allStaffs: dataStaffs, | |||||
| allStaffs, | |||||
| allTasks, | allTasks, | ||||
| isActive, | isActive, | ||||
| defaultManhourBreakdownByGrade, | defaultManhourBreakdownByGrade, | ||||
| @@ -63,15 +63,6 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { setValue, getValues, watch } = useFormContext<CreateProjectInputs>(); | const { setValue, getValues, watch } = useFormContext<CreateProjectInputs>(); | ||||
| // TODO: remove this when grade and positions are done | |||||
| const allStaffs = useMemo<StaffResult[]>(() => { | |||||
| return dataStaffs.map((staff, index) => ({ | |||||
| ...staff, | |||||
| grade: grades[index % grades.length].name, | |||||
| currentPosition: `Mock Postion ${index}`, | |||||
| })); | |||||
| }, [dataStaffs, grades]); | |||||
| const [filteredStaff, setFilteredStaff] = React.useState( | const [filteredStaff, setFilteredStaff] = React.useState( | ||||
| allStaffs.sort(staffComparator), | allStaffs.sort(staffComparator), | ||||
| ); | ); | ||||
| @@ -0,0 +1,126 @@ | |||||
| "use client"; | |||||
| import { | |||||
| FieldErrors, | |||||
| FormProvider, | |||||
| SubmitErrorHandler, | |||||
| SubmitHandler, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import StaffAllocation from "./StaffAllocation"; | |||||
| import { StaffResult } from "@/app/api/staff"; | |||||
| import { CreateTeamInputs, saveTeam } from "@/app/api/team/actions"; | |||||
| import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||||
| import { Check, Close } from "@mui/icons-material"; | |||||
| import { useCallback, useState } from "react"; | |||||
| import { useRouter, useSearchParams } from "next/navigation"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { Error } from "@mui/icons-material"; | |||||
| import TeamInfo from "./TeamInfo"; | |||||
| export interface Props { | |||||
| allstaff: StaffResult[]; | |||||
| } | |||||
| const CreateTeam: React.FC<Props> = ({ allstaff }) => { | |||||
| const formProps = useForm<CreateTeamInputs>(); | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const router = useRouter(); | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const { t } = useTranslation(); | |||||
| const searchParams = useSearchParams() | |||||
| const errors = formProps.formState.errors; | |||||
| const onSubmit = useCallback<SubmitHandler<CreateTeamInputs>>( | |||||
| async (data) => { | |||||
| try { | |||||
| console.log(data); | |||||
| await saveTeam(data); | |||||
| router.replace("/settings/team"); | |||||
| } catch (e) { | |||||
| console.log(e); | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| }, | |||||
| [router] | |||||
| ); | |||||
| const handleCancel = () => { | |||||
| router.back(); | |||||
| }; | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const hasErrorsInTab = ( | |||||
| tabIndex: number, | |||||
| errors: FieldErrors<CreateTeamInputs>, | |||||
| ) => { | |||||
| switch (tabIndex) { | |||||
| case 0: | |||||
| return Object.keys(errors).length > 0; | |||||
| default: | |||||
| false; | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <> | |||||
| <FormProvider {...formProps}> | |||||
| <Stack | |||||
| spacing={2} | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||||
| > | |||||
| <Tabs | |||||
| value={tabIndex} | |||||
| onChange={handleTabChange} | |||||
| variant="scrollable" | |||||
| > | |||||
| <Tab | |||||
| label={t("Team Info")} | |||||
| icon={ | |||||
| hasErrorsInTab(0, errors) ? ( | |||||
| <Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
| ) : undefined | |||||
| } | |||||
| iconPosition="end" | |||||
| /> | |||||
| <Tab label={t("Subsidiary Allocation")} iconPosition="end" /> | |||||
| </Tabs> | |||||
| {serverError && ( | |||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
| {serverError} | |||||
| </Typography> | |||||
| )} | |||||
| {tabIndex === 0 && <TeamInfo/>} | |||||
| {tabIndex === 1 && <StaffAllocation allStaffs={allstaff} />} | |||||
| {/* <StaffAllocation allStaffs={allstaff} /> */} | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Close />} | |||||
| onClick={handleCancel} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| type="submit" | |||||
| // disabled={Boolean(formProps.watch("isGridEditing"))} | |||||
| > | |||||
| {t("Confirm")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreateTeam; | |||||
| @@ -0,0 +1,40 @@ | |||||
| 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 CreateTeamLoading: 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>CreateTeam | |||||
| <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 CreateTeamLoading; | |||||
| @@ -0,0 +1,26 @@ | |||||
| import React from "react"; | |||||
| import CreateTeam from "./CreateTeam"; | |||||
| import CreateTeamLoading from "./CreateTeamLoading"; | |||||
| // import { fetchTeam, fetchTeamLeads } from "@/app/api/team"; | |||||
| import { useSearchParams } from "next/navigation"; | |||||
| import { fetchStaffCombo } from "@/app/api/staff/actions"; | |||||
| import { fetchStaff } from "@/app/api/staff"; | |||||
| interface SubComponents { | |||||
| Loading: typeof CreateTeamLoading; | |||||
| } | |||||
| const CreateTeamWrapper: React.FC & SubComponents = async () => { | |||||
| const [ | |||||
| staff, | |||||
| ] = await Promise.all([ | |||||
| fetchStaff(), | |||||
| ]); | |||||
| return <CreateTeam allstaff={staff}/>; | |||||
| }; | |||||
| CreateTeamWrapper.Loading = CreateTeamLoading; | |||||
| export default CreateTeamWrapper; | |||||
| @@ -0,0 +1,238 @@ | |||||
| "use client"; | |||||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import CustomInputForm from "../CustomInputForm"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| FieldErrors, | |||||
| FormProvider, | |||||
| SubmitErrorHandler, | |||||
| SubmitHandler, | |||||
| useForm, | |||||
| useFormContext, | |||||
| } from "react-hook-form"; | |||||
| import CreateTeamForm from "../CreateTeamForm"; | |||||
| import { CreateTeamInputs } from "@/app/api/team/actions"; | |||||
| import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions"; | |||||
| import { StaffResult, StaffTeamTable } from "@/app/api/staff"; | |||||
| import SearchResults, { Column } from "../SearchResults"; | |||||
| import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; | |||||
| import { Card } from "reactstrap"; | |||||
| import { Box, CardContent, Grid, IconButton, InputAdornment, Stack, Tab, Tabs, TabsProps, TextField, Typography } from "@mui/material"; | |||||
| import { differenceBy } from "lodash"; | |||||
| import StarsIcon from '@mui/icons-material/Stars'; | |||||
| export interface Props { | |||||
| allStaffs: StaffResult[]; | |||||
| } | |||||
| const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| setValue, | |||||
| getValues, | |||||
| formState: { defaultValues }, | |||||
| reset, | |||||
| resetField, | |||||
| } = useFormContext<CreateTeamInputs>(); | |||||
| const initialStaffs = staff.map((s) => ({ ...s })); | |||||
| // console.log(initialStaffs) | |||||
| const [filteredStaff, setFilteredStaff] = useState(initialStaffs); | |||||
| const [selectedStaff, setSelectedStaff] = useState<typeof filteredStaff>( | |||||
| initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) | |||||
| ); | |||||
| const [seletedTeamLead, setSeletedTeamLead] = useState<number>() | |||||
| // Adding / Removing staff | |||||
| const addStaff = useCallback((staff: StaffResult) => { | |||||
| setSelectedStaff((s) => [...s, staff]); | |||||
| }, []); | |||||
| const removeStaff = useCallback((staff: StaffResult) => { | |||||
| setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); | |||||
| }, []); | |||||
| const setTeamLead = useCallback((staff: StaffResult) => { | |||||
| setSeletedTeamLead(staff.id) | |||||
| const rearrangedList = getValues("addStaffIds").reduce<number[]>((acc, num, index) => { | |||||
| if (num === staff.id && index !== 0) { | |||||
| acc.splice(index, 1); | |||||
| acc.unshift(num) | |||||
| } | |||||
| return acc; | |||||
| }, getValues("addStaffIds")); | |||||
| console.log(rearrangedList) | |||||
| console.log(selectedStaff) | |||||
| const rearrangedStaff = rearrangedList.map((id) => { | |||||
| return selectedStaff.find((staff) => staff.id === id); | |||||
| }); | |||||
| console.log(rearrangedStaff) | |||||
| setSelectedStaff(rearrangedStaff as StaffResult[]); | |||||
| setValue("addStaffIds", rearrangedList) | |||||
| }, [addStaff, selectedStaff]); | |||||
| const clearSubsidiary = useCallback(() => { | |||||
| if (defaultValues !== undefined) { | |||||
| resetField("addStaffIds"); | |||||
| setSelectedStaff( | |||||
| initialStaffs.filter((s) => defaultValues.addStaffIds?.includes(s.id)) | |||||
| ); | |||||
| } | |||||
| }, [defaultValues]); | |||||
| // Sync with form | |||||
| useEffect(() => { | |||||
| console.log(selectedStaff) | |||||
| setValue( | |||||
| "addStaffIds", | |||||
| selectedStaff.map((s) => s.id) | |||||
| ); | |||||
| }, [selectedStaff, setValue]); | |||||
| useEffect(() => { | |||||
| console.log(selectedStaff) | |||||
| }, [selectedStaff]); | |||||
| const StaffPoolColumns = useMemo<Column<StaffResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| label: t("Add"), | |||||
| name: "id", | |||||
| onClick: addStaff, | |||||
| buttonIcon: <PersonAdd />, | |||||
| }, | |||||
| { label: t("Staff Id"), name: "staffId" }, | |||||
| { label: t("Staff Name"), name: "name" }, | |||||
| { label: t("Current Position"), name: "currentPosition" }, | |||||
| ], | |||||
| [addStaff, t] | |||||
| ); | |||||
| const allocatedStaffColumns = useMemo<Column<StaffResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| label: t("Remove"), | |||||
| name: "action", | |||||
| onClick: removeStaff, | |||||
| buttonIcon: <PersonRemove />, | |||||
| }, | |||||
| { label: t("Staff Id"), name: "staffId" }, | |||||
| { label: t("Staff Name"), name: "name" }, | |||||
| { label: t("Current Position"), name: "currentPosition" }, | |||||
| { | |||||
| label: t("Team Lead"), | |||||
| name: "action", | |||||
| onClick: setTeamLead, | |||||
| buttonIcon: <StarsIcon />, | |||||
| }, | |||||
| ], | |||||
| [removeStaff, selectedStaff, t] | |||||
| ); | |||||
| const [query, setQuery] = React.useState(""); | |||||
| const onQueryInputChange = React.useCallback< | |||||
| React.ChangeEventHandler<HTMLInputElement> | |||||
| >((e) => { | |||||
| setQuery(e.target.value); | |||||
| }, []); | |||||
| const clearQueryInput = React.useCallback(() => { | |||||
| setQuery(""); | |||||
| }, []); | |||||
| React.useEffect(() => { | |||||
| // setFilteredStaff( | |||||
| // initialStaffs.filter((s) => { | |||||
| // const q = query.toLowerCase(); | |||||
| // // s.staffId.toLowerCase().includes(q) | |||||
| // // const q = query.toLowerCase(); | |||||
| // // return s.name.toLowerCase().includes(q); | |||||
| // // s.code.toString().includes(q) || | |||||
| // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) | |||||
| // }) | |||||
| // ); | |||||
| }, [staff, query]); | |||||
| const resetStaff = React.useCallback(() => { | |||||
| clearQueryInput(); | |||||
| clearSubsidiary(); | |||||
| }, [clearQueryInput, clearSubsidiary]); | |||||
| const formProps = useForm({ | |||||
| }); | |||||
| // Tab related | |||||
| const [tabIndex, setTabIndex] = React.useState(0); | |||||
| const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <FormProvider {...formProps}> | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent | |||||
| sx={{ display: "flex", flexDirection: "column", gap: 1 }} | |||||
| > | |||||
| <Stack gap={2}> | |||||
| <Typography variant="overline" display="block"> | |||||
| {t("staff")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6} display="flex" alignItems="center"> | |||||
| <Search sx={{ marginInlineEnd: 1 }} /> | |||||
| <TextField | |||||
| variant="standard" | |||||
| fullWidth | |||||
| onChange={onQueryInputChange} | |||||
| value={query} | |||||
| placeholder={t("Search by subsidiary code, name or br no.")} | |||||
| InputProps={{ | |||||
| endAdornment: query && ( | |||||
| <InputAdornment position="end"> | |||||
| <IconButton onClick={clearQueryInput}> | |||||
| <Clear /> | |||||
| </IconButton> | |||||
| </InputAdornment> | |||||
| ), | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| <Tabs value={tabIndex} onChange={handleTabChange}> | |||||
| <Tab label={t("Staff Pool")} /> | |||||
| <Tab | |||||
| label={`${t("Allocated Staff")} (${selectedStaff.length})`} | |||||
| /> | |||||
| </Tabs> | |||||
| <Box sx={{ marginInline: -3 }}> | |||||
| {tabIndex === 0 && ( | |||||
| <SearchResults | |||||
| noWrapper | |||||
| items={differenceBy(filteredStaff, selectedStaff, "id")} | |||||
| columns={StaffPoolColumns} | |||||
| /> | |||||
| )} | |||||
| {tabIndex === 1 && ( | |||||
| <SearchResults | |||||
| noWrapper | |||||
| items={selectedStaff} | |||||
| columns={allocatedStaffColumns} | |||||
| /> | |||||
| )} | |||||
| </Box> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </FormProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default StaffAllocation; | |||||
| @@ -0,0 +1,69 @@ | |||||
| "use client"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import CardActions from "@mui/material/CardActions"; | |||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | |||||
| import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; | |||||
| import { useCallback } from "react"; | |||||
| import { CreateTeamInputs } from "@/app/api/team/actions"; | |||||
| const TeamInfo: React.FC = ( | |||||
| { | |||||
| // customerTypes, | |||||
| } | |||||
| ) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors, defaultValues }, | |||||
| control, | |||||
| reset, | |||||
| resetField, | |||||
| setValue, | |||||
| } = useFormContext<CreateTeamInputs>(); | |||||
| const resetCustomer = useCallback(() => { | |||||
| console.log(defaultValues); | |||||
| if (defaultValues !== undefined) { | |||||
| resetField("description"); | |||||
| } | |||||
| }, [defaultValues]); | |||||
| return ( | |||||
| <> | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Team Info")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| label={t("Team Description")} | |||||
| fullWidth | |||||
| multiline | |||||
| rows={4} | |||||
| {...register("description", { | |||||
| required: true, | |||||
| })} | |||||
| error={Boolean(errors.description)} | |||||
| helperText={Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) : t("Please input correct description"))} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default TeamInfo; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./CreateTeamWrapper"; | |||||
| @@ -12,7 +12,7 @@ interface CustomDatagridProps { | |||||
| columnWidth?: number; | columnWidth?: number; | ||||
| Style?: boolean; | Style?: boolean; | ||||
| sx?: SxProps<Theme>; | sx?: SxProps<Theme>; | ||||
| dataGridHeight?: number; | |||||
| dataGridHeight?: number | string; | |||||
| [key: string]: any; | [key: string]: any; | ||||
| checkboxSelection?: boolean; | checkboxSelection?: boolean; | ||||
| onRowSelectionModelChange?: ( | onRowSelectionModelChange?: ( | ||||
| @@ -262,7 +262,7 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| {t("Contact Info")} | {t("Contact Info")} | ||||
| </Typography> | </Typography> | ||||
| {Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | {Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | ||||
| {t("Please ensure all the fields are inputted and saved")} | |||||
| {t("Please ensure at least one row is created, and all the fields are inputted and saved")} | |||||
| </Typography>} | </Typography>} | ||||
| {Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | {Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | ||||
| {t("Please ensure all the email formats are correct")} | {t("Please ensure all the email formats are correct")} | ||||
| @@ -2,7 +2,7 @@ | |||||
| // import CreateProject from "./CreateProject"; | // import CreateProject from "./CreateProject"; | ||||
| // import { fetchProjectCategories } from "@/app/api/projects"; | // import { fetchProjectCategories } from "@/app/api/projects"; | ||||
| // import { fetchTeamLeads } from "@/app/api/staff"; | // import { fetchTeamLeads } from "@/app/api/staff"; | ||||
| import { Subsidiary, fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
| import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
| import CustomerDetail from "./CustomerDetail"; | import CustomerDetail from "./CustomerDetail"; | ||||
| // type Props = { | // type Props = { | ||||
| @@ -68,7 +68,7 @@ const CustomerInfo: React.FC<Props> = ({ | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("Customer Code")} | |||||
| label={`${t("Customer Code")}*`} | |||||
| fullWidth | fullWidth | ||||
| {...register("code", { | {...register("code", { | ||||
| required: true, | required: true, | ||||
| @@ -79,7 +79,7 @@ const CustomerInfo: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("Customer Name")} | |||||
| label={`${t("Customer Name")}*`} | |||||
| fullWidth | fullWidth | ||||
| {...register("name", { | {...register("name", { | ||||
| required: true, | required: true, | ||||
| @@ -67,6 +67,7 @@ const CustomerSearch: React.FC<Props> = ({ customers }) => { | |||||
| label: t("Delete"), | label: t("Delete"), | ||||
| onClick: onDeleteClick, | onClick: onDeleteClick, | ||||
| buttonIcon: <DeleteIcon />, | buttonIcon: <DeleteIcon />, | ||||
| color: "error" | |||||
| }, | }, | ||||
| ], | ], | ||||
| [onTaskClick, t], | [onTaskClick, t], | ||||
| @@ -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 ProjectCompletionReportGen from "@/components/Report/ProjectCompletionReportGen"; | |||||
| const ProjectCompletionReport: React.FC = () => { | |||||
| return ( | |||||
| <Suspense fallback={<ProjectCompletionReportGen.Loading />}> | |||||
| <ProjectCompletionReportGen /> | |||||
| </Suspense> | |||||
| ); | |||||
| }; | |||||
| export default ProjectCompletionReport; | |||||
| @@ -0,0 +1,2 @@ | |||||
| //src\components\LateStartReport\index.ts | |||||
| export { default } from "./ProjectCompletionReport"; | |||||
| @@ -0,0 +1,44 @@ | |||||
| //src\components\LateStartReportGen\LateStartReportGen.tsx | |||||
| "use client"; | |||||
| import React, { useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../../ReportSearchBox5"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { ProjectCompletion } from "@/app/api/report5"; | |||||
| //import { DownloadReportButton } from './DownloadReportButton'; | |||||
| interface Props { | |||||
| projects: ProjectCompletion[]; | |||||
| } | |||||
| type SearchQuery = Partial<Omit<ProjectCompletion, "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: "Report Period From", | |||||
| label2: "Report Period 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 ProjectCompletionReportGenLoading: 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 ProjectCompletionReportGenLoading; | |||||
| @@ -0,0 +1,19 @@ | |||||
| //src\components\LateStartReportGen\LateStartReportGenWrapper.tsx | |||||
| import { fetchProjectsProjectCompletion } from "@/app/api/report5"; | |||||
| import React from "react"; | |||||
| import ProjectCompletionReportGen from "./ProjectCompletionReportGen"; | |||||
| import ProjectCompletionReportGenLoading from "./ProjectCompletionReportGenLoading"; | |||||
| interface SubComponents { | |||||
| Loading: typeof ProjectCompletionReportGenLoading; | |||||
| } | |||||
| const ProjectCompletionReportGenWrapper: React.FC & SubComponents = async () => { | |||||
| const clentprojects = await fetchProjectsProjectCompletion(); | |||||
| return <ProjectCompletionReportGen projects={clentprojects} />; | |||||
| }; | |||||
| ProjectCompletionReportGenWrapper.Loading = ProjectCompletionReportGenLoading; | |||||
| export default ProjectCompletionReportGenWrapper; | |||||
| @@ -0,0 +1,2 @@ | |||||
| //src\components\LateStartReportGen\index.ts | |||||
| export { default } from "./ProjectCompletionReportGenWrapper"; | |||||
| @@ -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 ProjectCompletionReportWOGen from "@/components/Report/ProjectCompletionReportWOGen"; | |||||
| const ProjectCompletionReportWO: React.FC = () => { | |||||
| return ( | |||||
| <Suspense fallback={<ProjectCompletionReportWOGen.Loading />}> | |||||
| <ProjectCompletionReportWOGen /> | |||||
| </Suspense> | |||||
| ); | |||||
| }; | |||||
| export default ProjectCompletionReportWO; | |||||
| @@ -0,0 +1,2 @@ | |||||
| //src\components\LateStartReport\index.ts | |||||
| export { default } from "./ProjectCompletionReportWO"; | |||||
| @@ -0,0 +1,44 @@ | |||||
| //src\components\LateStartReportGen\LateStartReportGen.tsx | |||||
| "use client"; | |||||
| import React, { useMemo, useState } from "react"; | |||||
| import SearchBox, { Criterion } from "../../ReportSearchBox6"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { ProjectCompletionWO } from "@/app/api/report6"; | |||||
| //import { DownloadReportButton } from './DownloadReportButton'; | |||||
| interface Props { | |||||
| projects: ProjectCompletionWO[]; | |||||
| } | |||||
| type SearchQuery = Partial<Omit<ProjectCompletionWO, "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: "Report Period From", | |||||
| label2: "Report Period 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 ProjectCompletionReportGenLoading: 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 ProjectCompletionReportGenLoading; | |||||
| @@ -0,0 +1,19 @@ | |||||
| //src\components\LateStartReportGen\LateStartReportGenWrapper.tsx | |||||
| import { fetchProjectsProjectCompletionWO } from "@/app/api/report6"; | |||||
| import React from "react"; | |||||
| import ProjectCompletionReportWOGen from "./ProjectCompletionReportWOGen"; | |||||
| import ProjectCompletionReportWOGenLoading from "./ProjectCompletionReportWOGenLoading"; | |||||
| interface SubComponents { | |||||
| Loading: typeof ProjectCompletionReportWOGenLoading; | |||||
| } | |||||
| const ProjectCompletionReportWOGenWrapper: React.FC & SubComponents = async () => { | |||||
| const clentprojects = await fetchProjectsProjectCompletionWO(); | |||||
| return <ProjectCompletionReportWOGen projects={clentprojects} />; | |||||
| }; | |||||
| ProjectCompletionReportWOGenWrapper.Loading = ProjectCompletionReportWOGenLoading; | |||||
| export default ProjectCompletionReportWOGenWrapper; | |||||
| @@ -0,0 +1,2 @@ | |||||
| //src\components\LateStartReportGen\index.ts | |||||
| export { default } from "./ProjectCompletionReportWOGenWrapper"; | |||||
| @@ -289,9 +289,9 @@ function SearchBox<T extends string>({ | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<Search />} | startIcon={<Search />} | ||||
| onClick={handleSearch} | |||||
| onClick={handleDownload} | |||||
| > | > | ||||
| {t("Search")} | |||||
| {t("Download")} | |||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -289,9 +289,9 @@ function SearchBox<T extends string>({ | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<Search />} | startIcon={<Search />} | ||||
| onClick={handleSearch} | |||||
| onClick={handleDownload} | |||||
| > | > | ||||
| {t("Search")} | |||||
| {t("Download")} | |||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -289,9 +289,9 @@ function SearchBox<T extends string>({ | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<Search />} | startIcon={<Search />} | ||||
| onClick={handleSearch} | |||||
| onClick={handleDownload} | |||||
| > | > | ||||
| {t("Search")} | |||||
| {t("Download")} | |||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -289,9 +289,9 @@ function SearchBox<T extends string>({ | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<Search />} | startIcon={<Search />} | ||||
| onClick={handleSearch} | |||||
| onClick={handleDownload} | |||||
| > | > | ||||
| {t("Search")} | |||||
| {t("Download")} | |||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -0,0 +1,302 @@ | |||||
| //src\components\ReportSearchBox\SearchBox.tsx | |||||
| "use client"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import React, { useCallback, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import FormControl from "@mui/material/FormControl"; | |||||
| import InputLabel from "@mui/material/InputLabel"; | |||||
| import Select, { SelectChangeEvent } from "@mui/material/Select"; | |||||
| import MenuItem from "@mui/material/MenuItem"; | |||||
| import CardActions from "@mui/material/CardActions"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
| import Search from "@mui/icons-material/Search"; | |||||
| import dayjs from "dayjs"; | |||||
| import "dayjs/locale/zh-hk"; | |||||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||||
| import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
| import { Box } from "@mui/material"; | |||||
| import * as XLSX from 'xlsx-js-style'; | |||||
| //import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; | |||||
| interface BaseCriterion<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); | |||||
| }; | |||||
| const handleDownload = async () => { | |||||
| //setIsLoading(true); | |||||
| try { | |||||
| const response = await fetch('/temp/AR05_Project Completion Report.xlsx', { | |||||
| headers: { | |||||
| 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||||
| }, | |||||
| }); | |||||
| if (!response.ok) throw new Error('Network response was not ok.'); | |||||
| const data = await response.blob(); | |||||
| const reader = new FileReader(); | |||||
| reader.onload = (e) => { | |||||
| if (e.target && e.target.result) { | |||||
| const ab = e.target.result as ArrayBuffer; | |||||
| const workbook = XLSX.read(ab, { type: 'array' }); | |||||
| const firstSheetName = workbook.SheetNames[0]; | |||||
| const worksheet = workbook.Sheets[firstSheetName]; | |||||
| // Add the current date to cell C2 | |||||
| const cellAddress = 'C2'; | |||||
| const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD | |||||
| const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD | |||||
| XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); | |||||
| // Calculate the maximum length of content in each column and set column width | |||||
| const colWidths: number[] = []; | |||||
| const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; | |||||
| jsonData.forEach((row: (string | number)[]) => { | |||||
| row.forEach((cell: string | number, index: number) => { | |||||
| const valueLength = cell.toString().length; | |||||
| colWidths[index] = Math.max(colWidths[index] || 0, valueLength); | |||||
| }); | |||||
| }); | |||||
| // Apply calculated widths to each column, skipping column A | |||||
| worksheet['!cols'] = colWidths.map((width, index) => { | |||||
| if (index === 0) { | |||||
| return { wch: 8 }; // Set default or specific width for column A if needed | |||||
| } | |||||
| return { wch: width + 2 }; // Add padding to width | |||||
| }); | |||||
| // Style for cell A1: Font size 16 and bold | |||||
| if (worksheet['A1']) { | |||||
| worksheet['A1'].s = { | |||||
| font: { | |||||
| bold: true, | |||||
| sz: 16, // Font size 16 | |||||
| //name: 'Times New Roman' // Specify font | |||||
| } | |||||
| }; | |||||
| } | |||||
| // Apply styles from A2 to A3 (bold) | |||||
| ['A2', 'A3'].forEach(cell => { | |||||
| if (worksheet[cell]) { | |||||
| worksheet[cell].s = { font: { bold: true } }; | |||||
| } | |||||
| }); | |||||
| // Formatting from A5 to F5 | |||||
| // Apply styles from A5 to F5 (bold, bottom border, center alignment) | |||||
| for (let col = 0; col < 6; col++) { // Columns A to F | |||||
| const cellRef = XLSX.utils.encode_col(col) + '5'; | |||||
| if (worksheet[cellRef]) { | |||||
| worksheet[cellRef].s = { | |||||
| font: { bold: true }, | |||||
| alignment: { horizontal: 'center' }, | |||||
| border: { | |||||
| bottom: { style: 'thin', color: { auto: 1 } } | |||||
| } | |||||
| }; | |||||
| } | |||||
| } | |||||
| // Format filename with date | |||||
| const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD | |||||
| const filename = `AR05_Project_Completion_Report_${today}.xlsx`; // Append formatted date to the filename | |||||
| // Convert workbook back to XLSX file | |||||
| XLSX.writeFile(workbook, filename); | |||||
| } else { | |||||
| throw new Error('Failed to load file'); | |||||
| } | |||||
| }; | |||||
| reader.readAsArrayBuffer(data); | |||||
| } catch (error) { | |||||
| console.error('Error downloading the file: ', error); | |||||
| } | |||||
| //setIsLoading(false); | |||||
| }; | |||||
| return ( | |||||
| <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)} | |||||
| value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null} | |||||
| /> | |||||
| </FormControl> | |||||
| <Box | |||||
| display="flex" | |||||
| alignItems="center" | |||||
| justifyContent="center" | |||||
| marginInline={2} | |||||
| > | |||||
| {"-"} | |||||
| </Box> | |||||
| <FormControl fullWidth> | |||||
| <DatePicker | |||||
| label={c.label2} | |||||
| onChange={makeDateToChangeHandler(c.paramName)} | |||||
| value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null} | |||||
| /> | |||||
| </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={handleDownload} | |||||
| > | |||||
| {t("Download")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| } | |||||
| export default SearchBox; | |||||
| @@ -0,0 +1,3 @@ | |||||
| //src\components\SearchBox\index.ts | |||||
| export { default } from "./SearchBox5"; | |||||
| export type { Criterion } from "./SearchBox5"; | |||||
| @@ -0,0 +1,302 @@ | |||||
| //src\components\ReportSearchBox\SearchBox.tsx | |||||
| "use client"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import React, { useCallback, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import FormControl from "@mui/material/FormControl"; | |||||
| import InputLabel from "@mui/material/InputLabel"; | |||||
| import Select, { SelectChangeEvent } from "@mui/material/Select"; | |||||
| import MenuItem from "@mui/material/MenuItem"; | |||||
| import CardActions from "@mui/material/CardActions"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
| import Search from "@mui/icons-material/Search"; | |||||
| import dayjs from "dayjs"; | |||||
| import "dayjs/locale/zh-hk"; | |||||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||||
| import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
| import { Box } from "@mui/material"; | |||||
| import * as XLSX from 'xlsx-js-style'; | |||||
| //import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; | |||||
| interface BaseCriterion<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); | |||||
| }; | |||||
| const handleDownload = async () => { | |||||
| //setIsLoading(true); | |||||
| try { | |||||
| const response = await fetch('/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx', { | |||||
| headers: { | |||||
| 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||||
| }, | |||||
| }); | |||||
| if (!response.ok) throw new Error('Network response was not ok.'); | |||||
| const data = await response.blob(); | |||||
| const reader = new FileReader(); | |||||
| reader.onload = (e) => { | |||||
| if (e.target && e.target.result) { | |||||
| const ab = e.target.result as ArrayBuffer; | |||||
| const workbook = XLSX.read(ab, { type: 'array' }); | |||||
| const firstSheetName = workbook.SheetNames[0]; | |||||
| const worksheet = workbook.Sheets[firstSheetName]; | |||||
| // Add the current date to cell C2 | |||||
| const cellAddress = 'C2'; | |||||
| const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD | |||||
| const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD | |||||
| XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); | |||||
| // Calculate the maximum length of content in each column and set column width | |||||
| const colWidths: number[] = []; | |||||
| const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; | |||||
| jsonData.forEach((row: (string | number)[]) => { | |||||
| row.forEach((cell: string | number, index: number) => { | |||||
| const valueLength = cell.toString().length; | |||||
| colWidths[index] = Math.max(colWidths[index] || 0, valueLength); | |||||
| }); | |||||
| }); | |||||
| // Apply calculated widths to each column, skipping column A | |||||
| worksheet['!cols'] = colWidths.map((width, index) => { | |||||
| if (index === 0) { | |||||
| return { wch: 8 }; // Set default or specific width for column A if needed | |||||
| } | |||||
| return { wch: width + 2 }; // Add padding to width | |||||
| }); | |||||
| // Style for cell A1: Font size 16 and bold | |||||
| if (worksheet['A1']) { | |||||
| worksheet['A1'].s = { | |||||
| font: { | |||||
| bold: true, | |||||
| sz: 16, // Font size 16 | |||||
| //name: 'Times New Roman' // Specify font | |||||
| } | |||||
| }; | |||||
| } | |||||
| // Apply styles from A2 to A3 (bold) | |||||
| ['A2', 'A3'].forEach(cell => { | |||||
| if (worksheet[cell]) { | |||||
| worksheet[cell].s = { font: { bold: true } }; | |||||
| } | |||||
| }); | |||||
| // Formatting from A5 to G5 | |||||
| // Apply styles from A5 to G5 (bold, bottom border, center alignment) | |||||
| for (let col = 0; col < 7; col++) { // Columns A to G | |||||
| const cellRef = XLSX.utils.encode_col(col) + '5'; | |||||
| if (worksheet[cellRef]) { | |||||
| worksheet[cellRef].s = { | |||||
| font: { bold: true }, | |||||
| alignment: { horizontal: 'center' }, | |||||
| border: { | |||||
| bottom: { style: 'thin', color: { auto: 1 } } | |||||
| } | |||||
| }; | |||||
| } | |||||
| } | |||||
| // Format filename with date | |||||
| const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD | |||||
| const filename = `AR06_Project_Completion_Report_with_Outstanding_Un-billed_Hours_${today}.xlsx`; // Append formatted date to the filename | |||||
| // Convert workbook back to XLSX file | |||||
| XLSX.writeFile(workbook, filename); | |||||
| } else { | |||||
| throw new Error('Failed to load file'); | |||||
| } | |||||
| }; | |||||
| reader.readAsArrayBuffer(data); | |||||
| } catch (error) { | |||||
| console.error('Error downloading the file: ', error); | |||||
| } | |||||
| //setIsLoading(false); | |||||
| }; | |||||
| return ( | |||||
| <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)} | |||||
| value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null} | |||||
| /> | |||||
| </FormControl> | |||||
| <Box | |||||
| display="flex" | |||||
| alignItems="center" | |||||
| justifyContent="center" | |||||
| marginInline={2} | |||||
| > | |||||
| {"-"} | |||||
| </Box> | |||||
| <FormControl fullWidth> | |||||
| <DatePicker | |||||
| label={c.label2} | |||||
| onChange={makeDateToChangeHandler(c.paramName)} | |||||
| value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null} | |||||
| /> | |||||
| </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={handleDownload} | |||||
| > | |||||
| {t("Download")} | |||||
| </Button> | |||||
| </CardActions> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| } | |||||
| export default SearchBox; | |||||
| @@ -0,0 +1,3 @@ | |||||
| //src\components\SearchBox\index.ts | |||||
| export { default } from "./SearchBox6"; | |||||
| export type { Criterion } from "./SearchBox6"; | |||||
| @@ -137,7 +137,7 @@ function SearchBox<T extends string>({ | |||||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | <MenuItem value={"All"}>{t("All")}</MenuItem> | ||||
| {c.options.map((option, index) => ( | {c.options.map((option, index) => ( | ||||
| <MenuItem key={`${option}-${index}`} value={option}> | <MenuItem key={`${option}-${index}`} value={option}> | ||||
| {option} | |||||
| {t(option)} | |||||
| </MenuItem> | </MenuItem> | ||||
| ))} | ))} | ||||
| </Select> | </Select> | ||||
| @@ -12,6 +12,8 @@ import TablePagination, { | |||||
| } from "@mui/material/TablePagination"; | } from "@mui/material/TablePagination"; | ||||
| import TableRow from "@mui/material/TableRow"; | import TableRow from "@mui/material/TableRow"; | ||||
| import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; | import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; | ||||
| import { t } from "i18next"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| export interface ResultWithId { | export interface ResultWithId { | ||||
| id: string | number; | id: string | number; | ||||
| @@ -21,6 +23,7 @@ interface BaseColumn<T extends ResultWithId> { | |||||
| name: keyof T; | name: keyof T; | ||||
| label: string; | label: string; | ||||
| color?: IconButtonOwnProps["color"]; | color?: IconButtonOwnProps["color"]; | ||||
| needTranslation?: boolean | |||||
| } | } | ||||
| interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | ||||
| @@ -51,6 +54,7 @@ function SearchResults<T extends ResultWithId>({ | |||||
| }: Props<T>) { | }: Props<T>) { | ||||
| const [page, setPage] = React.useState(0); | const [page, setPage] = React.useState(0); | ||||
| const [rowsPerPage, setRowsPerPage] = React.useState(10); | const [rowsPerPage, setRowsPerPage] = React.useState(10); | ||||
| const { t } = useTranslation() | |||||
| const handleChangePage: TablePaginationProps["onPageChange"] = ( | const handleChangePage: TablePaginationProps["onPageChange"] = ( | ||||
| _event, | _event, | ||||
| @@ -98,7 +102,7 @@ function SearchResults<T extends ResultWithId>({ | |||||
| {column.buttonIcon} | {column.buttonIcon} | ||||
| </IconButton> | </IconButton> | ||||
| ) : ( | ) : ( | ||||
| <>{item[columnName]}</> | |||||
| <>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</> | |||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| ); | ); | ||||
| @@ -263,7 +263,7 @@ const ContactInfo: React.FC<Props> = ({ | |||||
| {t("Contact Info")} | {t("Contact Info")} | ||||
| </Typography> | </Typography> | ||||
| {Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | {Boolean(errors.addContacts?.type === "required") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | ||||
| {t("Please ensure all the fields are inputted and saved")} | |||||
| {t("Please ensure at least one row is created, and all the fields are inputted and saved")} | |||||
| </Typography>} | </Typography>} | ||||
| {Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | {Boolean(errors.addContacts?.type === "email_format") && <Typography sx={(theme) => ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> | ||||
| {t("Please ensure all the email formats are correct")} | {t("Please ensure all the email formats are correct")} | ||||
| @@ -57,7 +57,7 @@ const SubsidiaryInfo: React.FC<Props> = ({ | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("Subsidiary Code")} | |||||
| label={`${t("Subsidiary Code")}*`} | |||||
| fullWidth | fullWidth | ||||
| {...register("code", { | {...register("code", { | ||||
| required: true, | required: true, | ||||
| @@ -68,7 +68,7 @@ const SubsidiaryInfo: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("Subsidiary Name")} | |||||
| label={`${t("Subsidiary Name")}*`} | |||||
| fullWidth | fullWidth | ||||
| {...register("name", { | {...register("name", { | ||||
| required: true, | required: true, | ||||
| @@ -67,6 +67,7 @@ const SubsidiarySearch: React.FC<Props> = ({ subsidiaries }) => { | |||||
| label: t("Delete"), | label: t("Delete"), | ||||
| onClick: onDeleteClick, | onClick: onDeleteClick, | ||||
| buttonIcon: <DeleteIcon />, | buttonIcon: <DeleteIcon />, | ||||
| color: "error" | |||||
| }, | }, | ||||
| ], | ], | ||||
| [onTaskClick, t], | [onTaskClick, t], | ||||
| @@ -0,0 +1,90 @@ | |||||
| "use client"; | |||||
| import { TeamResult } from "@/app/api/team"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import SearchResults, { Column } from "../SearchResults/index"; | |||||
| import EditNote from "@mui/icons-material/EditNote"; | |||||
| import DeleteIcon from '@mui/icons-material/Delete'; | |||||
| import { deleteStaff } from "@/app/api/staff/actions"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| interface Props { | |||||
| team: TeamResult[]; | |||||
| } | |||||
| type SearchQuery = Partial<Omit<TeamResult, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const TeamSearch: React.FC<Props> = ({ team }) => { | |||||
| const { t } = useTranslation(); | |||||
| const [filteredTeam, setFilteredTeam] = useState(team); | |||||
| const [data, setData] = useState<TeamResult>(); | |||||
| const [isOpen, setIsOpen] = useState(false); | |||||
| const router = useRouter(); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { | |||||
| label: t("Team Name"), | |||||
| paramName: "name", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Team Code"), | |||||
| paramName: "code", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Team Description"), | |||||
| paramName: "description", | |||||
| type: "text", | |||||
| }, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| const columns = useMemo<Column<TeamResult>[]>( | |||||
| () => [ | |||||
| // { | |||||
| // name: "action", | |||||
| // label: t("Actions"), | |||||
| // onClick: onStaffClick, | |||||
| // buttonIcon: <EditNote />, | |||||
| // }, | |||||
| { name: "name", label: t("Name") }, | |||||
| { name: "code", label: t("Code") }, | |||||
| { name: "description", label: t("description") }, | |||||
| // { | |||||
| // name: "action", | |||||
| // label: t("Actions"), | |||||
| // onClick: deleteClick, | |||||
| // buttonIcon: <DeleteIcon />, | |||||
| // }, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| // setFilteredStaff( | |||||
| // staff.filter( | |||||
| // (s) => | |||||
| // s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && | |||||
| // s.name.toLowerCase().includes(query.name.toLowerCase()) | |||||
| // // (query.team === "All" || s.team === query.team) && | |||||
| // // (query.category === "All" || s.category === query.category) && | |||||
| // // (query.team === "All" || s.team === query.team), | |||||
| // ) | |||||
| // ) | |||||
| }} | |||||
| /> | |||||
| <SearchResults<TeamResult> items={filteredTeam} columns={columns} /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default TeamSearch; | |||||
| @@ -0,0 +1,40 @@ | |||||
| 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 TeamSearchLoading: 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 TeamSearchLoading; | |||||
| @@ -0,0 +1,21 @@ | |||||
| // import { fetchTeam, fetchTeamLeads } from "@/app/api/Team"; | |||||
| import React from "react"; | |||||
| import TeamSearch from "./TeamSearch"; | |||||
| import TeamSearchLoading from "./TeamSearchLoading"; | |||||
| import { fetchTeam } from "@/app/api/team"; | |||||
| // import { preloadTeam } from "@/app/api/Team"; | |||||
| interface SubComponents { | |||||
| Loading: typeof TeamSearchLoading; | |||||
| } | |||||
| const TeamSearchWrapper: React.FC & SubComponents = async () => { | |||||
| const Team = await fetchTeam(); | |||||
| console.log(Team); | |||||
| return <TeamSearch team={Team} />; | |||||
| }; | |||||
| TeamSearchWrapper.Loading = TeamSearchLoading; | |||||
| export default TeamSearchWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./TeamSearchWrapper"; | |||||
| @@ -0,0 +1,31 @@ | |||||
| { | |||||
| "Staff Reimbursement": "Staff Reimbursement", | |||||
| "Create Claim": "Create Claim", | |||||
| "Creation Date": "Creation Date", | |||||
| "Creation Date From": "Creation Date From", | |||||
| "Creation Date To": "Creation Date To", | |||||
| "Related Project": "Related Project", | |||||
| "Related Project Name": "Related Project Name", | |||||
| "Expense Type": "Expense Type", | |||||
| "Status": "Status", | |||||
| "Amount (HKD)": "Amount (HKD)", | |||||
| "Remarks": "Remarks", | |||||
| "Invoice Date": "Invoice Date", | |||||
| "Supporting Document": "Supporting Document", | |||||
| "Total": "Total", | |||||
| "Add Record": "Add Record", | |||||
| "Project Name": "Project Name", | |||||
| "Project": "Project", | |||||
| "Claim Code": "Claim Code", | |||||
| "Petty Cash": "Petty Cash", | |||||
| "Expense": "Expense", | |||||
| "Not Submitted": "Not Submitted", | |||||
| "Waiting for Approval": "Waiting for Approval", | |||||
| "Approved": "Approved", | |||||
| "Rejected": "Rejected", | |||||
| "Description": "Description", | |||||
| "Actions": "Actions" | |||||
| } | |||||
| @@ -1,10 +1,22 @@ | |||||
| { | { | ||||
| "Grade {{grade}}": "Grade {{grade}}", | "Grade {{grade}}": "Grade {{grade}}", | ||||
| "All": "All", | |||||
| "Petty Cash": "Petty Cash", | |||||
| "Expense": "Expense", | |||||
| "Not Submitted": "Not Submitted", | |||||
| "Waiting for Approval": "Waiting for Approval", | |||||
| "Approved": "Approved", | |||||
| "Rejected": "Rejected", | |||||
| "Search": "Search", | "Search": "Search", | ||||
| "Search Criteria": "Search Criteria", | "Search Criteria": "Search Criteria", | ||||
| "Cancel": "Cancel", | "Cancel": "Cancel", | ||||
| "Confirm": "Confirm", | "Confirm": "Confirm", | ||||
| "Submit": "Submit", | "Submit": "Submit", | ||||
| "Save": "Save", | |||||
| "Save And Submit": "Save And Submit", | |||||
| "Reset": "Reset" | "Reset": "Reset" | ||||
| } | } | ||||
| @@ -43,7 +43,7 @@ | |||||
| "Contact Name": "Contact Name", | "Contact Name": "Contact Name", | ||||
| "Contact Email": "Contact Email", | "Contact Email": "Contact Email", | ||||
| "Contact Phone": "Contact Phone", | "Contact Phone": "Contact Phone", | ||||
| "Please ensure all the fields are inputted and saved": "Please ensure all the fields are inputted and saved", | |||||
| "Please ensure at least one row is created, and all the fields are inputted and saved": "Please ensure at least one row is created, and all the fields are inputted and saved", | |||||
| "Please ensure all the email formats are correct": "Please ensure all the email formats are correct", | "Please ensure all the email formats are correct": "Please ensure all the email formats are correct", | ||||
| "Do you want to submit?": "Do you want to submit?", | "Do you want to submit?": "Do you want to submit?", | ||||
| @@ -43,7 +43,7 @@ | |||||
| "Contact Name": "Contact Name", | "Contact Name": "Contact Name", | ||||
| "Contact Email": "Contact Email", | "Contact Email": "Contact Email", | ||||
| "Contact Phone": "Contact Phone", | "Contact Phone": "Contact Phone", | ||||
| "Please ensure all the fields are inputted and saved": "Please ensure all the fields are inputted and saved", | |||||
| "Please ensure at least one row is created, and all the fields are inputted and saved": "Please ensure at least one row is created, and all the fields are inputted and saved", | |||||
| "Please ensure all the email formats are correct": "Please ensure all the email formats are correct", | "Please ensure all the email formats are correct": "Please ensure all the email formats are correct", | ||||
| "Do you want to submit?": "Do you want to submit?", | "Do you want to submit?": "Do you want to submit?", | ||||
| @@ -0,0 +1,31 @@ | |||||
| { | |||||
| "Staff Reimbursement": "員工報銷", | |||||
| "Create Claim": "建立報銷", | |||||
| "Creation Date": "建立日期", | |||||
| "Creation Date From": "建立日期 (從)", | |||||
| "Creation Date To": "建立日期 (至)", | |||||
| "Related Project": "相關項目名稱", | |||||
| "Related Project Name": "相關項目名稱", | |||||
| "Expense Type": "費用類別", | |||||
| "Status": "狀態", | |||||
| "Amount (HKD)": "金額 (HKD)", | |||||
| "Remarks": "備註", | |||||
| "Invoice Date": "收據日期", | |||||
| "Supporting Document": "支援文件", | |||||
| "Total": "總金額", | |||||
| "Add Record": "新增記錄", | |||||
| "Project Name": "項目名稱", | |||||
| "Project": "項目", | |||||
| "Claim Code": "報銷編號", | |||||
| "Petty Cash": "小額開支", | |||||
| "Expense": "普通開支", | |||||
| "Not Submitted": "尚未提交", | |||||
| "Waiting for Approval": "等待批核", | |||||
| "Approved": "已批核", | |||||
| "Rejected": "已拒絕", | |||||
| "Description": "描述", | |||||
| "Actions": "行動" | |||||
| } | |||||
| @@ -1,8 +1,20 @@ | |||||
| { | { | ||||
| "All": "全部", | |||||
| "Petty Cash": "小額開支", | |||||
| "Expense": "普通開支", | |||||
| "Not Submitted": "尚未提交", | |||||
| "Waiting for Approval": "等待批核", | |||||
| "Approved": "已批核", | |||||
| "Rejected": "已拒絕", | |||||
| "Search": "搜尋", | "Search": "搜尋", | ||||
| "Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
| "Cancel": "取消", | "Cancel": "取消", | ||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| "Submit": "提交", | "Submit": "提交", | ||||
| "Save": "儲存", | |||||
| "Save And Submit": "儲存及提交", | |||||
| "Reset": "重置" | "Reset": "重置" | ||||
| } | } | ||||
| @@ -43,7 +43,7 @@ | |||||
| "Contact Name": "聯絡姓名", | "Contact Name": "聯絡姓名", | ||||
| "Contact Email": "聯絡電郵", | "Contact Email": "聯絡電郵", | ||||
| "Contact Phone": "聯絡電話", | "Contact Phone": "聯絡電話", | ||||
| "Please ensure all the fields are inputted and saved": "請確保所有欄位已輸入及儲存", | |||||
| "Please ensure at least one row is created, and all the fields are inputted and saved": "請確保已建立至少一行, 及已輸入和儲存所有欄位", | |||||
| "Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確", | "Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確", | ||||
| "Do you want to submit?": "你是否確認要提交?", | "Do you want to submit?": "你是否確認要提交?", | ||||
| @@ -43,7 +43,7 @@ | |||||
| "Contact Name": "聯絡姓名", | "Contact Name": "聯絡姓名", | ||||
| "Contact Email": "聯絡電郵", | "Contact Email": "聯絡電郵", | ||||
| "Contact Phone": "聯絡電話", | "Contact Phone": "聯絡電話", | ||||
| "Please ensure all the fields are inputted and saved": "請確保所有欄位已輸入及儲存", | |||||
| "Please ensure at least one row is created, and all the fields are inputted and saved": "請確保已建立至少一行, 及已輸入和儲存所有欄位", | |||||
| "Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確", | "Please ensure all the email formats are correct": "請確保所有電郵格式輸入正確", | ||||
| "Do you want to submit?": "你是否確認要提交?", | "Do you want to submit?": "你是否確認要提交?", | ||||