From cef15cef561dc1cb36b12921cd0e2395da57754f Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Wed, 24 Apr 2024 14:33:33 +0800 Subject: [PATCH 1/9] add report ar05 --- .../temp/AR05_Project Completion Report.xlsx | Bin 0 -> 12445 bytes .../ProjectCompletionReport/page.tsx | 10 +- src/app/api/report5/index.ts | 42 +++ .../ProjectCompletionReport.tsx | 17 + .../Report/ProjectCompletionReport/index.ts | 2 + .../ProjectCompletionReportGen.tsx | 44 +++ .../ProjectCompletionReportGenLoading.tsx | 41 +++ .../ProjectCompletionReportGenWrapper.tsx | 19 ++ .../ProjectCompletionReportGen/index.ts | 2 + .../ReportSearchBox5/SearchBox5.tsx | 302 ++++++++++++++++++ src/components/ReportSearchBox5/index.ts | 3 + 11 files changed, 477 insertions(+), 5 deletions(-) create mode 100644 public/temp/AR05_Project Completion Report.xlsx create mode 100644 src/app/api/report5/index.ts create mode 100644 src/components/Report/ProjectCompletionReport/ProjectCompletionReport.tsx create mode 100644 src/components/Report/ProjectCompletionReport/index.ts create mode 100644 src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGen.tsx create mode 100644 src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenLoading.tsx create mode 100644 src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenWrapper.tsx create mode 100644 src/components/Report/ProjectCompletionReportGen/index.ts create mode 100644 src/components/ReportSearchBox5/SearchBox5.tsx create mode 100644 src/components/ReportSearchBox5/index.ts diff --git a/public/temp/AR05_Project Completion Report.xlsx b/public/temp/AR05_Project Completion Report.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..60deae81b83a6bf0bc05771a2318ceaa9f4a6f0b GIT binary patch literal 12445 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 { +const ProjectCompletionReport: React.FC = () => { return ( @@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => { {/* }> */} - + ); }; -export default ProjectLateReport; +export default ProjectCompletionReport; diff --git a/src/app/api/report5/index.ts b/src/app/api/report5/index.ts new file mode 100644 index 0000000..f0ed563 --- /dev/null +++ b/src/app/api/report5/index.ts @@ -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", + }, +]; diff --git a/src/components/Report/ProjectCompletionReport/ProjectCompletionReport.tsx b/src/components/Report/ProjectCompletionReport/ProjectCompletionReport.tsx new file mode 100644 index 0000000..dbd2e2d --- /dev/null +++ b/src/components/Report/ProjectCompletionReport/ProjectCompletionReport.tsx @@ -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 ( + }> + + + ); +}; + +export default ProjectCompletionReport; \ No newline at end of file diff --git a/src/components/Report/ProjectCompletionReport/index.ts b/src/components/Report/ProjectCompletionReport/index.ts new file mode 100644 index 0000000..a5233e6 --- /dev/null +++ b/src/components/Report/ProjectCompletionReport/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReport\index.ts +export { default } from "./ProjectCompletionReport"; diff --git a/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGen.tsx b/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGen.tsx new file mode 100644 index 0000000..75b12ac --- /dev/null +++ b/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGen.tsx @@ -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>; +type SearchParamNames = keyof SearchQuery; + +const ProgressByClientSearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + + const searchCriteria: Criterion[] = 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 ( + <> + { + console.log(query); + }} + /> + {/* */} + + ); +}; + +export default ProgressByClientSearch; diff --git a/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenLoading.tsx b/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenLoading.tsx new file mode 100644 index 0000000..466b74d --- /dev/null +++ b/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenLoading.tsx @@ -0,0 +1,41 @@ +//src\components\LateStartReportGen\LateStartReportGenLoading.tsx +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const ProjectCompletionReportGenLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectCompletionReportGenLoading; diff --git a/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenWrapper.tsx b/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenWrapper.tsx new file mode 100644 index 0000000..02f9624 --- /dev/null +++ b/src/components/Report/ProjectCompletionReportGen/ProjectCompletionReportGenWrapper.tsx @@ -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 ; +}; + +ProjectCompletionReportGenWrapper.Loading = ProjectCompletionReportGenLoading; + +export default ProjectCompletionReportGenWrapper; \ No newline at end of file diff --git a/src/components/Report/ProjectCompletionReportGen/index.ts b/src/components/Report/ProjectCompletionReportGen/index.ts new file mode 100644 index 0000000..269443a --- /dev/null +++ b/src/components/Report/ProjectCompletionReportGen/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReportGen\index.ts +export { default } from "./ProjectCompletionReportGenWrapper"; diff --git a/src/components/ReportSearchBox5/SearchBox5.tsx b/src/components/ReportSearchBox5/SearchBox5.tsx new file mode 100644 index 0000000..b252103 --- /dev/null +++ b/src/components/ReportSearchBox5/SearchBox5.tsx @@ -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 { + label: string; + label2?: string; + paramName: T; + paramName2?: T; +} + +interface TextCriterion extends BaseCriterion { + type: "text"; +} + +interface SelectCriterion extends BaseCriterion { + type: "select"; + options: string[]; +} + +interface DateRangeCriterion extends BaseCriterion { + type: "dateRange"; +} + +export type Criterion = + | TextCriterion + | SelectCriterion + | DateRangeCriterion; + +interface Props { + criteria: Criterion[]; + onSearch: (inputs: Record) => void; + onReset?: () => void; +} + +function SearchBox({ + criteria, + onSearch, + onReset, +}: Props) { + const { t } = useTranslation("common"); + const defaultInputs = useMemo( + () => + criteria.reduce>( + (acc, c) => { + return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + }, + {} as Record, + ), + [criteria], + ); + const [inputs, setInputs] = useState(defaultInputs); + + const makeInputChangeHandler = useCallback( + (paramName: T): React.ChangeEventHandler => { + return (e) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, + [], + ); + + const makeSelectChangeHandler = useCallback((paramName: T) => { + return (e: SelectChangeEvent) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, []); + + const makeDateChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + }; + }, []); + + const makeDateToChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), + })); + }; + }, []); + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/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 ( + + + {t("Search Criteria")} + + {criteria.map((c) => { + return ( + + {c.type === "text" && ( + + )} + {c.type === "select" && ( + + {c.label} + + + )} + {c.type === "dateRange" && ( + + + + + + + {"-"} + + + + + + + )} + + ); + })} + + + + + + + + ); +} + +export default SearchBox; diff --git a/src/components/ReportSearchBox5/index.ts b/src/components/ReportSearchBox5/index.ts new file mode 100644 index 0000000..493ae58 --- /dev/null +++ b/src/components/ReportSearchBox5/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBox5"; +export type { Criterion } from "./SearchBox5"; From 494ac814b864d7ee0b704f7f768f57529a70febd Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Wed, 24 Apr 2024 16:12:09 +0800 Subject: [PATCH 2/9] update team master --- src/app/(main)/settings/team/create/page.tsx | 45 ++++ src/app/(main)/settings/team/page.tsx | 53 ++++ src/app/api/team/index.ts | 20 ++ src/components/CreateTeam/CreateTeam.tsx | 126 ++++++++++ .../CreateTeam/CreateTeamLoading.tsx | 40 +++ .../CreateTeam/CreateTeamWrapper.tsx | 26 ++ src/components/CreateTeam/StaffAllocation.tsx | 233 ++++++++++++++++++ src/components/CreateTeam/TeamInfo.tsx | 69 ++++++ src/components/CreateTeam/index.ts | 1 + src/components/TeamSearch/TeamSearch.tsx | 90 +++++++ .../TeamSearch/TeamSearchLoading.tsx | 40 +++ .../TeamSearch/TeamSearchWrapper.tsx | 21 ++ src/components/TeamSearch/index.ts | 1 + 13 files changed, 765 insertions(+) create mode 100644 src/app/(main)/settings/team/create/page.tsx create mode 100644 src/app/(main)/settings/team/page.tsx create mode 100644 src/app/api/team/index.ts create mode 100644 src/components/CreateTeam/CreateTeam.tsx create mode 100644 src/components/CreateTeam/CreateTeamLoading.tsx create mode 100644 src/components/CreateTeam/CreateTeamWrapper.tsx create mode 100644 src/components/CreateTeam/StaffAllocation.tsx create mode 100644 src/components/CreateTeam/TeamInfo.tsx create mode 100644 src/components/CreateTeam/index.ts create mode 100644 src/components/TeamSearch/TeamSearch.tsx create mode 100644 src/components/TeamSearch/TeamSearchLoading.tsx create mode 100644 src/components/TeamSearch/TeamSearchWrapper.tsx create mode 100644 src/components/TeamSearch/index.ts diff --git a/src/app/(main)/settings/team/create/page.tsx b/src/app/(main)/settings/team/create/page.tsx new file mode 100644 index 0000000..721fda7 --- /dev/null +++ b/src/app/(main)/settings/team/create/page.tsx @@ -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 ( + <> + {t("Create Team")} + + + + + ); +}; + +export default CreateTeamPage; diff --git a/src/app/(main)/settings/team/page.tsx b/src/app/(main)/settings/team/page.tsx new file mode 100644 index 0000000..5e78fb3 --- /dev/null +++ b/src/app/(main)/settings/team/page.tsx @@ -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 ( + <> + + + {t("Team")} + + + + + }> + + + + + ); + }; + + export default Team; \ No newline at end of file diff --git a/src/app/api/team/index.ts b/src/app/api/team/index.ts new file mode 100644 index 0000000..81799d7 --- /dev/null +++ b/src/app/api/team/index.ts @@ -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(`${BASE_API_URL}/team`, { + next: { tags: ["team"] }, + }); + }); \ No newline at end of file diff --git a/src/components/CreateTeam/CreateTeam.tsx b/src/components/CreateTeam/CreateTeam.tsx new file mode 100644 index 0000000..93b585e --- /dev/null +++ b/src/components/CreateTeam/CreateTeam.tsx @@ -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 = ({ allstaff }) => { + const formProps = useForm(); + 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>( + 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>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], +); +const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors, +) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } +}; + return ( + <> + + + + + ) : undefined + } + iconPosition="end" + /> + + + {serverError && ( + + {serverError} + + )} + {tabIndex === 0 && } + {tabIndex === 1 && } + + {/* */} + + + + + + + + ); +}; + +export default CreateTeam; diff --git a/src/components/CreateTeam/CreateTeamLoading.tsx b/src/components/CreateTeam/CreateTeamLoading.tsx new file mode 100644 index 0000000..48c009d --- /dev/null +++ b/src/components/CreateTeam/CreateTeamLoading.tsx @@ -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 ( + <> + + + + + + + + + + + CreateTeam + + + + + + + + + + + ); +}; + +export default CreateTeamLoading; diff --git a/src/components/CreateTeam/CreateTeamWrapper.tsx b/src/components/CreateTeam/CreateTeamWrapper.tsx new file mode 100644 index 0000000..18b41a5 --- /dev/null +++ b/src/components/CreateTeam/CreateTeamWrapper.tsx @@ -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 ; +}; + +CreateTeamWrapper.Loading = CreateTeamLoading; + +export default CreateTeamWrapper; diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx new file mode 100644 index 0000000..f85c89b --- /dev/null +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -0,0 +1,233 @@ +"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 = ({ allStaffs: staff }) => { + const { t } = useTranslation(); + const { + setValue, + getValues, + formState: { defaultValues }, + reset, + resetField, + } = useFormContext(); + + const initialStaffs = staff.map((s) => ({ ...s })); +// console.log(initialStaffs) + const [filteredStaff, setFilteredStaff] = useState(initialStaffs); + const [selectedStaff, setSelectedStaff] = useState( + initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) + ); + const [seletedTeamLead, setSeletedTeamLead] = useState() + // 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((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) + }, []); + + 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]); + + const StaffPoolColumns = useMemo[]>( + () => [ + { + label: t("Add"), + name: "id", + onClick: addStaff, + buttonIcon: , + }, + { label: t("Staff Id"), name: "staffId" }, + { label: t("Staff Name"), name: "name" }, + { label: t("Current Position"), name: "currentPosition" }, + ], + [addStaff, t] + ); + + const allocatedStaffColumns = useMemo[]>( + () => [ + { + label: t("Remove"), + name: "action", + onClick: removeStaff, + buttonIcon: , + }, + { 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: , + }, + ], + [removeStaff, t] + ); + + const [query, setQuery] = React.useState(""); + const onQueryInputChange = React.useCallback< + React.ChangeEventHandler + >((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>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], + ); + + return ( + <> + + + + + + {t("staff")} + + + + + + + + + + ), + }} + /> + + + + + + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + + + + + + + ); +}; + +export default StaffAllocation; diff --git a/src/components/CreateTeam/TeamInfo.tsx b/src/components/CreateTeam/TeamInfo.tsx new file mode 100644 index 0000000..9dd4060 --- /dev/null +++ b/src/components/CreateTeam/TeamInfo.tsx @@ -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(); + + const resetCustomer = useCallback(() => { + console.log(defaultValues); + if (defaultValues !== undefined) { + resetField("description"); + } + }, [defaultValues]); + + return ( + <> + + + + + {t("Team Info")} + + + + + + + + + + + ); +}; +export default TeamInfo; diff --git a/src/components/CreateTeam/index.ts b/src/components/CreateTeam/index.ts new file mode 100644 index 0000000..0dc474f --- /dev/null +++ b/src/components/CreateTeam/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateTeamWrapper"; diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx new file mode 100644 index 0000000..85d970d --- /dev/null +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -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>; +type SearchParamNames = keyof SearchQuery; + +const TeamSearch: React.FC = ({ team }) => { + const { t } = useTranslation(); + const [filteredTeam, setFilteredTeam] = useState(team); + const [data, setData] = useState(); + const [isOpen, setIsOpen] = useState(false); + const router = useRouter(); + + const searchCriteria: Criterion[] = 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[]>( + () => [ + // { + // name: "action", + // label: t("Actions"), + // onClick: onStaffClick, + // buttonIcon: , + // }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("description") }, + // { + // name: "action", + // label: t("Actions"), + // onClick: deleteClick, + // buttonIcon: , + // }, + ], + [t], + ); + + return ( + <> + { + // 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), + // ) + // ) + }} + /> + items={filteredTeam} columns={columns} /> + + + ); +}; +export default TeamSearch; diff --git a/src/components/TeamSearch/TeamSearchLoading.tsx b/src/components/TeamSearch/TeamSearchLoading.tsx new file mode 100644 index 0000000..2d9be5b --- /dev/null +++ b/src/components/TeamSearch/TeamSearchLoading.tsx @@ -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 ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default TeamSearchLoading; diff --git a/src/components/TeamSearch/TeamSearchWrapper.tsx b/src/components/TeamSearch/TeamSearchWrapper.tsx new file mode 100644 index 0000000..bd4f714 --- /dev/null +++ b/src/components/TeamSearch/TeamSearchWrapper.tsx @@ -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 ; +}; + +TeamSearchWrapper.Loading = TeamSearchLoading; + +export default TeamSearchWrapper; diff --git a/src/components/TeamSearch/index.ts b/src/components/TeamSearch/index.ts new file mode 100644 index 0000000..f0bf2b6 --- /dev/null +++ b/src/components/TeamSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./TeamSearchWrapper"; From b939d0798ab2db40278f3fc4375dd5c87c3616dc Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Wed, 24 Apr 2024 17:16:51 +0800 Subject: [PATCH 3/9] bug fix --- src/components/ReportSearchBox/SearchBox.tsx | 4 ++-- src/components/ReportSearchBox2/SearchBox2.tsx | 4 ++-- src/components/ReportSearchBox3/SearchBox3.tsx | 4 ++-- src/components/ReportSearchBox4/SearchBox4.tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ReportSearchBox/SearchBox.tsx b/src/components/ReportSearchBox/SearchBox.tsx index c629ad4..83e3aab 100644 --- a/src/components/ReportSearchBox/SearchBox.tsx +++ b/src/components/ReportSearchBox/SearchBox.tsx @@ -289,9 +289,9 @@ function SearchBox({ diff --git a/src/components/ReportSearchBox2/SearchBox2.tsx b/src/components/ReportSearchBox2/SearchBox2.tsx index 37d65ba..d9436a4 100644 --- a/src/components/ReportSearchBox2/SearchBox2.tsx +++ b/src/components/ReportSearchBox2/SearchBox2.tsx @@ -289,9 +289,9 @@ function SearchBox({ diff --git a/src/components/ReportSearchBox3/SearchBox3.tsx b/src/components/ReportSearchBox3/SearchBox3.tsx index 8bf2fea..5fe1867 100644 --- a/src/components/ReportSearchBox3/SearchBox3.tsx +++ b/src/components/ReportSearchBox3/SearchBox3.tsx @@ -289,9 +289,9 @@ function SearchBox({ diff --git a/src/components/ReportSearchBox4/SearchBox4.tsx b/src/components/ReportSearchBox4/SearchBox4.tsx index 8b9dd4d..a0df1a3 100644 --- a/src/components/ReportSearchBox4/SearchBox4.tsx +++ b/src/components/ReportSearchBox4/SearchBox4.tsx @@ -289,9 +289,9 @@ function SearchBox({ From 746a017737477699e4dd040271fcde363e850cea Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Wed, 24 Apr 2024 17:32:34 +0800 Subject: [PATCH 4/9] add report ar06 --- ...port with Outstanding Un-billed Hours.xlsx | Bin 0 -> 12670 bytes .../ProjectCompletionReportWO/page.tsx | 10 +- src/app/api/report6/index.ts | 42 +++ .../ProjectCompletionReportWO.tsx | 17 + .../Report/ProjectCompletionReportWO/index.ts | 2 + .../ProjectCompletionReportWOGen.tsx | 44 +++ .../ProjectCompletionReportWOGenLoading.tsx | 41 +++ .../ProjectCompletionReportWOGenWrapper.tsx | 19 ++ .../ProjectCompletionReportWOGen/index.ts | 2 + .../ReportSearchBox6/SearchBox6.tsx | 302 ++++++++++++++++++ src/components/ReportSearchBox6/index.ts | 3 + 11 files changed, 477 insertions(+), 5 deletions(-) create mode 100644 public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx create mode 100644 src/app/api/report6/index.ts create mode 100644 src/components/Report/ProjectCompletionReportWO/ProjectCompletionReportWO.tsx create mode 100644 src/components/Report/ProjectCompletionReportWO/index.ts create mode 100644 src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGen.tsx create mode 100644 src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenLoading.tsx create mode 100644 src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenWrapper.tsx create mode 100644 src/components/Report/ProjectCompletionReportWOGen/index.ts create mode 100644 src/components/ReportSearchBox6/SearchBox6.tsx create mode 100644 src/components/ReportSearchBox6/index.ts diff --git a/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx b/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..419bd2e45ac75ed8a2d1048027ae33e96fb3c124 GIT binary patch literal 12670 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 pzsiH(&Ht0i|J~f^&A*xdlh{=iU|xL?06>0SX { +const ProjectCompletionReportWO: React.FC = () => { return ( @@ -17,8 +17,8 @@ const ProjectLateReport: React.FC = () => { {/* }> */} - + ); }; -export default ProjectLateReport; +export default ProjectCompletionReportWO; diff --git a/src/app/api/report6/index.ts b/src/app/api/report6/index.ts new file mode 100644 index 0000000..e1f2925 --- /dev/null +++ b/src/app/api/report6/index.ts @@ -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", + }, +]; diff --git a/src/components/Report/ProjectCompletionReportWO/ProjectCompletionReportWO.tsx b/src/components/Report/ProjectCompletionReportWO/ProjectCompletionReportWO.tsx new file mode 100644 index 0000000..c0165f2 --- /dev/null +++ b/src/components/Report/ProjectCompletionReportWO/ProjectCompletionReportWO.tsx @@ -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 ( + }> + + + ); +}; + +export default ProjectCompletionReportWO; \ No newline at end of file diff --git a/src/components/Report/ProjectCompletionReportWO/index.ts b/src/components/Report/ProjectCompletionReportWO/index.ts new file mode 100644 index 0000000..0382210 --- /dev/null +++ b/src/components/Report/ProjectCompletionReportWO/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReport\index.ts +export { default } from "./ProjectCompletionReportWO"; diff --git a/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGen.tsx b/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGen.tsx new file mode 100644 index 0000000..2942e38 --- /dev/null +++ b/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGen.tsx @@ -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>; +type SearchParamNames = keyof SearchQuery; + +const ProgressByClientSearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + + const searchCriteria: Criterion[] = 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 ( + <> + { + console.log(query); + }} + /> + {/* */} + + ); +}; + +export default ProgressByClientSearch; diff --git a/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenLoading.tsx b/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenLoading.tsx new file mode 100644 index 0000000..466b74d --- /dev/null +++ b/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenLoading.tsx @@ -0,0 +1,41 @@ +//src\components\LateStartReportGen\LateStartReportGenLoading.tsx +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const ProjectCompletionReportGenLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectCompletionReportGenLoading; diff --git a/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenWrapper.tsx b/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenWrapper.tsx new file mode 100644 index 0000000..c294e0c --- /dev/null +++ b/src/components/Report/ProjectCompletionReportWOGen/ProjectCompletionReportWOGenWrapper.tsx @@ -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 ; +}; + +ProjectCompletionReportWOGenWrapper.Loading = ProjectCompletionReportWOGenLoading; + +export default ProjectCompletionReportWOGenWrapper; \ No newline at end of file diff --git a/src/components/Report/ProjectCompletionReportWOGen/index.ts b/src/components/Report/ProjectCompletionReportWOGen/index.ts new file mode 100644 index 0000000..20bd632 --- /dev/null +++ b/src/components/Report/ProjectCompletionReportWOGen/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReportGen\index.ts +export { default } from "./ProjectCompletionReportWOGenWrapper"; diff --git a/src/components/ReportSearchBox6/SearchBox6.tsx b/src/components/ReportSearchBox6/SearchBox6.tsx new file mode 100644 index 0000000..1f201b1 --- /dev/null +++ b/src/components/ReportSearchBox6/SearchBox6.tsx @@ -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 { + label: string; + label2?: string; + paramName: T; + paramName2?: T; +} + +interface TextCriterion extends BaseCriterion { + type: "text"; +} + +interface SelectCriterion extends BaseCriterion { + type: "select"; + options: string[]; +} + +interface DateRangeCriterion extends BaseCriterion { + type: "dateRange"; +} + +export type Criterion = + | TextCriterion + | SelectCriterion + | DateRangeCriterion; + +interface Props { + criteria: Criterion[]; + onSearch: (inputs: Record) => void; + onReset?: () => void; +} + +function SearchBox({ + criteria, + onSearch, + onReset, +}: Props) { + const { t } = useTranslation("common"); + const defaultInputs = useMemo( + () => + criteria.reduce>( + (acc, c) => { + return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + }, + {} as Record, + ), + [criteria], + ); + const [inputs, setInputs] = useState(defaultInputs); + + const makeInputChangeHandler = useCallback( + (paramName: T): React.ChangeEventHandler => { + return (e) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, + [], + ); + + const makeSelectChangeHandler = useCallback((paramName: T) => { + return (e: SelectChangeEvent) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, []); + + const makeDateChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + }; + }, []); + + const makeDateToChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), + })); + }; + }, []); + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/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 ( + + + {t("Search Criteria")} + + {criteria.map((c) => { + return ( + + {c.type === "text" && ( + + )} + {c.type === "select" && ( + + {c.label} + + + )} + {c.type === "dateRange" && ( + + + + + + + {"-"} + + + + + + + )} + + ); + })} + + + + + + + + ); +} + +export default SearchBox; diff --git a/src/components/ReportSearchBox6/index.ts b/src/components/ReportSearchBox6/index.ts new file mode 100644 index 0000000..9ead696 --- /dev/null +++ b/src/components/ReportSearchBox6/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBox6"; +export type { Criterion } from "./SearchBox6"; From 3cd7aef4f46502bc0cc72db04b61b4706fc966bc Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Wed, 24 Apr 2024 18:37:24 +0800 Subject: [PATCH 5/9] update create team --- src/components/CreateTeam/StaffAllocation.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx index f85c89b..6a1f778 100644 --- a/src/components/CreateTeam/StaffAllocation.tsx +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -54,11 +54,12 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, []); const setTeamLead = useCallback((staff: StaffResult) => { + setSeletedTeamLead(staff.id) const rearrangedList = getValues("addStaffIds").reduce((acc, num, index) => { if (num === staff.id && index !== 0) { acc.splice(index, 1); - acc.unshift(num); + acc.unshift(num) } return acc; }, getValues("addStaffIds")); @@ -69,10 +70,10 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { return selectedStaff.find((staff) => staff.id === id); }); console.log(rearrangedStaff) - // setSelectedStaff(rearrangedStaff as StaffResult[]); + setSelectedStaff(rearrangedStaff as StaffResult[]); setValue("addStaffIds", rearrangedList) - }, []); + }, [addStaff, selectedStaff]); const clearSubsidiary = useCallback(() => { if (defaultValues !== undefined) { @@ -92,6 +93,10 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { ); }, [selectedStaff, setValue]); + useEffect(() => { + console.log(selectedStaff) + }, [selectedStaff]); + const StaffPoolColumns = useMemo[]>( () => [ { @@ -125,7 +130,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { buttonIcon: , }, ], - [removeStaff, t] + [removeStaff, selectedStaff, t] ); const [query, setQuery] = React.useState(""); From b45d61c3051354758db9488a0d4d5615269052bb Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 24 Apr 2024 18:39:31 +0800 Subject: [PATCH 6/9] update claim, customer, subsidiary --- .../(main)/staffReimbursement/create/page.tsx | 14 +- src/app/(main)/staffReimbursement/page.tsx | 12 +- src/app/api/claims/actions.ts | 48 +++ src/app/api/claims/index.ts | 46 ++- src/app/utils/comboUtil.ts | 11 + src/app/utils/commonUtil.ts | 23 ++ src/app/utils/formatUtil.ts | 8 + src/components/ClaimDetail/ClaimDetail.tsx | 105 ++++++ .../ClaimDetail/ClaimDetailWrapper.tsx | 20 ++ src/components/ClaimDetail/ClaimFormInfo.tsx | 76 +++++ .../ClaimFormInputGrid.tsx} | 299 +++++++++++------- src/components/ClaimDetail/index.ts | 1 + src/components/ClaimSearch/ClaimSearch.tsx | 53 ++-- src/components/CreateClaim/ClaimDetails.tsx | 67 ---- src/components/CreateClaim/CreateClaim.tsx | 48 --- src/components/CreateClaim/index.ts | 1 - .../CustomDatagrid/CustomDatagrid.tsx | 2 +- src/components/CustomerDetail/ContactInfo.tsx | 2 +- .../CustomerDetail/CustomerDetailWrapper.tsx | 2 +- .../CustomerDetail/CustomerInfo.tsx | 4 +- .../CustomerSearch/CustomerSearch.tsx | 1 + src/components/SearchBox/SearchBox.tsx | 2 +- .../SearchResults/SearchResults.tsx | 6 +- .../SubsidiaryDetail/ContactInfo.tsx | 2 +- .../SubsidiaryDetail/SubsidiaryInfo.tsx | 4 +- .../SubsidiarySearch/SubsidiarySearch.tsx | 1 + src/i18n/en/claim.json | 31 ++ src/i18n/en/common.json | 12 + src/i18n/en/customer.json | 2 +- src/i18n/en/subsidiary.json | 2 +- src/i18n/zh/claim.json | 31 ++ src/i18n/zh/common.json | 12 + src/i18n/zh/customer.json | 2 +- src/i18n/zh/subsidiary.json | 2 +- 34 files changed, 671 insertions(+), 281 deletions(-) create mode 100644 src/app/api/claims/actions.ts create mode 100644 src/app/utils/comboUtil.ts create mode 100644 src/components/ClaimDetail/ClaimDetail.tsx create mode 100644 src/components/ClaimDetail/ClaimDetailWrapper.tsx create mode 100644 src/components/ClaimDetail/ClaimFormInfo.tsx rename src/components/{CreateClaim/ClaimInputGrid.tsx => ClaimDetail/ClaimFormInputGrid.tsx} (51%) create mode 100644 src/components/ClaimDetail/index.ts delete mode 100644 src/components/CreateClaim/ClaimDetails.tsx delete mode 100644 src/components/CreateClaim/CreateClaim.tsx delete mode 100644 src/components/CreateClaim/index.ts create mode 100644 src/i18n/en/claim.json create mode 100644 src/i18n/zh/claim.json diff --git a/src/app/(main)/staffReimbursement/create/page.tsx b/src/app/(main)/staffReimbursement/create/page.tsx index eafce4f..f1effc4 100644 --- a/src/app/(main)/staffReimbursement/create/page.tsx +++ b/src/app/(main)/staffReimbursement/create/page.tsx @@ -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 ( <> {t("Create Claim")} - + + + ); }; -export default CreateClaims; +export default ClaimDetails; diff --git a/src/app/(main)/staffReimbursement/page.tsx b/src/app/(main)/staffReimbursement/page.tsx index 1a1afcf..ee8781f 100644 --- a/src/app/(main)/staffReimbursement/page.tsx +++ b/src/app/(main)/staffReimbursement/page.tsx @@ -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")} - }> - - + + }> + + + ); }; diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts new file mode 100644 index 0000000..e77b17f --- /dev/null +++ b/src/app/api/claims/actions.ts @@ -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( + `${BASE_API_URL}/claim/save`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + revalidateTag("claims"); + + return saveCustomer; +}; \ No newline at end of file diff --git a/src/app/api/claims/index.ts b/src/app/api/claims/index.ts index f012bcc..ceb9887 100644 --- a/src/app/api/claims/index.ts +++ b/src/app/api/claims/index.ts @@ -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(`${BASE_API_URL}/claim`); +}); + +export const fetchProjectCombo = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/projects`, { + next: { tags: ["projects"] }, + }); }); -const mockClaims: ClaimResult[] = [ +// export const fetchAllCustomers = cache(async () => { +// return serverFetchJson(`${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", diff --git a/src/app/utils/comboUtil.ts b/src/app/utils/comboUtil.ts new file mode 100644 index 0000000..0fcb75b --- /dev/null +++ b/src/app/utils/comboUtil.ts @@ -0,0 +1,11 @@ +export const expenseTypeCombo = [ + "Petty Cash", + "Expense" +] + +export const claimStatusCombo = [ + "Not Submitted", + "Waiting for Approval", + "Approved", + "Rejected" +] \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index e69de29..d4c71b6 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -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 + } + } +} \ No newline at end of file diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 4f364e8..cd28eb5 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -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", diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimDetail/ClaimDetail.tsx new file mode 100644 index 0000000..36ff8ed --- /dev/null +++ b/src/components/ClaimDetail/ClaimDetail.tsx @@ -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 = ({ projectCombo }) => { + const { t } = useTranslation("common"); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + + const formProps = useForm({ + defaultValues: { + id: null, + expenseType: expenseTypeCombo[0], + addClaimDetails: [] + }, + }); + + const handleCancel = () => { + router.back(); + }; + + const onSubmit = useCallback>( + 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>( + (errors) => { + // Set the tab so that the focus will go there + console.log(errors) + }, + [], + ); + + return ( + + + + {serverError && ( + + {serverError} + + )} + + + + + + + + ); +}; + +export default ClaimDetail; diff --git a/src/components/ClaimDetail/ClaimDetailWrapper.tsx b/src/components/ClaimDetail/ClaimDetailWrapper.tsx new file mode 100644 index 0000000..602f9e3 --- /dev/null +++ b/src/components/ClaimDetail/ClaimDetailWrapper.tsx @@ -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 ( + + ); +}; + +export default ClaimDetailWrapper; diff --git a/src/components/ClaimDetail/ClaimFormInfo.tsx b/src/components/ClaimDetail/ClaimFormInfo.tsx new file mode 100644 index 0000000..6db692d --- /dev/null +++ b/src/components/ClaimDetail/ClaimFormInfo.tsx @@ -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 = ({ projectCombo }) => { + const { t } = useTranslation(); + + const { + control, + register, + } = useFormContext(); + + return ( + + + + + + + + + + {t("Expense Type")} + ( + + )} + /> + + + + + + + + + + ); +}; + +export default ClaimFormInfo; diff --git a/src/components/CreateClaim/ClaimInputGrid.tsx b/src/components/ClaimDetail/ClaimFormInputGrid.tsx similarity index 51% rename from src/components/CreateClaim/ClaimInputGrid.tsx rename to src/components/ClaimDetail/ClaimFormInputGrid.tsx index 1231001..b6c2fde 100644 --- a/src/components/CreateClaim/ClaimInputGrid.tsx +++ b/src/components/ClaimDetail/ClaimFormInputGrid.tsx @@ -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) => {
- Total: + {t("Total")}:
@@ -133,7 +107,7 @@ const BottomBar = (props: BottomBarProps) => { onClick={handleAddClick} sx={{ margin: "20px" }} > - Add record + {t("Add Record")}
); @@ -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 = ({ ...props }) => { - const [rows, setRows] = useState(initialRows); - const [day, setDay] = useState(dayjs()); +const ClaimFormInputGrid: React.FC = ({ + // onClose, + projectCombo, +}) => { + const { t } = useTranslation() + const { control, setValue, getValues, formState: { errors } } = useFormContext(); + const { fields } = useFieldArray({ + control, + name: "addClaimDetails" + }) + + const [rows, setRows] = useState([]); const [rowModesModel, setRowModesModel] = React.useState( {}, ); + // Row function const handleRowEditStop: GridEventListener<"rowEditStop"> = ( params, event, @@ -217,20 +202,77 @@ const ClaimInputGrid: React.FC = ({ ...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> = 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, params: GridRenderEditCellParams) => { + + 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) => { + + 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 = ({ ...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 = ({ ...props }) => { }, }, { - field: "date", - headerName: "Invoice Date", + field: "invoiceDate", + headerName: t("Invoice Date"), // width: 220, flex: 1, editable: true, type: "date", + renderCell: (params: GridRenderCellParams) => { + 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 = ({ ...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 ? ( - + handleLinkClick(params)} href="#">{params.value} + {/* {params.value} - + */} ) : ( No Documents ); }, renderEditCell: (params) => { - return params.value ? ( + const currentRow = rows.find(row => row.id === params.row.id); + return params.formattedValue ? ( - - {params.value} - + handleLinkClick(params)} href="#">{params.formattedValue} + {/* + {params.formattedValue} + */} ) : ( - + + ); }, }, - ]; + ], [rows, rowModesModel, t],); return ( = ({ ...props }) => { }, }} > - ({ 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")} + } + {Boolean(errors.addClaimDetails?.type === "format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> + {t("Please ensure the date formats are correct")} + } +
+ - + initialState={{ + pagination: { paginationModel: { pageSize: 5 } }, + }} + /> +
); }; -export default ClaimInputGrid; +export default ClaimFormInputGrid; diff --git a/src/components/ClaimDetail/index.ts b/src/components/ClaimDetail/index.ts new file mode 100644 index 0000000..0fa3ab2 --- /dev/null +++ b/src/components/ClaimDetail/index.ts @@ -0,0 +1 @@ +export { default } from "./ClaimDetailWrapper"; diff --git a/src/components/ClaimSearch/ClaimSearch.tsx b/src/components/ClaimSearch/ClaimSearch.tsx index 6ab02cf..582b9f8 100644 --- a/src/components/ClaimSearch/ClaimSearch.tsx +++ b/src/components/ClaimSearch/ClaimSearch.tsx @@ -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>; +type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const ClaimSearch: React.FC = ({ 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[] = 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[]>( + const columns = useMemo[]>( () => [ // { // name: "action", @@ -69,9 +56,9 @@ const ClaimSearch: React.FC = ({ 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 = ({ claims }) => { { - 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") + ), + ); }} /> - items={filteredClaims} columns={columns} /> + items={filteredClaims} columns={columns} /> ); }; diff --git a/src/components/CreateClaim/ClaimDetails.tsx b/src/components/CreateClaim/ClaimDetails.tsx deleted file mode 100644 index 0a56ef4..0000000 --- a/src/components/CreateClaim/ClaimDetails.tsx +++ /dev/null @@ -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 ( - - - - {/* - {t("Related Project")} - */} - - - - {t("Related Project")} - - - - - - {t("Expense Type")} - - - - - - - - - - - {/* - - */} - - - ); -}; - -export default ClaimDetails; diff --git a/src/components/CreateClaim/CreateClaim.tsx b/src/components/CreateClaim/CreateClaim.tsx deleted file mode 100644 index 5e89398..0000000 --- a/src/components/CreateClaim/CreateClaim.tsx +++ /dev/null @@ -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>( - (_e, newValue) => { - setTabIndex(newValue); - }, - [], - ); - - return ( - <> - - - - - - - ); -}; - -export default CreateProject; diff --git a/src/components/CreateClaim/index.ts b/src/components/CreateClaim/index.ts deleted file mode 100644 index a0bd052..0000000 --- a/src/components/CreateClaim/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CreateClaim"; diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index 151e90d..314ba6c 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/src/components/CustomDatagrid/CustomDatagrid.tsx @@ -12,7 +12,7 @@ interface CustomDatagridProps { columnWidth?: number; Style?: boolean; sx?: SxProps; - dataGridHeight?: number; + dataGridHeight?: number | string; [key: string]: any; checkboxSelection?: boolean; onRowSelectionModelChange?: ( diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerDetail/ContactInfo.tsx index f608b00..fdc6c76 100644 --- a/src/components/CustomerDetail/ContactInfo.tsx +++ b/src/components/CustomerDetail/ContactInfo.tsx @@ -262,7 +262,7 @@ const ContactInfo: React.FC = ({ {t("Contact Info")} {Boolean(errors.addContacts?.type === "required") && ({ 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")} } {Boolean(errors.addContacts?.type === "email_format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {t("Please ensure all the email formats are correct")} diff --git a/src/components/CustomerDetail/CustomerDetailWrapper.tsx b/src/components/CustomerDetail/CustomerDetailWrapper.tsx index f8d0d12..0206940 100644 --- a/src/components/CustomerDetail/CustomerDetailWrapper.tsx +++ b/src/components/CustomerDetail/CustomerDetailWrapper.tsx @@ -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 = { diff --git a/src/components/CustomerDetail/CustomerInfo.tsx b/src/components/CustomerDetail/CustomerInfo.tsx index 3eaa327..416902e 100644 --- a/src/components/CustomerDetail/CustomerInfo.tsx +++ b/src/components/CustomerDetail/CustomerInfo.tsx @@ -68,7 +68,7 @@ const CustomerInfo: React.FC = ({ = ({ = ({ customers }) => { label: t("Delete"), onClick: onDeleteClick, buttonIcon: , + color: "error" }, ], [onTaskClick, t], diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 13b03ee..26914fe 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -137,7 +137,7 @@ function SearchBox({ {t("All")} {c.options.map((option, index) => ( - {option} + {t(option)} ))} diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index bfecd36..ea0744c 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -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 { name: keyof T; label: string; color?: IconButtonOwnProps["color"]; + needTranslation?: boolean } interface ColumnWithAction extends BaseColumn { @@ -51,6 +54,7 @@ function SearchResults({ }: Props) { 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({ {column.buttonIcon} ) : ( - <>{item[columnName]} + <>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]} )} ); diff --git a/src/components/SubsidiaryDetail/ContactInfo.tsx b/src/components/SubsidiaryDetail/ContactInfo.tsx index ef84ad7..e88201c 100644 --- a/src/components/SubsidiaryDetail/ContactInfo.tsx +++ b/src/components/SubsidiaryDetail/ContactInfo.tsx @@ -263,7 +263,7 @@ const ContactInfo: React.FC = ({ {t("Contact Info")} {Boolean(errors.addContacts?.type === "required") && ({ 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")} } {Boolean(errors.addContacts?.type === "email_format") && ({ color: theme.palette.error.main })} variant="overline" display='inline-block' noWrap> {t("Please ensure all the email formats are correct")} diff --git a/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx b/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx index 84522a4..046becd 100644 --- a/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx +++ b/src/components/SubsidiaryDetail/SubsidiaryInfo.tsx @@ -57,7 +57,7 @@ const SubsidiaryInfo: React.FC = ({ = ({ = ({ subsidiaries }) => { label: t("Delete"), onClick: onDeleteClick, buttonIcon: , + color: "error" }, ], [onTaskClick, t], diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json new file mode 100644 index 0000000..bfd0f84 --- /dev/null +++ b/src/i18n/en/claim.json @@ -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" +} \ No newline at end of file diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 536e11b..51ea204 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -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" } \ No newline at end of file diff --git a/src/i18n/en/customer.json b/src/i18n/en/customer.json index 38871c6..f7b7dff 100644 --- a/src/i18n/en/customer.json +++ b/src/i18n/en/customer.json @@ -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?", diff --git a/src/i18n/en/subsidiary.json b/src/i18n/en/subsidiary.json index 3c06c1c..f62a640 100644 --- a/src/i18n/en/subsidiary.json +++ b/src/i18n/en/subsidiary.json @@ -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?", diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json new file mode 100644 index 0000000..a22acca --- /dev/null +++ b/src/i18n/zh/claim.json @@ -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": "行動" +} \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index c1cb0a9..f857985 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -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": "重置" } \ No newline at end of file diff --git a/src/i18n/zh/customer.json b/src/i18n/zh/customer.json index 71cb293..d682f81 100644 --- a/src/i18n/zh/customer.json +++ b/src/i18n/zh/customer.json @@ -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?": "你是否確認要提交?", diff --git a/src/i18n/zh/subsidiary.json b/src/i18n/zh/subsidiary.json index e1c861a..c56a884 100644 --- a/src/i18n/zh/subsidiary.json +++ b/src/i18n/zh/subsidiary.json @@ -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?": "你是否確認要提交?", From 58b730b5333648e28d55c83e90da387e50dac300 Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Wed, 24 Apr 2024 18:55:49 +0800 Subject: [PATCH 7/9] add combo list --- src/app/utils/ComboConst.js | 28 ++++++++++++++++++++++++++++ src/auth/utils.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/app/utils/ComboConst.js create mode 100644 src/auth/utils.js diff --git a/src/app/utils/ComboConst.js b/src/app/utils/ComboConst.js new file mode 100644 index 0000000..7c9c0b5 --- /dev/null +++ b/src/app/utils/ComboConst.js @@ -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",}, + ]) +} diff --git a/src/auth/utils.js b/src/auth/utils.js new file mode 100644 index 0000000..143f099 --- /dev/null +++ b/src/auth/utils.js @@ -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'} +} From edce54f3549110a9d4cd4d14d10ebed20d6e190f Mon Sep 17 00:00:00 2001 From: Wayne Date: Wed, 24 Apr 2024 21:37:08 +0900 Subject: [PATCH 8/9] Add subsidiary names to project creation --- src/app/(main)/projects/create/page.tsx | 3 ++ .../CreateProject/CreateProject.tsx | 5 ++- .../CreateProject/CreateProjectWrapper.tsx | 5 ++- .../CreateProject/MilestoneSection.tsx | 10 ++++-- .../CreateProject/ProjectClientDetails.tsx | 33 ++++++++++++++----- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx index 102e272..746fcb1 100644 --- a/src/app/(main)/projects/create/page.tsx +++ b/src/app/(main)/projects/create/page.tsx @@ -1,3 +1,4 @@ +import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; import { fetchProjectBuildingTypes, fetchProjectCategories, @@ -31,6 +32,8 @@ const Projects: React.FC = async () => { fetchProjectServiceTypes(); fetchProjectBuildingTypes(); fetchProjectWorkNatures(); + fetchAllCustomers(); + fetchAllSubsidiaries(); preloadTeamLeads(); preloadStaff(); diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index b8faa5e..e272a9e 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -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[]; @@ -76,6 +77,7 @@ const CreateProject: React.FC = ({ teamLeads, grades, allCustomers, + allSubsidiaries, contractTypes, fundingTypes, locationTypes, @@ -171,6 +173,7 @@ const CreateProject: React.FC = ({ locationTypes={locationTypes} serviceTypes={serviceTypes} allCustomers={allCustomers} + allSubsidiaries={allSubsidiaries} projectCategories={projectCategories} teamLeads={teamLeads} isActive={tabIndex === 0} diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx index 67acc03..915ef67 100644 --- a/src/components/CreateProject/CreateProjectWrapper.tsx +++ b/src/components/CreateProject/CreateProjectWrapper.tsx @@ -10,7 +10,7 @@ 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"; const CreateProjectWrapper: React.FC = async () => { const [ @@ -19,6 +19,7 @@ const CreateProjectWrapper: React.FC = async () => { projectCategories, teamLeads, allCustomers, + allSubsidiaries, contractTypes, fundingTypes, locationTypes, @@ -32,6 +33,7 @@ const CreateProjectWrapper: React.FC = async () => { fetchProjectCategories(), fetchTeamLeads(), fetchAllCustomers(), + fetchAllSubsidiaries(), fetchProjectContractTypes(), fetchProjectFundingTypes(), fetchProjectLocationTypes(), @@ -47,6 +49,7 @@ const CreateProjectWrapper: React.FC = async () => { projectCategories={projectCategories} taskTemplates={taskTemplates} teamLeads={teamLeads} + allSubsidiaries={allSubsidiaries} allCustomers={allCustomers} contractTypes={contractTypes} fundingTypes={fundingTypes} diff --git a/src/components/CreateProject/MilestoneSection.tsx b/src/components/CreateProject/MilestoneSection.tsx index 68a35a2..9984875 100644 --- a/src/components/CreateProject/MilestoneSection.tsx +++ b/src/components/CreateProject/MilestoneSection.tsx @@ -43,7 +43,8 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { t, i18n: { language }, } = useTranslation(); - const { getValues, setValue } = useFormContext(); + const { getValues, setValue, formState } = + useFormContext(); const [payments, setPayments] = useState( getValues("milestones")[taskGroupId]?.payments || [], ); @@ -223,6 +224,9 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { ); + const startDate = getValues("milestones")[taskGroupId]?.startDate; + const endDate = getValues("milestones")[taskGroupId]?.endDate; + return ( @@ -237,7 +241,7 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { { if (!date) return; const milestones = getValues("milestones"); @@ -256,7 +260,7 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { { if (!date) return; const milestones = getValues("milestones"); diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 8bdbf4d..ef5a89f 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -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 = ({ projectCategories, teamLeads, allCustomers, + allSubsidiaries, serviceTypes, contractTypes, fundingTypes, @@ -69,6 +71,13 @@ const ProjectClientDetails: React.FC = ({ getValues, } = useFormContext(); + 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 = ({ name="clientSubsidiaryId" render={({ field }) => ( )} /> From cb49386ca726f5ddbdbce8e0c81081ebc6c3cc56 Mon Sep 17 00:00:00 2001 From: Wayne Date: Wed, 24 Apr 2024 22:40:47 +0900 Subject: [PATCH 9/9] Use grade and position from API for staff --- src/app/(main)/projects/create/page.tsx | 2 ++ src/app/api/grades/index.ts | 10 ++++++++++ src/app/api/positions/index.ts | 18 +++++++++--------- src/components/CreateProject/CreateProject.tsx | 2 -- .../CreateProject/CreateProjectWrapper.tsx | 12 ++++-------- .../CreateProject/StaffAllocation.tsx | 11 +---------- 6 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx index 746fcb1..c652a70 100644 --- a/src/app/(main)/projects/create/page.tsx +++ b/src/app/(main)/projects/create/page.tsx @@ -1,4 +1,5 @@ import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchGrades } from "@/app/api/grades"; import { fetchProjectBuildingTypes, fetchProjectCategories, @@ -34,6 +35,7 @@ const Projects: React.FC = async () => { fetchProjectWorkNatures(); fetchAllCustomers(); fetchAllSubsidiaries(); + fetchGrades(); preloadTeamLeads(); preloadStaff(); diff --git a/src/app/api/grades/index.ts b/src/app/api/grades/index.ts index 88398d5..33f0f6d 100644 --- a/src/app/api/grades/index.ts +++ b/src/app/api/grades/index.ts @@ -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(`${BASE_API_URL}/grades`, { + next: { tags: ["grades"] }, + }); +}); diff --git a/src/app/api/positions/index.ts b/src/app/api/positions/index.ts index 981554f..e144e93 100644 --- a/src/app/api/positions/index.ts +++ b/src/app/api/positions/index.ts @@ -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(`${BASE_API_URL}/positions`, { - next: { tags: ["positions"] }, - }); -}); \ No newline at end of file + return serverFetchJson(`${BASE_API_URL}/positions`, { + next: { tags: ["positions"] }, + }); +}); diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index e272a9e..3e1a3f0 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -51,8 +51,6 @@ export interface Props { buildingTypes: BuildingType[]; workNatures: WorkNature[]; allStaffs: StaffResult[]; - - // Mocked grades: Grade[]; } diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx index 915ef67..3ca2fae 100644 --- a/src/components/CreateProject/CreateProjectWrapper.tsx +++ b/src/components/CreateProject/CreateProjectWrapper.tsx @@ -11,6 +11,7 @@ import { } from "@/app/api/projects"; import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchGrades } from "@/app/api/grades"; const CreateProjectWrapper: React.FC = async () => { const [ @@ -27,6 +28,7 @@ const CreateProjectWrapper: React.FC = async () => { buildingTypes, workNatures, allStaffs, + grades, ] = await Promise.all([ fetchAllTasks(), fetchTaskTemplates(), @@ -41,6 +43,7 @@ const CreateProjectWrapper: React.FC = async () => { fetchProjectBuildingTypes(), fetchProjectWorkNatures(), fetchStaff(), + fetchGrades(), ]); return ( @@ -58,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} /> ); }; diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index b724b0e..b65aeb5 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -54,7 +54,7 @@ export interface Props { } const StaffAllocation: React.FC = ({ - allStaffs: dataStaffs, + allStaffs, allTasks, isActive, defaultManhourBreakdownByGrade, @@ -63,15 +63,6 @@ const StaffAllocation: React.FC = ({ const { t } = useTranslation(); const { setValue, getValues, watch } = useFormContext(); - // TODO: remove this when grade and positions are done - const allStaffs = useMemo(() => { - 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), );