@@ -1,14 +1,14 @@ | |||
//src\app\(main)\analytics\LateStartReport\page.tsx | |||
//src\app\(main)\analytics\ProjectCompletionReport\page.tsx | |||
import { Metadata } from "next"; | |||
import { I18nProvider } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import LateStartReportComponent from "@/components/LateStartReport"; | |||
import ProjectCompletionReportComponent from "@/components/Report/ProjectCompletionReport"; | |||
export const metadata: Metadata = { | |||
title: "Project Status by Client", | |||
}; | |||
const ProjectLateReport: React.FC = () => { | |||
const ProjectCompletionReport: React.FC = () => { | |||
return ( | |||
<I18nProvider namespaces={["analytics"]}> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
@@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => { | |||
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||
<ProgressCashFlowSearch/> | |||
</Suspense> */} | |||
<LateStartReportComponent /> | |||
<ProjectCompletionReportComponent /> | |||
</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 { I18nProvider } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import LateStartReportComponent from "@/components/LateStartReport"; | |||
import ProjectCompletionReportWOComponent from "@/components/Report/ProjectCompletionReportWO"; | |||
export const metadata: Metadata = { | |||
title: "Project Status by Client", | |||
}; | |||
const ProjectLateReport: React.FC = () => { | |||
const ProjectCompletionReportWO: React.FC = () => { | |||
return ( | |||
<I18nProvider namespaces={["analytics"]}> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
@@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => { | |||
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}> | |||
<ProgressCashFlowSearch/> | |||
</Suspense> */} | |||
<LateStartReportComponent /> | |||
<ProjectCompletionReportWOComponent /> | |||
</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 { | |||
fetchProjectBuildingTypes, | |||
fetchProjectCategories, | |||
@@ -31,6 +33,9 @@ const Projects: React.FC = async () => { | |||
fetchProjectServiceTypes(); | |||
fetchProjectBuildingTypes(); | |||
fetchProjectWorkNatures(); | |||
fetchAllCustomers(); | |||
fetchAllSubsidiaries(); | |||
fetchGrades(); | |||
preloadTeamLeads(); | |||
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 { Metadata } from "next"; | |||
@@ -7,15 +7,17 @@ export const metadata: Metadata = { | |||
title: "Create Claim", | |||
}; | |||
const CreateClaims: React.FC = async () => { | |||
const { t } = await getServerI18n("claims"); | |||
const ClaimDetails: React.FC = async () => { | |||
const { t } = await getServerI18n("claim"); | |||
return ( | |||
<> | |||
<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 ClaimSearch from "@/components/ClaimSearch"; | |||
import { getServerI18n } from "@/i18n"; | |||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||
import Add from "@mui/icons-material/Add"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
@@ -14,7 +14,7 @@ export const metadata: Metadata = { | |||
}; | |||
const StaffReimbursement: React.FC = async () => { | |||
const { t } = await getServerI18n("claims"); | |||
const { t } = await getServerI18n("claim"); | |||
preloadClaims(); | |||
return ( | |||
@@ -37,9 +37,11 @@ const StaffReimbursement: React.FC = async () => { | |||
{t("Create Claim")} | |||
</Button> | |||
</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 "server-only"; | |||
export interface ClaimResult { | |||
export interface Claim { | |||
id: number; | |||
created: string; | |||
name: string; | |||
@@ -11,18 +13,52 @@ export interface ClaimResult { | |||
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 = () => { | |||
fetchClaims(); | |||
}; | |||
export const fetchClaims = cache(async () => { | |||
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, | |||
created: "2023-11-22", | |||
created: "2023/11/22", | |||
name: "Consultancy Project A", | |||
cost: 121.0, | |||
type: "Expense", | |||
@@ -31,7 +67,7 @@ const mockClaims: ClaimResult[] = [ | |||
}, | |||
{ | |||
id: 2, | |||
created: "2023-11-30", | |||
created: "2023/11/30", | |||
name: "Consultancy Project A", | |||
cost: 4300.0, | |||
type: "Expense", | |||
@@ -40,7 +76,7 @@ const mockClaims: ClaimResult[] = [ | |||
}, | |||
{ | |||
id: 3, | |||
created: "2023-12-12", | |||
created: "2023/12/12", | |||
name: "Construction Project C", | |||
cost: 3675.0, | |||
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 { | |||
name: string; | |||
id: number; | |||
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"; | |||
export interface PositionResult { | |||
id: number; | |||
code: string; | |||
name: string; | |||
description: string; | |||
id: number; | |||
code: string; | |||
name: string; | |||
description: string; | |||
} | |||
export const preloadPositions = () => { | |||
fetchPositions(); | |||
fetchPositions(); | |||
}; | |||
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", { | |||
minimumFractionDigits: 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 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", { | |||
weekday: "short", | |||
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 Stack from "@mui/material/Stack"; | |||
import Link from "next/link"; | |||
import { t } from "i18next"; | |||
import { | |||
Box, | |||
Container, | |||
Modal, | |||
Select, | |||
SelectChangeEvent, | |||
Typography, | |||
Box, Card, Typography, | |||
} from "@mui/material"; | |||
import { Close } from "@mui/icons-material"; | |||
import AddIcon from "@mui/icons-material/Add"; | |||
import EditIcon from "@mui/icons-material/Edit"; | |||
import DeleteIcon from "@mui/icons-material/DeleteOutlined"; | |||
@@ -25,35 +18,30 @@ import SaveIcon from "@mui/icons-material/Save"; | |||
import CancelIcon from "@mui/icons-material/Close"; | |||
import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined"; | |||
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 { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
import { | |||
GridRowsProp, | |||
GridRowModesModel, | |||
GridRowModes, | |||
DataGrid, | |||
GridColDef, | |||
GridToolbarContainer, | |||
GridFooterContainer, | |||
GridActionsCellItem, | |||
GridEventListener, | |||
GridRowId, | |||
GridRowModel, | |||
GridRowEditStopReasons, | |||
GridEditInputCell, | |||
GridValueSetterParams, | |||
GridTreeNodeWithRender, | |||
GridRenderCellParams, | |||
} from "@mui/x-data-grid"; | |||
import { LocalizationProvider } from "@mui/x-date-pickers"; | |||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
import dayjs from "dayjs"; | |||
import { Props } from "react-intl/src/components/relative"; | |||
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 { | |||
getCostTotal: () => number; | |||
@@ -63,15 +51,6 @@ interface BottomBarProps { | |||
) => void; | |||
} | |||
interface EditToolbarProps { | |||
// setDay: (newDay : dayjs.Dayjs) => void; | |||
setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; | |||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
setRowModesModel: ( | |||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
) => void; | |||
} | |||
interface EditFooterProps { | |||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
setRowModesModel: ( | |||
@@ -80,17 +59,17 @@ interface EditFooterProps { | |||
} | |||
const BottomBar = (props: BottomBarProps) => { | |||
const { t } = useTranslation("claim") | |||
const { setRows, setRowModesModel, getCostTotal } = props; | |||
// const getCostTotal = props.getCostTotal; | |||
const [newId, setNewId] = useState(-1); | |||
const [invalidDays, setInvalidDays] = useState(0); | |||
const handleAddClick = () => { | |||
const id = newId; | |||
setNewId(newId - 1); | |||
setRows((oldRows) => [ | |||
...oldRows, | |||
{ id, projectCode: "", task: "", isNew: true }, | |||
{ id, invoiceDate: new Date(), project: null, description: null, amount: null, newSupportingDocument: null, supportingDocumentName: null, isNew: true }, | |||
]); | |||
setRowModesModel((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 [invalid, setInvalid] = useState(false); | |||
@@ -122,7 +96,7 @@ const BottomBar = (props: BottomBarProps) => { | |||
<div> | |||
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||
<Box flex={1.5} textAlign={"right"} marginRight="4rem"> | |||
<b>Total:</b> | |||
<b>{t("Total")}:</b> | |||
</Box> | |||
<TotalCell value={getCostTotal()} /> | |||
</div> | |||
@@ -133,7 +107,7 @@ const BottomBar = (props: BottomBarProps) => { | |||
onClick={handleAddClick} | |||
sx={{ margin: "20px" }} | |||
> | |||
Add record | |||
{t("Add Record")} | |||
</Button> | |||
</div> | |||
); | |||
@@ -150,40 +124,51 @@ const EditFooter = (props: EditFooterProps) => { | |||
); | |||
}; | |||
interface ClaimInputGridProps { | |||
onClose?: () => void; | |||
interface ClaimFormInputGridProps { | |||
// onClose?: () => void; | |||
projectCombo: ProjectCombo[] | |||
} | |||
const initialRows: GridRowsProp = [ | |||
{ | |||
id: 1, | |||
date: new Date(), | |||
invoiceDate: new Date(), | |||
description: "Taxi to client office", | |||
cost: 169.5, | |||
document: "taxi_receipt.jpg", | |||
amount: 169.5, | |||
supportingDocumentName: "taxi_receipt.jpg", | |||
}, | |||
{ | |||
id: 2, | |||
date: dayjs().add(-14, "days").toDate(), | |||
invoiceDate: dayjs().add(-14, "days").toDate(), | |||
description: "MTR fee to Kowloon Bay Office", | |||
cost: 15.5, | |||
document: "octopus_invoice.jpg", | |||
amount: 15.5, | |||
supportingDocumentName: "octopus_invoice.jpg", | |||
}, | |||
{ | |||
id: 3, | |||
date: dayjs().add(-44, "days").toDate(), | |||
invoiceDate: dayjs().add(-44, "days").toDate(), | |||
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>( | |||
{}, | |||
); | |||
// Row function | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
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) => { | |||
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 = () => { | |||
let sum = 0; | |||
rows.forEach((row) => { | |||
sum += row["cost"] ?? 0; | |||
sum += row["amount"] ?? 0; | |||
}); | |||
return sum; | |||
}; | |||
@@ -256,11 +298,11 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||
), | |||
}; | |||
const columns: GridColDef[] = [ | |||
const columns: GridColDef[] = React.useMemo(() => [ | |||
{ | |||
field: "actions", | |||
type: "actions", | |||
headerName: "Actions", | |||
headerName: t("Actions"), | |||
width: 100, | |||
cellClassName: "actions", | |||
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, | |||
flex: 1, | |||
editable: true, | |||
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", | |||
headerName: "Description", | |||
headerName: t("Description"), | |||
// width: 220, | |||
flex: 2, | |||
editable: true, | |||
type: "string", | |||
}, | |||
{ | |||
field: "cost", | |||
headerName: "Cost (HKD)", | |||
field: "amount", | |||
headerName: t("Amount (HKD)"), | |||
editable: true, | |||
type: "number", | |||
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, | |||
flex: 2, | |||
renderCell: (params) => { | |||
return params.value ? ( | |||
<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} | |||
</a> | |||
</a> */} | |||
</span> | |||
) : ( | |||
<span style={{ color: palette.text.disabled }}>No Documents</span> | |||
); | |||
}, | |||
renderEditCell: (params) => { | |||
return params.value ? ( | |||
const currentRow = rows.find(row => row.id === params.row.id); | |||
return params.formattedValue ? ( | |||
<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 | |||
title="Remove Document" | |||
onClick={(event) => console.log(event)} | |||
onClick={() => handleFileDelete(params.row.id)} | |||
> | |||
<ImageNotSupportedOutlinedIcon | |||
sx={{ fontSize: "25px", color: "red" }} | |||
@@ -369,15 +440,24 @@ const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||
</Button> | |||
</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 ( | |||
<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 | |||
getCostTotal={getCostTotal} | |||
setRows={setRows} | |||
setRowModesModel={setRowModesModel} | |||
// sx={{flex:2}} | |||
// sx={{flex:2}} | |||
/> | |||
</Box> | |||
); | |||
}; | |||
export default ClaimInputGrid; | |||
export default ClaimFormInputGrid; |
@@ -0,0 +1 @@ | |||
export { default } from "./ClaimDetailWrapper"; |
@@ -1,65 +1,52 @@ | |||
"use client"; | |||
import { ClaimResult } from "@/app/api/claims"; | |||
import { Claim, ClaimSearchForm } from "@/app/api/claims"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import SearchBox, { Criterion } from "../SearchBox/index"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchResults, { Column } from "../SearchResults/index"; | |||
import EditNote from "@mui/icons-material/EditNote"; | |||
import { dateInRange } from "@/app/utils/commonUtil"; | |||
import { claimStatusCombo, expenseTypeCombo } from "@/app/utils/comboUtil"; | |||
interface Props { | |||
claims: ClaimResult[]; | |||
claims: Claim[]; | |||
} | |||
type SearchQuery = Partial<Omit<ClaimResult, "id">>; | |||
type SearchQuery = Partial<Omit<ClaimSearchForm, "id">>; | |||
type SearchParamNames = keyof SearchQuery; | |||
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. | |||
const [filteredClaims, setFilteredClaims] = useState(claims); | |||
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("Cost (HKD)"), | |||
paramName: "cost", | |||
type: "text", | |||
}, | |||
{ | |||
label: t("Expense Type"), | |||
paramName: "type", | |||
type: "select", | |||
options: ["Expense", "Petty Cash"], | |||
options: expenseTypeCombo, | |||
}, | |||
{ | |||
label: t("Status"), | |||
paramName: "status", | |||
type: "select", | |||
options: [ | |||
"Not Submitted", | |||
"Waiting for Approval", | |||
"Approved", | |||
"Rejected", | |||
], | |||
}, | |||
{ | |||
label: t("Remarks"), | |||
paramName: "remarks", | |||
type: "text", | |||
options: claimStatusCombo, | |||
}, | |||
], | |||
[t], | |||
); | |||
const onClaimClick = useCallback((claim: ClaimResult) => { | |||
const onClaimClick = useCallback((claim: Claim) => { | |||
console.log(claim); | |||
}, []); | |||
const columns = useMemo<Column<ClaimResult>[]>( | |||
const columns = useMemo<Column<Claim>[]>( | |||
() => [ | |||
// { | |||
// name: "action", | |||
@@ -69,9 +56,9 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||
// }, | |||
{ name: "created", label: t("Creation Date") }, | |||
{ 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") }, | |||
], | |||
[t, onClaimClick], | |||
@@ -82,10 +69,18 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||
<SearchBox | |||
criteria={searchCriteria} | |||
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 { Typography } from "@mui/material"; | |||
import { Grade } from "@/app/api/grades"; | |||
import { Customer } from "@/app/api/customer"; | |||
import { Customer, Subsidiary } from "@/app/api/customer"; | |||
export interface Props { | |||
allTasks: Task[]; | |||
@@ -43,6 +43,7 @@ export interface Props { | |||
taskTemplates: TaskTemplate[]; | |||
teamLeads: StaffResult[]; | |||
allCustomers: Customer[]; | |||
allSubsidiaries: Subsidiary[]; | |||
fundingTypes: FundingType[]; | |||
serviceTypes: ServiceType[]; | |||
contractTypes: ContractType[]; | |||
@@ -50,8 +51,6 @@ export interface Props { | |||
buildingTypes: BuildingType[]; | |||
workNatures: WorkNature[]; | |||
allStaffs: StaffResult[]; | |||
// Mocked | |||
grades: Grade[]; | |||
} | |||
@@ -76,6 +75,7 @@ const CreateProject: React.FC<Props> = ({ | |||
teamLeads, | |||
grades, | |||
allCustomers, | |||
allSubsidiaries, | |||
contractTypes, | |||
fundingTypes, | |||
locationTypes, | |||
@@ -171,6 +171,7 @@ const CreateProject: React.FC<Props> = ({ | |||
locationTypes={locationTypes} | |||
serviceTypes={serviceTypes} | |||
allCustomers={allCustomers} | |||
allSubsidiaries={allSubsidiaries} | |||
projectCategories={projectCategories} | |||
teamLeads={teamLeads} | |||
isActive={tabIndex === 0} | |||
@@ -10,7 +10,8 @@ import { | |||
fetchProjectWorkNatures, | |||
} from "@/app/api/projects"; | |||
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 [ | |||
@@ -19,6 +20,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||
projectCategories, | |||
teamLeads, | |||
allCustomers, | |||
allSubsidiaries, | |||
contractTypes, | |||
fundingTypes, | |||
locationTypes, | |||
@@ -26,12 +28,14 @@ const CreateProjectWrapper: React.FC = async () => { | |||
buildingTypes, | |||
workNatures, | |||
allStaffs, | |||
grades, | |||
] = await Promise.all([ | |||
fetchAllTasks(), | |||
fetchTaskTemplates(), | |||
fetchProjectCategories(), | |||
fetchTeamLeads(), | |||
fetchAllCustomers(), | |||
fetchAllSubsidiaries(), | |||
fetchProjectContractTypes(), | |||
fetchProjectFundingTypes(), | |||
fetchProjectLocationTypes(), | |||
@@ -39,6 +43,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||
fetchProjectBuildingTypes(), | |||
fetchProjectWorkNatures(), | |||
fetchStaff(), | |||
fetchGrades(), | |||
]); | |||
return ( | |||
@@ -47,6 +52,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||
projectCategories={projectCategories} | |||
taskTemplates={taskTemplates} | |||
teamLeads={teamLeads} | |||
allSubsidiaries={allSubsidiaries} | |||
allCustomers={allCustomers} | |||
contractTypes={contractTypes} | |||
fundingTypes={fundingTypes} | |||
@@ -55,14 +61,7 @@ const CreateProjectWrapper: React.FC = async () => { | |||
buildingTypes={buildingTypes} | |||
workNatures={workNatures} | |||
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, | |||
i18n: { language }, | |||
} = useTranslation(); | |||
const { getValues, setValue } = useFormContext<CreateProjectInputs>(); | |||
const { getValues, setValue, formState } = | |||
useFormContext<CreateProjectInputs>(); | |||
const [payments, setPayments] = useState<PaymentRow[]>( | |||
getValues("milestones")[taskGroupId]?.payments || [], | |||
); | |||
@@ -223,6 +224,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
</Button> | |||
); | |||
const startDate = getValues("milestones")[taskGroupId]?.startDate; | |||
const endDate = getValues("milestones")[taskGroupId]?.endDate; | |||
return ( | |||
<Stack gap={1}> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
@@ -237,7 +241,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
<FormControl fullWidth> | |||
<DatePicker | |||
label={t("Stage Start Date")} | |||
value={dayjs(getValues("milestones")[taskGroupId]?.startDate)} | |||
value={startDate ? dayjs(startDate) : null} | |||
onChange={(date) => { | |||
if (!date) return; | |||
const milestones = getValues("milestones"); | |||
@@ -256,7 +260,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
<FormControl fullWidth> | |||
<DatePicker | |||
label={t("Stage End Date")} | |||
value={dayjs(getValues("milestones")[taskGroupId]?.endDate)} | |||
value={endDate ? dayjs(endDate) : null} | |||
onChange={(date) => { | |||
if (!date) return; | |||
const milestones = getValues("milestones"); | |||
@@ -27,7 +27,7 @@ import { | |||
WorkNature, | |||
} from "@/app/api/projects"; | |||
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 React, { useEffect, useMemo, useState } from "react"; | |||
import { fetchCustomer } from "@/app/api/customer/actions"; | |||
@@ -39,6 +39,7 @@ interface Props { | |||
projectCategories: ProjectCategory[]; | |||
teamLeads: StaffResult[]; | |||
allCustomers: Customer[]; | |||
allSubsidiaries: Subsidiary[]; | |||
serviceTypes: ServiceType[]; | |||
contractTypes: ContractType[]; | |||
fundingTypes: FundingType[]; | |||
@@ -52,6 +53,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
projectCategories, | |||
teamLeads, | |||
allCustomers, | |||
allSubsidiaries, | |||
serviceTypes, | |||
contractTypes, | |||
fundingTypes, | |||
@@ -69,6 +71,13 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
getValues, | |||
} = useFormContext<CreateProjectInputs>(); | |||
const subsidiaryMap = useMemo<{ | |||
[id: Subsidiary["id"]]: Subsidiary; | |||
}>( | |||
() => allSubsidiaries.reduce((acc, sub) => ({ ...acc, [sub.id]: sub }), {}), | |||
[allSubsidiaries], | |||
); | |||
const selectedCustomerId = watch("clientId"); | |||
const selectedCustomer = useMemo( | |||
() => allCustomers.find((c) => c.id === selectedCustomerId), | |||
@@ -482,14 +491,20 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
name="clientSubsidiaryId" | |||
render={({ 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> | |||
)} | |||
/> | |||
@@ -54,7 +54,7 @@ export interface Props { | |||
} | |||
const StaffAllocation: React.FC<Props> = ({ | |||
allStaffs: dataStaffs, | |||
allStaffs, | |||
allTasks, | |||
isActive, | |||
defaultManhourBreakdownByGrade, | |||
@@ -63,15 +63,6 @@ const StaffAllocation: React.FC<Props> = ({ | |||
const { t } = useTranslation(); | |||
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( | |||
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; | |||
Style?: boolean; | |||
sx?: SxProps<Theme>; | |||
dataGridHeight?: number; | |||
dataGridHeight?: number | string; | |||
[key: string]: any; | |||
checkboxSelection?: boolean; | |||
onRowSelectionModelChange?: ( | |||
@@ -262,7 +262,7 @@ const ContactInfo: React.FC<Props> = ({ | |||
{t("Contact Info")} | |||
</Typography> | |||
{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>} | |||
{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")} | |||
@@ -2,7 +2,7 @@ | |||
// import CreateProject from "./CreateProject"; | |||
// import { fetchProjectCategories } from "@/app/api/projects"; | |||
// 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"; | |||
// type Props = { | |||
@@ -68,7 +68,7 @@ const CustomerInfo: React.FC<Props> = ({ | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Customer Code")} | |||
label={`${t("Customer Code")}*`} | |||
fullWidth | |||
{...register("code", { | |||
required: true, | |||
@@ -79,7 +79,7 @@ const CustomerInfo: React.FC<Props> = ({ | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Customer Name")} | |||
label={`${t("Customer Name")}*`} | |||
fullWidth | |||
{...register("name", { | |||
required: true, | |||
@@ -67,6 +67,7 @@ const CustomerSearch: React.FC<Props> = ({ customers }) => { | |||
label: t("Delete"), | |||
onClick: onDeleteClick, | |||
buttonIcon: <DeleteIcon />, | |||
color: "error" | |||
}, | |||
], | |||
[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 | |||
variant="outlined" | |||
startIcon={<Search />} | |||
onClick={handleSearch} | |||
onClick={handleDownload} | |||
> | |||
{t("Search")} | |||
{t("Download")} | |||
</Button> | |||
</CardActions> | |||
</CardContent> | |||
@@ -289,9 +289,9 @@ function SearchBox<T extends string>({ | |||
<Button | |||
variant="outlined" | |||
startIcon={<Search />} | |||
onClick={handleSearch} | |||
onClick={handleDownload} | |||
> | |||
{t("Search")} | |||
{t("Download")} | |||
</Button> | |||
</CardActions> | |||
</CardContent> | |||
@@ -289,9 +289,9 @@ function SearchBox<T extends string>({ | |||
<Button | |||
variant="outlined" | |||
startIcon={<Search />} | |||
onClick={handleSearch} | |||
onClick={handleDownload} | |||
> | |||
{t("Search")} | |||
{t("Download")} | |||
</Button> | |||
</CardActions> | |||
</CardContent> | |||
@@ -289,9 +289,9 @@ function SearchBox<T extends string>({ | |||
<Button | |||
variant="outlined" | |||
startIcon={<Search />} | |||
onClick={handleSearch} | |||
onClick={handleDownload} | |||
> | |||
{t("Search")} | |||
{t("Download")} | |||
</Button> | |||
</CardActions> | |||
</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> | |||
{c.options.map((option, index) => ( | |||
<MenuItem key={`${option}-${index}`} value={option}> | |||
{option} | |||
{t(option)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
@@ -12,6 +12,8 @@ import TablePagination, { | |||
} from "@mui/material/TablePagination"; | |||
import TableRow from "@mui/material/TableRow"; | |||
import IconButton, { IconButtonOwnProps, IconButtonPropsColorOverrides } from "@mui/material/IconButton"; | |||
import { t } from "i18next"; | |||
import { useTranslation } from "react-i18next"; | |||
export interface ResultWithId { | |||
id: string | number; | |||
@@ -21,6 +23,7 @@ interface BaseColumn<T extends ResultWithId> { | |||
name: keyof T; | |||
label: string; | |||
color?: IconButtonOwnProps["color"]; | |||
needTranslation?: boolean | |||
} | |||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||
@@ -51,6 +54,7 @@ function SearchResults<T extends ResultWithId>({ | |||
}: Props<T>) { | |||
const [page, setPage] = React.useState(0); | |||
const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||
const { t } = useTranslation() | |||
const handleChangePage: TablePaginationProps["onPageChange"] = ( | |||
_event, | |||
@@ -98,7 +102,7 @@ function SearchResults<T extends ResultWithId>({ | |||
{column.buttonIcon} | |||
</IconButton> | |||
) : ( | |||
<>{item[columnName]}</> | |||
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</> | |||
)} | |||
</TableCell> | |||
); | |||
@@ -263,7 +263,7 @@ const ContactInfo: React.FC<Props> = ({ | |||
{t("Contact Info")} | |||
</Typography> | |||
{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>} | |||
{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")} | |||
@@ -57,7 +57,7 @@ const SubsidiaryInfo: React.FC<Props> = ({ | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Subsidiary Code")} | |||
label={`${t("Subsidiary Code")}*`} | |||
fullWidth | |||
{...register("code", { | |||
required: true, | |||
@@ -68,7 +68,7 @@ const SubsidiaryInfo: React.FC<Props> = ({ | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Subsidiary Name")} | |||
label={`${t("Subsidiary Name")}*`} | |||
fullWidth | |||
{...register("name", { | |||
required: true, | |||
@@ -67,6 +67,7 @@ const SubsidiarySearch: React.FC<Props> = ({ subsidiaries }) => { | |||
label: t("Delete"), | |||
onClick: onDeleteClick, | |||
buttonIcon: <DeleteIcon />, | |||
color: "error" | |||
}, | |||
], | |||
[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}}", | |||
"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 Criteria": "Search Criteria", | |||
"Cancel": "Cancel", | |||
"Confirm": "Confirm", | |||
"Submit": "Submit", | |||
"Save": "Save", | |||
"Save And Submit": "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 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", | |||
"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 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", | |||
"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 Criteria": "搜尋條件", | |||
"Cancel": "取消", | |||
"Confirm": "確認", | |||
"Submit": "提交", | |||
"Save": "儲存", | |||
"Save And Submit": "儲存及提交", | |||
"Reset": "重置" | |||
} |
@@ -43,7 +43,7 @@ | |||
"Contact Name": "聯絡姓名", | |||
"Contact Email": "聯絡電郵", | |||
"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": "請確保所有電郵格式輸入正確", | |||
"Do you want to submit?": "你是否確認要提交?", | |||
@@ -43,7 +43,7 @@ | |||
"Contact Name": "聯絡姓名", | |||
"Contact Email": "聯絡電郵", | |||
"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": "請確保所有電郵格式輸入正確", | |||
"Do you want to submit?": "你是否確認要提交?", | |||