From afc013a306ba3172d3cfc723d540dcef98917521 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Wed, 22 May 2024 13:31:14 +0800 Subject: [PATCH 01/16] update overconsumption report --- .../ResourceOverconsumptionReport/page.tsx | 40 ++- src/app/api/report3/index.ts | 42 --- src/app/api/reports/actions.ts | 15 +- src/app/api/reports/index.ts | 14 + .../Report/ReportSearchBox3/SearchBox3.tsx | 313 ------------------ .../Report/ReportSearchBox3/index.ts | 3 - .../ResourceOverconsumptionReportGen.tsx | 45 --- ...esourceOverconsumptionReportGenWrapper.tsx | 19 -- .../ResourceOverconsumptionReportGen/index.ts | 2 - .../ResourceOverconsumptionReport.tsx | 96 ++++++ .../ResourceOverconsumptionReportLoading.tsx} | 4 +- .../ResourceOverconsumptionReportWrapper.tsx | 20 ++ .../ResourceOverconsumptionReport/index.ts | 1 + src/components/SearchBox/SearchBox.tsx | 62 +++- src/components/utils/numberInput.tsx | 192 +++++++++++ src/theme/colorConst.js | 153 +++++++++ 16 files changed, 565 insertions(+), 456 deletions(-) delete mode 100644 src/app/api/report3/index.ts delete mode 100644 src/components/Report/ReportSearchBox3/SearchBox3.tsx delete mode 100644 src/components/Report/ReportSearchBox3/index.ts delete mode 100644 src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx delete mode 100644 src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx delete mode 100644 src/components/Report/ResourceOverconsumptionReportGen/index.ts create mode 100644 src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx rename src/components/{Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx => ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx} (89%) create mode 100644 src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx create mode 100644 src/components/ResourceOverconsumptionReport/index.ts create mode 100644 src/components/utils/numberInput.tsx diff --git a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx index a1751be..771d21f 100644 --- a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx +++ b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx @@ -1,24 +1,28 @@ -//src\app\(main)\analytics\ResourceOvercomsumptionReport\page.tsx import { Metadata } from "next"; -import { I18nProvider } from "@/i18n"; -import Typography from "@mui/material/Typography"; -import ResourceOverconsumptionReportComponent from "@/components/Report/ResourceOverconsumptionReport"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import ResourceOverconsumptionReport from "@/components/ResourceOverconsumptionReport"; +import { Typography } from "@mui/material"; export const metadata: Metadata = { - title: "Resource Overconsumption Report", + title: "Staff Monthly Work Hours Analysis Report", }; -const ResourceOverconsumptionReport: React.FC = () => { - return ( - - - Resource Overconsumption Report - - {/* }> - - */} - - - ); +const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => { + const { t } = await getServerI18n("User Group"); + + return ( + <> + + {t("Project Resource Overconsumption Report")} + + + }> + + + + + ); }; -export default ResourceOverconsumptionReport; + +export default StaffMonthlyWorkHoursAnalysisReport; diff --git a/src/app/api/report3/index.ts b/src/app/api/report3/index.ts deleted file mode 100644 index b2c8751..0000000 --- a/src/app/api/report3/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -//src\app\api\report\index.ts -import { cache } from "react"; - -export interface ResourceOverconsumption { - id: number; - projectCode: string; - projectName: string; - team: string; - teamLeader: string; - startDate: string; - startDateFrom: string; - startDateTo: string; - targetEndDate: string; - client: string; - subsidiary: string; - status: string; -} - -export const preloadProjects = () => { - fetchProjectsResourceOverconsumption(); -}; - -export const fetchProjectsResourceOverconsumption = cache(async () => { - return mockProjects; -}); - -const mockProjects: ResourceOverconsumption[] = [ - { - 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", - status: "1", - }, -]; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 3d49a16..a4c2b85 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -1,7 +1,7 @@ "use server"; import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; -import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest } from "."; +import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest, ProjectResourceOverconsumptionReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; export interface FileResponse { @@ -35,6 +35,19 @@ export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRe return reportBlob }; +export const fetchProjectResourceOverconsumptionReport = async (data: ProjectResourceOverconsumptionReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/ProjectResourceOverconsumptionReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; + // export const fetchLateStartReport = async (data: LateStartReportRequest) => { // const response = await serverFetchBlob( // `${BASE_API_URL}/reports/downloadLateStartReport`, diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index dd1afb8..f1fa613 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -25,6 +25,20 @@ export interface MonthlyWorkHoursReportRequest { id: number; yearMonth: string; } +// - Project Resource Overconsumption Report +export interface ProjectResourceOverconsumptionReportFilter { + team: string[]; + customer: string[]; + status: string[]; + lowerLimit: number; +} + +export interface ProjectResourceOverconsumptionReportRequest { + teamId?: number + custId?: number + status: "All" | "Within Budget" | "Potential Overconsumption" | "Overconsumption" + lowerLimit: number +} export interface LateStartReportFilter { remainedDays: number; diff --git a/src/components/Report/ReportSearchBox3/SearchBox3.tsx b/src/components/Report/ReportSearchBox3/SearchBox3.tsx deleted file mode 100644 index aafa7d0..0000000 --- a/src/components/Report/ReportSearchBox3/SearchBox3.tsx +++ /dev/null @@ -1,313 +0,0 @@ -//src\components\ReportSearchBox3\SearchBox3.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/AR03_Resource Overconsumption.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 }); - - // 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 A4 (bold) - ['A2', 'A3', 'A4'].forEach(cell => { - if (worksheet[cell]) { - worksheet[cell].s = { font: { bold: true } }; - } - }); - - // Formatting from A6 to L6 - // Apply styles from A6 to L6 (bold, bottom border, center alignment) - for (let col = 0; col < 12; col++) { // Columns A to K - const cellRef = XLSX.utils.encode_col(col) + '6'; - if (worksheet[cellRef]) { - worksheet[cellRef].s = { - font: { bold: true }, - alignment: { horizontal: 'center' }, - border: { - bottom: { style: 'thin', color: { auto: 1 } } - } - }; - } - } - - const firstTableData = [ - ['Column1', 'Column2', 'Column3'], // Row 1 - ['Data1', 'Data2', 'Data3'], // Row 2 - // ... more rows as needed - ]; - // Find the last row of the first table - let lastRowOfFirstTable = 6; // Starting row for data in the first table - while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { - lastRowOfFirstTable++; - } - - // 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 - }); - - // 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 = `AR03_Resource_Overconsumption_${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/Report/ReportSearchBox3/index.ts b/src/components/Report/ReportSearchBox3/index.ts deleted file mode 100644 index d481fbd..0000000 --- a/src/components/Report/ReportSearchBox3/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -//src\components\SearchBox\index.ts -export { default } from "./SearchBox3"; -export type { Criterion } from "./SearchBox3"; diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx deleted file mode 100644 index a6ec216..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx +++ /dev/null @@ -1,45 +0,0 @@ -//src\components\LateStartReportGen\LateStartReportGen.tsx -"use client"; -import React, { useMemo, useState } from "react"; -import SearchBox, { Criterion } from "../ReportSearchBox3"; -import { useTranslation } from "react-i18next"; -import { ResourceOverconsumption } from "@/app/api/report3"; - -interface Props { - projects: ResourceOverconsumption[]; -} -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: "select", options: ["AAA", "BBB", "CCC"] }, - { label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] }, - { label: "Status", paramName: "status", type: "select", options: ["Overconsumption", "Potential Overconsumption"] }, - // { - // label: "Status", - // label2: "Remained Date To", - // paramName: "targetEndDate", - // type: "dateRange", - // }, - ], - [t], - ); - - return ( - <> - { - console.log(query); - }} - /> - {/* */} - - ); -}; - -export default ProgressByClientSearch; diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx deleted file mode 100644 index a93f64b..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx +++ /dev/null @@ -1,19 +0,0 @@ -//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx -import { fetchProjectsResourceOverconsumption } from "@/app/api/report3"; -import React from "react"; -import ResourceOvercomsumptionReportGen from "./ResourceOverconsumptionReportGen"; -import ResourceOvercomsumptionReportGenLoading from "./ResourceOverconsumptionReportGenLoading"; - -interface SubComponents { - Loading: typeof ResourceOvercomsumptionReportGenLoading; -} - -const ResourceOvercomsumptionReportGenWrapper: React.FC & SubComponents = async () => { - const clentprojects = await fetchProjectsResourceOverconsumption(); - - return ; -}; - -ResourceOvercomsumptionReportGenWrapper.Loading = ResourceOvercomsumptionReportGenLoading; - -export default ResourceOvercomsumptionReportGenWrapper; \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReportGen/index.ts b/src/components/Report/ResourceOverconsumptionReportGen/index.ts deleted file mode 100644 index 82fb633..0000000 --- a/src/components/Report/ResourceOverconsumptionReportGen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//src\components\DelayReportGen\index.ts -export { default } from "./ResourceOverconsumptionReportGenWrapper"; diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx new file mode 100644 index 0000000..5e6e76a --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx @@ -0,0 +1,96 @@ +"use client"; +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { fetchMonthlyWorkHoursReport, fetchProjectCashFlowReport, fetchProjectResourceOverconsumptionReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { BASE_API_URL } from "@/config/api"; +import { ProjectResourceOverconsumptionReportFilter, ProjectResourceOverconsumptionReportRequest } from "@/app/api/reports"; +import { StaffResult } from "@/app/api/staff"; +import { TeamResult } from "@/app/api/team"; +import { Customer } from "@/app/api/customer"; + +interface Props { + team: TeamResult[] + customer: Customer[] +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ResourceOverconsumptionReport: React.FC = ({ team, customer }) => { + const { t } = useTranslation(); + const teamCombo = team.map(t => `${t.name} - ${t.code}`) + const custCombo = customer.map(c => `${c.name} - ${c.code}`) + const statusCombo = ["Within Budget, Overconsumption", "Potential Overconsumption"] + // const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`) + // console.log(staffs) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team"), + paramName: "team", + type: "select", + options: teamCombo, + needAll: true + }, + { + label: t("Client"), + paramName: "customer", + type: "select", + options: custCombo, + needAll: true + }, + { + label: t("Status"), + paramName: "status", + type: "select", + options: statusCombo, + needAll: true + }, + { + label: t("lowerLimit"), + paramName: "lowerLimit", + type: "number", + }, + ], + [t], + ); + +return ( + <> + { + let index = 0 + let postData: ProjectResourceOverconsumptionReportRequest = { + status: "All", + lowerLimit: 0.9 + } + if (query.team.length > 0 && query.team.toLocaleLowerCase() !== "all") { + index = teamCombo.findIndex(team => team === query.team) + postData.teamId = team[index].id + } + if (query.customer.length > 0 && query.customer.toLocaleLowerCase() !== "all") { + index = custCombo.findIndex(customer => customer === query.customer) + postData.custId = customer[index].id + } + if (Boolean(query.lowerLimit)) { + postData.lowerLimit = query.lowerLimit/100 + } + postData.status = query.status + console.log(postData) + const response = await fetchProjectResourceOverconsumptionReport(postData) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } + } + } + /> + + ) +} + +export default ResourceOverconsumptionReport \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx similarity index 89% rename from src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx rename to src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx index 9ae7417..945d13c 100644 --- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx @@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack"; import React from "react"; // Can make this nicer -export const ResourceOvercomsumptionReportGenLoading: React.FC = () => { +export const ResourceOvercomsumptionReportLoading: React.FC = () => { return ( <> @@ -38,4 +38,4 @@ export const ResourceOvercomsumptionReportGenLoading: React.FC = () => { ); }; -export default ResourceOvercomsumptionReportGenLoading; +export default ResourceOvercomsumptionReportLoading; diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx new file mode 100644 index 0000000..1ab9d24 --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import ResourceOvercomsumptionReportLoading from "./ResourceOverconsumptionReportLoading"; +import ResourceOverconsumptionReport from "./ResourceOverconsumptionReport"; +import { fetchAllCustomers } from "@/app/api/customer"; +import { fetchTeam } from "@/app/api/team"; + +interface SubComponents { + Loading: typeof ResourceOvercomsumptionReportLoading; +} + +const ResourceOvercomsumptionReportWrapper: React.FC & SubComponents = async () => { + const customers = await fetchAllCustomers() + const teams = await fetchTeam () + + return ; +}; + +ResourceOvercomsumptionReportWrapper.Loading = ResourceOvercomsumptionReportLoading; + +export default ResourceOvercomsumptionReportWrapper; \ No newline at end of file diff --git a/src/components/ResourceOverconsumptionReport/index.ts b/src/components/ResourceOverconsumptionReport/index.ts new file mode 100644 index 0000000..b5f20e2 --- /dev/null +++ b/src/components/ResourceOverconsumptionReport/index.ts @@ -0,0 +1 @@ +export { default } from "./ResourceOverconsumptionReportWrapper"; \ No newline at end of file diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 686eb8d..2ab52b0 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -4,7 +4,7 @@ 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 React, { FocusEvent, KeyboardEvent, PointerEvent, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import TextField from "@mui/material/TextField"; import FormControl from "@mui/material/FormControl"; @@ -15,7 +15,7 @@ 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 FileDownload from '@mui/icons-material/FileDownload'; +import FileDownload from "@mui/icons-material/FileDownload"; import dayjs from "dayjs"; import "dayjs/locale/zh-hk"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; @@ -23,6 +23,17 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { Box } from "@mui/material"; import { DateCalendar } from "@mui/x-date-pickers"; +import { + Unstable_NumberInput as BaseNumberInput, + NumberInputProps, + numberInputClasses, +} from "@mui/base/Unstable_NumberInput"; +import { + StyledButton, + StyledInputElement, + StyledInputRoot, +} from "@/theme/colorConst"; +import { InputAdornment, NumberInput } from "../utils/numberInput"; interface BaseCriterion { label: string; @@ -49,17 +60,22 @@ interface MonthYearCriterion extends BaseCriterion { type: "monthYear"; } +interface NumberCriterion extends BaseCriterion { + type: "number"; +} + export type Criterion = | TextCriterion | SelectCriterion | DateRangeCriterion - | MonthYearCriterion; + | MonthYearCriterion + | NumberCriterion; interface Props { criteria: Criterion[]; onSearch: (inputs: Record) => void; onReset?: () => void; - formType?: String, + formType?: String; } function SearchBox({ @@ -75,10 +91,14 @@ function SearchBox({ (acc, c) => { return { ...acc, - [c.paramName]: c.type === "select" ? - !(c.needAll === false) ? "All" : - c.options.length > 0 ? c.options[0] : "" - : "" + [c.paramName]: + c.type === "select" + ? !(c.needAll === false) + ? "All" + : c.options.length > 0 + ? c.options[0] + : "" + : "", }; }, {} as Record @@ -86,7 +106,7 @@ function SearchBox({ [criteria] ); const [inputs, setInputs] = useState(defaultInputs); - + const makeInputChangeHandler = useCallback( (paramName: T): React.ChangeEventHandler => { return (e) => { @@ -95,6 +115,15 @@ function SearchBox({ }, [] ); + + const makeNumberChangeHandler = useCallback( + (paramName: T): (event: FocusEvent | PointerEvent | KeyboardEvent, value: number | null) => void => { + return (event, value) => { + setInputs((i) => ({ ...i, [paramName]: value })); + }; + }, + [] + ); const makeSelectChangeHandler = useCallback((paramName: T) => { return (e: SelectChangeEvent) => { @@ -110,7 +139,7 @@ function SearchBox({ const makeMonthYearChangeHandler = useCallback((paramName: T) => { return (e: any) => { - console.log(dayjs(e).format("YYYY-MM")) + console.log(dayjs(e).format("YYYY-MM")); setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") })); }; }, []); @@ -168,6 +197,15 @@ function SearchBox({ )} + {c.type === "number" && ( + %} + /> + )} {c.type === "monthYear" && ( ({ - + */} diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 8fb7fab..9e4ac7f 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -39,6 +39,7 @@ import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComp interface Props { isActive: boolean; isSubProject: boolean; + isEditMode: boolean; mainProjects?: MainProject[]; projectCategories: ProjectCategory[]; teamLeads: StaffResult[]; @@ -55,6 +56,7 @@ interface Props { const ProjectClientDetails: React.FC = ({ isActive, isSubProject, + isEditMode, mainProjects, projectCategories, teamLeads, @@ -110,6 +112,7 @@ const ProjectClientDetails: React.FC = ({ ); // get customer (client) contact combo + const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) useEffect(() => { if (selectedCustomerId !== undefined) { fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => { @@ -118,7 +121,7 @@ const ProjectClientDetails: React.FC = ({ // if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0]) // else - setValue("clientSubsidiaryId", undefined) + if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } else setValue("clientSubsidiaryId", null) // if (contacts.length > 0) setValue("clientContactId", contacts[0].id) // else setValue("clientContactId", undefined) }); @@ -130,11 +133,11 @@ const ProjectClientDetails: React.FC = ({ if (Boolean(clientSubsidiaryId)) { // get subsidiary contact combo const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!! - setSubsidiaryContacts(contacts) + setSubsidiaryContacts(() => contacts) setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && Boolean(defaultValues?.clientSubsidiaryId) ? contacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? contacts[0].id : contacts[0].id) setValue("isSubsidiaryContact", true) } else if (customerContacts?.length > 0) { - setSubsidiaryContacts([]) + setSubsidiaryContacts(() => []) setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && !Boolean(defaultValues?.clientSubsidiaryId) ? customerContacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? customerContacts[0].id : customerContacts[0].id) setValue("isSubsidiaryContact", false) } @@ -153,7 +156,7 @@ const ProjectClientDetails: React.FC = ({ // Automatically update the project & client details whene select a main project const mainProjectId = watch("mainProjectId") useEffect(() => { - if (mainProjectId !== undefined && mainProjects !== undefined) { + if (mainProjectId !== undefined && mainProjects !== undefined && !isEditMode) { const mainProject = mainProjects.find(project => project.projectId === mainProjectId); if (mainProject !== undefined) { @@ -174,7 +177,7 @@ const ProjectClientDetails: React.FC = ({ setValue("clientContactId", mainProject.clientContactId) } } - }, [getValues, mainProjectId, setValue]) + }, [getValues, mainProjectId, setValue, isEditMode]) // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( // (acc, building) => ({ ...acc, [building.id]: building.name }), @@ -202,6 +205,7 @@ const ProjectClientDetails: React.FC = ({ name="mainProjectId" label={t("Main Project")} noOptionsText={t("No Main Project")} + disabled={isEditMode} /> @@ -438,11 +442,11 @@ const ProjectClientDetails: React.FC = ({ )} - + {/* - + */} ); diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index 81d3c97..e61f995 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslation } from "react-i18next"; -import React, { useEffect, useMemo } from "react"; +import React, { SyntheticEvent, useEffect, useMemo } from "react"; import RestartAlt from "@mui/icons-material/RestartAlt"; import SearchResults, { Column } from "../SearchResults"; import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; @@ -160,8 +160,8 @@ const StaffAllocation: React.FC = ({ }, [columnFilters]); const [filters, setFilters] = React.useState(defaultFilterValues); const makeFilterSelect = React.useCallback( - (filter: keyof StaffResult) => (event: SelectChangeEvent) => { - setFilters((f) => ({ ...f, [filter]: event.target.value })); + (filter: keyof StaffResult) => (event: SyntheticEvent, value: NonNullable) => { + setFilters((f) => ({ ...f, [filter]: value })); }, [], ); @@ -239,20 +239,25 @@ const StaffAllocation: React.FC = ({ return ( - {label} - + renderInput={(params) => } + /> ); @@ -289,11 +294,11 @@ const StaffAllocation: React.FC = ({ )} - + {/* - + */} {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx index fe02f37..759b05f 100644 --- a/src/components/CreateProject/TaskSetup.tsx +++ b/src/components/CreateProject/TaskSetup.tsx @@ -135,7 +135,7 @@ const TaskSetup: React.FC = ({ taskTemplate.id === selectedTaskTemplateId)} options={[{id: "All", name: t("All tasks")}, ...taskTemplates.map(taskTemplate => ({id: taskTemplate.id, name: taskTemplate.name}))]} @@ -207,11 +207,11 @@ const TaskSetup: React.FC = ({ allItemsLabel={t("Task Pool")} selectedItemsLabel={t("Project Task List")} /> - + {/* - + */} ); diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index 79ee51b..bbb1d67 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -20,7 +20,6 @@ type SearchParamNames = keyof SearchQuery; const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const router = useRouter(); const { t } = useTranslation("projects"); - console.log(projects) const [filteredProjects, setFilteredProjects] = useState(projects); @@ -62,7 +61,9 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const onProjectClick = useCallback( (project: ProjectResult) => { - router.push(`/projects/edit?id=${project.id}`); + if (Boolean(project.mainProject)) { + router.push(`/projects/edit/sub?id=${project.id}`); + } else router.push(`/projects/edit?id=${project.id}`); }, [router], ); From b91acd66758870f55af6ff5db545d5e4b6259f8f Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Wed, 22 May 2024 16:05:31 +0800 Subject: [PATCH 04/16] update --- src/components/EditTeam/Allocation.tsx | 24 ++- src/components/EditUser/AuthAllocation.tsx | 196 +++++++++++---------- src/components/EditUser/EditUser.tsx | 100 +++++++---- src/components/EditUser/UserDetail.tsx | 41 ++++- 4 files changed, 214 insertions(+), 147 deletions(-) diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index 2376ece..b9762f5 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -60,24 +60,20 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)) } ); - const [seletedTeamLead, setSeletedTeamLead] = useState(); const [deletedStaffIds, setDeletedStaffIds] = useState([]); // Adding / Removing staff const addStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => [...s, staff]); - // setDeletedStaffIds((s) => s.filter((s) => s === selectedStaff.id)) }, []); const removeStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); - // setDeletedStaffIds((s) => s) setDeletedStaffIds((prevIds) => [...prevIds, staff.id]); }, []); const setTeamLead = useCallback( (staff: StaffResult) => { - setSeletedTeamLead(staff.id); const rearrangedList = getValues("addStaffIds").reduce( (acc, num, index) => { if (num === staff.id && index !== 0) { @@ -171,16 +167,16 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { }, []); 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)) - // }) - // ); + setFilteredStaff( + initialStaffs.filter((i) => { + const q = query.toLowerCase(); + return ( + i.staffId.toLowerCase().includes(q) || + i.name.toLowerCase().includes(q) || + i.currentPosition.toLowerCase().includes(q) + ); + }) + ); }, [staff, query]); useEffect(() => { diff --git a/src/components/EditUser/AuthAllocation.tsx b/src/components/EditUser/AuthAllocation.tsx index afb44d5..fe6c5a9 100644 --- a/src/components/EditUser/AuthAllocation.tsx +++ b/src/components/EditUser/AuthAllocation.tsx @@ -1,93 +1,97 @@ "use client"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; -import { Add, Clear, PersonAdd, PersonRemove, Remove, Search } from "@mui/icons-material"; +import { + Add, + Clear, + PersonAdd, + PersonRemove, + Remove, + Search, +} from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { - FieldErrors, - FormProvider, - SubmitErrorHandler, - SubmitHandler, - useForm, - useFormContext, - } from "react-hook-form"; + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; import { - Box, - Card, - CardContent, - Grid, - IconButton, - InputAdornment, - Stack, - Tab, - Tabs, - TabsProps, - TextField, - Typography, - } from "@mui/material"; - import { differenceBy } from "lodash"; + Box, + Card, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { differenceBy } from "lodash"; import { UserInputs } from "@/app/api/user/actions"; import { auth } from "@/app/api/group/actions"; import SearchResults, { Column } from "../SearchResults"; export interface Props { - auths: auth[] - - } + auths: auth[]; +} const AuthAllocation: React.FC = ({ auths }) => { - const { t } = useTranslation(); - const searchParams = useSearchParams(); - const id = parseInt(searchParams.get("id") || "0"); - const { - setValue, - getValues, - formState: { defaultValues }, - reset, - resetField, - } = useFormContext(); - const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id); - const [filteredAuths, setFilteredAuths] = useState(initialAuths); - const [selectedAuths, setSelectedAuths] = useState( - () => { - return filteredAuths.filter( - (s) => getValues("addAuthIds")?.includes(s.id) - ); - } + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const id = parseInt(searchParams.get("id") || "0"); + const { + setValue, + getValues, + formState: { defaultValues }, + reset, + resetField, + } = useFormContext(); + const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id); + const [filteredAuths, setFilteredAuths] = useState(initialAuths); + const [selectedAuths, setSelectedAuths] = useState( + () => { + return filteredAuths.filter( + (s) => getValues("addAuthIds")?.includes(s.id) ); - const [removeAuthIds, setRemoveAuthIds] = useState([]); + } + ); + const [removeAuthIds, setRemoveAuthIds] = useState([]); - // Adding / Removing Auth - const addAuth = useCallback((auth: auth) => { - setSelectedAuths((a) => [...a, auth]); - }, []); - const removeAuth = useCallback((auth: auth) => { - setSelectedAuths((a) => a.filter((a) => a.id !== auth.id)); - setRemoveAuthIds((prevIds) => [...prevIds, auth.id]); - }, []); + // Adding / Removing Auth + const addAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => [...a, auth]); + }, []); + const removeAuth = useCallback((auth: auth) => { + setSelectedAuths((a) => a.filter((a) => a.id !== auth.id)); + setRemoveAuthIds((prevIds) => [...prevIds, auth.id]); + }, []); - const clearAuth = useCallback(() => { - if (defaultValues !== undefined) { - resetField("addAuthIds"); - setSelectedAuths( - initialAuths.filter((auth) => defaultValues.addAuthIds?.includes(auth.id)) - ); - } - }, [defaultValues]); + const clearAuth = useCallback(() => { + if (defaultValues !== undefined) { + resetField("addAuthIds"); + setSelectedAuths( + initialAuths.filter( + (auth) => defaultValues.addAuthIds?.includes(auth.id) + ) + ); + } + }, [defaultValues]); - // Sync with form + // Sync with form useEffect(() => { setValue( "addAuthIds", selectedAuths.map((a) => a.id) ); - setValue( - "removeAuthIds", - removeAuthIds - ); + setValue("removeAuthIds", removeAuthIds); }, [selectedAuths, removeAuthIds, setValue]); - const AuthPoolColumns = useMemo[]>( () => [ { @@ -97,8 +101,7 @@ const AuthAllocation: React.FC = ({ auths }) => { buttonIcon: , }, { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, - // { label: t("Current Position"), name: "currentPosition" }, + { label: t("description"), name: "name" }, ], [addAuth, t] ); @@ -109,10 +112,10 @@ const AuthAllocation: React.FC = ({ auths }) => { label: t("Remove"), name: "id", onClick: removeAuth, - buttonIcon: , + buttonIcon: , }, { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("description"), name: "name" }, ], [removeAuth, selectedAuths, t] ); @@ -128,16 +131,14 @@ const AuthAllocation: React.FC = ({ auths }) => { }, []); 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)) - // }) - // ); + setFilteredAuths( + initialAuths.filter((a) => + ( + a.authority.toLowerCase().includes(query.toLowerCase()) || + a.name?.toLowerCase().includes(query.toLowerCase()) + ) + ) + ); }, [auths, query]); const resetAuth = React.useCallback(() => { @@ -147,16 +148,16 @@ const AuthAllocation: React.FC = ({ auths }) => { const formProps = useForm({}); - // Tab related - const [tabIndex, setTabIndex] = React.useState(0); - const handleTabChange = React.useCallback>( - (_e, newValue) => { - setTabIndex(newValue); - }, - [] - ); + // Tab related + const [tabIndex, setTabIndex] = React.useState(0); + const handleTabChange = React.useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); -return ( + return ( <> @@ -175,7 +176,9 @@ return ( fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t( + "Search by Authority or description or position." + )} InputProps={{ endAdornment: query && ( @@ -191,18 +194,20 @@ return ( - {tabIndex === 0 && ( + {tabIndex === 0 && ( )} - {tabIndex === 1 && ( + {tabIndex === 1 && ( ); - -} -export default AuthAllocation \ No newline at end of file +}; +export default AuthAllocation; diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index bbbd174..947a116 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -1,6 +1,12 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from "react"; import SearchResults, { Column } from "../SearchResults"; // import { TeamResult } from "@/app/api/team"; import { useTranslation } from "react-i18next"; @@ -26,23 +32,24 @@ import { } from "react-hook-form"; import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; import { StaffResult } from "@/app/api/staff"; -import { UserInputs, adminChangePassword, editUser, fetchUserDetails } from "@/app/api/user/actions"; +import { + UserInputs, + adminChangePassword, + editUser, + fetchUserDetails, +} from "@/app/api/user/actions"; import UserDetail from "./UserDetail"; import { UserResult, passwordRule } from "@/app/api/user"; import { auth, fetchAuth } from "@/app/api/group/actions"; import AuthAllocation from "./AuthAllocation"; interface Props { - user: UserResult, - rules: passwordRule, - auths: auth[] -} + user: UserResult; + rules: passwordRule; + auths: auth[]; +} -const EditUser: React.FC = async ({ - user, - rules, - auths -}) => { +const EditUser: React.FC = async ({ user, rules, auths }) => { const { t } = useTranslation(); const formProps = useForm(); const searchParams = useSearchParams(); @@ -50,6 +57,13 @@ const EditUser: React.FC = async ({ const [tabIndex, setTabIndex] = useState(0); const router = useRouter(); const [serverError, setServerError] = useState(""); + const addAuthIds = + auths && auths.length > 0 + ? auths + .filter((item) => item.v === 1) + .map((item) => item.id) + .sort((a, b) => a - b) + : []; const handleTabChange = useCallback>( (_e, newValue) => { @@ -60,22 +74,27 @@ const EditUser: React.FC = async ({ const errors = formProps.formState.errors; - useEffect(() => { + const resetForm = React.useCallback(() => { + console.log("triggerred"); + console.log(addAuthIds); try { - const addAuthIds = auths && auths.length > 0 - ? auths.filter((item) => item.v === 1).map((item) => item.id).sort((a, b) => a - b) - : [] - formProps.reset({ name: user.username, email: user.email, - addAuthIds: addAuthIds + addAuthIds: addAuthIds, + removeAuthIds: [], + password: "", }); + console.log(formProps.formState.defaultValues); } catch (error) { console.log(error); setServerError(t("An error has occurred. Please try again later.")); } - }, [user, auths]); + }, [auths, user]); + + useEffect(() => { + resetForm(); + }, []); const hasErrorsInTab = ( tabIndex: number, @@ -96,22 +115,33 @@ const EditUser: React.FC = async ({ const onSubmit = useCallback>( async (data) => { try { - let haveError = false - let regex_pw = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/ - let pw = '' + let haveError = false; + let regex_pw = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/; + let pw = ""; if (data.password && data.password.length > 0) { - pw = data.password + pw = data.password; if (pw.length < rules.min) { - haveError = true - formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" }) + haveError = true; + formProps.setError("password", { + message: t("The password requires 8-20 characters."), + type: "required", + }); } if (pw.length > rules.max) { - haveError = true - formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" }) + haveError = true; + formProps.setError("password", { + message: t("The password requires 8-20 characters."), + type: "required", + }); } if (!regex_pw.test(pw)) { - haveError = true - formProps.setError("password", { message: "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", type: "required" }) + haveError = true; + formProps.setError("password", { + message: + "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", + type: "required", + }); } } const userData = { @@ -119,16 +149,16 @@ const EditUser: React.FC = async ({ locked: false, addAuthIds: data.addAuthIds || [], removeAuthIds: data.removeAuthIds || [], - } + }; const pwData = { id: id, password: "", - newPassword: pw - } + newPassword: pw, + }; if (haveError) { - return + return; } - console.log("passed") + console.log("passed"); await editUser(id, userData); if (data.password && data.password.length > 0) { await adminChangePassword(pwData); @@ -185,12 +215,12 @@ const EditUser: React.FC = async ({ {tabIndex == 0 && } - {tabIndex === 1 && } + {tabIndex === 1 && } diff --git a/src/components/EditUser/UserDetail.tsx b/src/components/EditUser/UserDetail.tsx index fc2f419..1d251c4 100644 --- a/src/components/EditUser/UserDetail.tsx +++ b/src/components/EditUser/UserDetail.tsx @@ -9,13 +9,13 @@ import { Stack, TextField, Typography, + makeStyles, } from "@mui/material"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; - - const UserDetail: React.FC = () => { + const { t } = useTranslation(); const { register, @@ -45,6 +45,30 @@ const UserDetail: React.FC = () => { label={t("password")} fullWidth {...register("password")} + // helperText={ + // Boolean(errors.password) && + // (errors.password?.message + // ? t(errors.password.message) + // : + // (<> + // - 8-20 characters + //
+ // - Uppercase letters + //
+ // - Lowercase letters + //
+ // - Numbers + //
+ // - Symbols + // ) + // ) + // } + helperText={ + Boolean(errors.password) && + (errors.password?.message + ? t(errors.password.message) + : t("Please input correct password")) + } error={Boolean(errors.password)} />
@@ -55,3 +79,16 @@ const UserDetail: React.FC = () => { }; export default UserDetail; + + +{/* <> + - 8-20 characters +
+ - Uppercase letters +
+ - Lowercase letters +
+ - Numbers +
+ - Symbols + */} \ No newline at end of file From 812dca7afe12bbefaeacf13b88372631e14165f6 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 22 May 2024 16:09:11 +0800 Subject: [PATCH 05/16] update --- .../ResourceOverconsumptionReport.tsx | 17 ----------------- .../ResourceOverconsumptionReport/index.ts | 2 -- 2 files changed, 19 deletions(-) delete mode 100644 src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx delete mode 100644 src/components/Report/ResourceOverconsumptionReport/index.ts diff --git a/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx deleted file mode 100644 index 345b2f2..0000000 --- a/src/components/Report/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx +++ /dev/null @@ -1,17 +0,0 @@ -//src\components\DelayReport\DelayReport.tsx -"use client"; -import * as React from "react"; -import "../../../app/global.css"; -import { Suspense } from "react"; -import ResourceOverconsumptionReportGen from "@/components/Report/ResourceOverconsumptionReportGen"; - -const ResourceOverconsumptionReport: React.FC = () => { - - return ( - }> - - - ); -}; - -export default ResourceOverconsumptionReport; \ No newline at end of file diff --git a/src/components/Report/ResourceOverconsumptionReport/index.ts b/src/components/Report/ResourceOverconsumptionReport/index.ts deleted file mode 100644 index ce20324..0000000 --- a/src/components/Report/ResourceOverconsumptionReport/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//src\components\LateStartReport\index.ts -export { default } from "./ResourceOverconsumptionReport"; From 493ea486119a41914e4e27c4ceea17247f590133 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Wed, 22 May 2024 16:23:04 +0800 Subject: [PATCH 06/16] Update Search Box, fetch report for PandL --- src/app/api/reports/actions.ts | 15 ++++++++++++- src/app/api/reports/index.ts | 16 +++++++++++++- src/components/SearchBox/SearchBox.tsx | 30 ++++++++++++++++++++------ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 3d49a16..ddcab13 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -1,7 +1,7 @@ "use server"; import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; -import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest } from "."; +import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest, ProjectPandLReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; export interface FileResponse { @@ -63,3 +63,16 @@ export const fetchLateStartReport = async (data: LateStartReportRequest) => { return response }; +export const fetchProjectPandLReport = async (data: ProjectPandLReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/projectpandlreport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; + diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index dd1afb8..e633084 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -4,6 +4,18 @@ export interface FinancialStatusReportFilter { project: string[]; } +// - Project P&L Report +export interface ProjectPandLReportFilter { + project: string[]; + startMonth: string; + startMonthTo: string; +} + +export interface ProjectPandLReportRequest { + projectId: number; + dateType: string; +} + // - Project Cash Flow Report export interface ProjectCashFlowReportFilter { project: string[]; @@ -12,9 +24,11 @@ export interface ProjectCashFlowReportFilter { export interface ProjectCashFlowReportRequest { projectId: number; - dateType: string; + startMonth: string; + endMonth: string; } + // - Monthly Work Hours Report export interface MonthlyWorkHoursReportFilter { staff: string[]; diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 686eb8d..cd28040 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -21,7 +21,7 @@ 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 { Box, FormHelperText } from "@mui/material"; import { DateCalendar } from "@mui/x-date-pickers"; interface BaseCriterion { @@ -43,6 +43,7 @@ interface SelectCriterion extends BaseCriterion { interface DateRangeCriterion extends BaseCriterion { type: "dateRange"; + needMonth?: boolean; } interface MonthYearCriterion extends BaseCriterion { @@ -102,9 +103,14 @@ function SearchBox({ }; }, []); - const makeDateChangeHandler = useCallback((paramName: T) => { + const makeDateChangeHandler = useCallback((paramName: T, needMonth?: boolean) => { return (e: any) => { - setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + if(needMonth){ + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") })); + }else{ + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + } + }; }, []); @@ -115,12 +121,19 @@ function SearchBox({ }; }, []); - const makeDateToChangeHandler = useCallback((paramName: T) => { + const makeDateToChangeHandler = useCallback((paramName: T, needMonth?: boolean) => { return (e: any) => { + if(needMonth){ + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM"), + })); + }else{ setInputs((i) => ({ ...i, [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), })); + } }; }, []); @@ -175,6 +188,9 @@ function SearchBox({ adapterLocale="zh-hk" > + + {c.label} + ({ ({ From 1b12a18e3a9e15e81d279a7803d1a72e3a850de8 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Wed, 22 May 2024 16:25:41 +0800 Subject: [PATCH 07/16] Add PandL Report navigation --- .../analytics/ProjectPandLReport/page.tsx | 30 +++++++++ .../GenerateProjectPandLReport.tsx | 63 +++++++++++++++++++ .../GenerateProjectPandLReportLoading.tsx | 38 +++++++++++ .../GenerateProjectPandLReportWrapper.tsx | 18 ++++++ .../GenerateProjectPandLReport/index.ts | 1 + .../NavigationContent/NavigationContent.tsx | 2 +- 6 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/app/(main)/analytics/ProjectPandLReport/page.tsx create mode 100644 src/components/GenerateProjectPandLReport/GenerateProjectPandLReport.tsx create mode 100644 src/components/GenerateProjectPandLReport/GenerateProjectPandLReportLoading.tsx create mode 100644 src/components/GenerateProjectPandLReport/GenerateProjectPandLReportWrapper.tsx create mode 100644 src/components/GenerateProjectPandLReport/index.ts diff --git a/src/app/(main)/analytics/ProjectPandLReport/page.tsx b/src/app/(main)/analytics/ProjectPandLReport/page.tsx new file mode 100644 index 0000000..156cc3d --- /dev/null +++ b/src/app/(main)/analytics/ProjectPandLReport/page.tsx @@ -0,0 +1,30 @@ +import { Metadata } from "next"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateProjectPandLReport from "@/components/GenerateProjectPandLReport"; +import { Typography } from "@mui/material"; + +export const metadata: Metadata = { + title: "Project Cash Flow Report", +}; + +const ProjectPandLReport: React.FC = async () => { + const { t } = await getServerI18n("reports"); + fetchProjects(); + + return ( + <> + + {t("Project P&L Report")} + + + }> + + + + + ); +}; + +export default ProjectPandLReport; diff --git a/src/components/GenerateProjectPandLReport/GenerateProjectPandLReport.tsx b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReport.tsx new file mode 100644 index 0000000..2fb5d63 --- /dev/null +++ b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReport.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { ProjectPandLReportFilter } from "@/app/api/reports"; +import { fetchProjectPandLReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { dateTypeCombo } from "@/app/utils/comboUtil"; +import { FormHelperText } from "@mui/material"; +import { errorDialog, errorDialogWithContent } from "../Swal/CustomAlerts"; + +interface Props { + projects: ProjectResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const GenerateProjectPandLReport: React.FC = ({ projects }) => { + const { t } = useTranslation("report"); + const projectCombo = projects.map(project => `${project.code} - ${project.name}`) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Project *"), paramName: "project", type: "select", options: projectCombo, needAll: false}, + { label: t("Start Month *"), label2: t("End Month *"), paramName: "startMonth", type: "dateRange", needMonth: true }, + + ], + [t], + ); + + return ( + <> + { + + if (query.project.length > 0 && query.project.toLocaleLowerCase() !== "all") { + const projectIndex = projectCombo.findIndex(project => project === query.project) + console.log(projects[projectIndex].id, query.startMonth, query.startMonthTo) + if(projects[projectIndex].id != null && query.startMonth != "" && query.startMonthTo != undefined){ + const response = await fetchProjectPandLReport({ projectId: projects[projectIndex].id, startMonth: query.startMonth, endMonth: query.startMonthTo }) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } + }else{ + errorDialogWithContent(t("Download Fail"), + t(`Please check the required field`), t) + .then(() => { + window.location.reload() + }) + } + } + }} + formType={"download"} + /> + + ); +}; + +export default GenerateProjectPandLReport; \ No newline at end of file diff --git a/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportLoading.tsx b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportLoading.tsx new file mode 100644 index 0000000..04826a4 --- /dev/null +++ b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportLoading.tsx @@ -0,0 +1,38 @@ +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 GenerateProjectPandLReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default GenerateProjectPandLReportLoading; \ No newline at end of file diff --git a/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportWrapper.tsx b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportWrapper.tsx new file mode 100644 index 0000000..00ca669 --- /dev/null +++ b/src/components/GenerateProjectPandLReport/GenerateProjectPandLReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import GenerateProjectPandLReportLoading from "./GenerateProjectPandLReportLoading"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateProjectPandLReport from "./GenerateProjectPandLReport"; + +interface SubComponents { + Loading: typeof GenerateProjectPandLReportLoading; +} + +const GenerateProjectPandLReportWrapper: React.FC & SubComponents = async () => { + const projects = await fetchProjects(); + + return ; +}; + +GenerateProjectPandLReportWrapper.Loading = GenerateProjectPandLReportLoading; + +export default GenerateProjectPandLReportWrapper; \ No newline at end of file diff --git a/src/components/GenerateProjectPandLReport/index.ts b/src/components/GenerateProjectPandLReport/index.ts new file mode 100644 index 0000000..b56feba --- /dev/null +++ b/src/components/GenerateProjectPandLReport/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateProjectPandLReportWrapper"; \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index a50b3a0..0ed436c 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -175,7 +175,7 @@ const NavigationContent: React.FC = ({ abilities }) => { { icon: , label: "Project P&L Report", - path: "/analytics/ProjectPLReport", + path: "/analytics/ProjectPandLReport", }, { icon: , From fd25f5de03f4b2fbc41c30cca36062db8442056e Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 22 May 2024 17:11:05 +0800 Subject: [PATCH 08/16] update project --- .../{create/sub => createSub}/not-found.tsx | 0 .../{create/sub => createSub}/page.tsx | 0 .../{edit/sub => editSub}/not-found.tsx | 0 .../projects/{edit/sub => editSub}/page.tsx | 0 src/app/(main)/projects/page.tsx | 2 +- src/components/Breadcrumb/Breadcrumb.tsx | 4 ++-- .../ControlledAutoComplete.tsx | 20 +++++++++---------- src/components/CreateProject/Milestone.tsx | 10 ++++++++++ .../CreateProject/ProjectClientDetails.tsx | 8 ++++++-- .../ProjectSearch/ProjectSearch.tsx | 2 +- 10 files changed, 30 insertions(+), 16 deletions(-) rename src/app/(main)/projects/{create/sub => createSub}/not-found.tsx (100%) rename src/app/(main)/projects/{create/sub => createSub}/page.tsx (100%) rename src/app/(main)/projects/{edit/sub => editSub}/not-found.tsx (100%) rename src/app/(main)/projects/{edit/sub => editSub}/page.tsx (100%) diff --git a/src/app/(main)/projects/create/sub/not-found.tsx b/src/app/(main)/projects/createSub/not-found.tsx similarity index 100% rename from src/app/(main)/projects/create/sub/not-found.tsx rename to src/app/(main)/projects/createSub/not-found.tsx diff --git a/src/app/(main)/projects/create/sub/page.tsx b/src/app/(main)/projects/createSub/page.tsx similarity index 100% rename from src/app/(main)/projects/create/sub/page.tsx rename to src/app/(main)/projects/createSub/page.tsx diff --git a/src/app/(main)/projects/edit/sub/not-found.tsx b/src/app/(main)/projects/editSub/not-found.tsx similarity index 100% rename from src/app/(main)/projects/edit/sub/not-found.tsx rename to src/app/(main)/projects/editSub/not-found.tsx diff --git a/src/app/(main)/projects/edit/sub/page.tsx b/src/app/(main)/projects/editSub/page.tsx similarity index 100% rename from src/app/(main)/projects/edit/sub/page.tsx rename to src/app/(main)/projects/editSub/page.tsx diff --git a/src/app/(main)/projects/page.tsx b/src/app/(main)/projects/page.tsx index 129d601..7cf44d0 100644 --- a/src/app/(main)/projects/page.tsx +++ b/src/app/(main)/projects/page.tsx @@ -43,7 +43,7 @@ const Projects: React.FC = async () => { color="secondary" startIcon={} LinkComponent={Link} - href="/projects/create/sub" + href="/projects/createSub" > {t("Create Sub Project")} } diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 8c1b9b0..73f41f6 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -15,9 +15,9 @@ const pathToLabelMap: { [path: string]: string } = { "/home": "User Workspace", "/projects": "Projects", "/projects/create": "Create Project", - "/projects/create/sub": "Sub Project", + "/projects/createSub": "Sub Project", "/projects/edit": "Edit Project", - "/projects/edit/sub": "Sub Project", + "/projects/editSub": "Sub Project", "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/staffReimbursement": "Staff Reimbursement", diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx index 36e7542..ed7e81b 100644 --- a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx +++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx @@ -72,11 +72,11 @@ function ControlledAutoComplete< ); }} - // renderTags={(tagValue, getTagProps) => { - // return tagValue.map((option, index) => ( - // - // )) - // }} + renderTags={(tagValue, getTagProps) => { + return tagValue.map((option, index) => ( + + )) + }} onChange={(event, value) => { field.onChange(value?.map(v => v.id)) }} @@ -99,11 +99,11 @@ function ControlledAutoComplete< ); }} - // renderTags={(tagValue, getTagProps) => { - // return tagValue.map((option, index) => ( - // - // )) - // }} + renderTags={(tagValue, getTagProps) => { + return tagValue.map((option, index) => ( + + )) + }} onChange={(event, value) => { field.onChange(value?.id ?? null) }} diff --git a/src/components/CreateProject/Milestone.tsx b/src/components/CreateProject/Milestone.tsx index f2e62ce..d854af9 100644 --- a/src/components/CreateProject/Milestone.tsx +++ b/src/components/CreateProject/Milestone.tsx @@ -50,6 +50,16 @@ const Milestone: React.FC = ({ allTasks, isActive }) => { const [currentTaskGroupId, setCurrentTaskGroupId] = useState( taskGroups[0].id, ); + + /*const onSelectTaskGroup = useCallback( + (event: SelectChangeEvent) => { + const id = event.target.value; + const newTaksGroupId = typeof id === "string" ? parseInt(id) : id; + setCurrentTaskGroupId(newTaksGroupId); + }, + [], + );*/ + const onSelectTaskGroup = useCallback( (event: SyntheticEvent, value: NonNullable) => { const id = value.id; diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 9e4ac7f..bce421a 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -113,6 +113,7 @@ const ProjectClientDetails: React.FC = ({ // get customer (client) contact combo const [firstCustomerLoaded, setFirstCustomerLoaded] = useState(false) + const [isMainProjectInfoLoading, setIsMainProjectInfoLoading] = useState(false) useEffect(() => { if (selectedCustomerId !== undefined) { fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => { @@ -120,8 +121,10 @@ const ProjectClientDetails: React.FC = ({ setCustomerSubsidiaryIds(subsidiaryIds); // if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0]) - // else - if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } else setValue("clientSubsidiaryId", null) + // else + if (isEditMode && !firstCustomerLoaded) { setFirstCustomerLoaded(true) } + else if (!isEditMode && isMainProjectInfoLoading) { setIsMainProjectInfoLoading(false) } + else setValue("clientSubsidiaryId", null) // if (contacts.length > 0) setValue("clientContactId", contacts[0].id) // else setValue("clientContactId", undefined) }); @@ -160,6 +163,7 @@ const ProjectClientDetails: React.FC = ({ const mainProject = mainProjects.find(project => project.projectId === mainProjectId); if (mainProject !== undefined) { + setIsMainProjectInfoLoading(() => true) setValue("projectName", mainProject.projectName) setValue("projectCategoryId", mainProject.projectCategoryId) setValue("projectLeadId", mainProject.projectLeadId) diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index bbb1d67..6456902 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -62,7 +62,7 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const onProjectClick = useCallback( (project: ProjectResult) => { if (Boolean(project.mainProject)) { - router.push(`/projects/edit/sub?id=${project.id}`); + router.push(`/projects/editSub?id=${project.id}`); } else router.push(`/projects/edit?id=${project.id}`); }, [router], From cf3a51860a31cd51955fb7fc364fe0961b834d9b Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 22 May 2024 17:15:55 +0800 Subject: [PATCH 09/16] update report --- src/app/api/reports/actions.ts | 2 +- src/app/api/reports/index.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 3b94fca..ff874ba 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -1,7 +1,7 @@ "use server"; import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; -import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest } from "."; +import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; export interface FileResponse { diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 491797a..748561d 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -24,8 +24,7 @@ export interface ProjectCashFlowReportFilter { export interface ProjectCashFlowReportRequest { projectId: number; - startMonth: string; - endMonth: string; + dateType: string; } From e09a59af33cf5ea29f1215aba94228893cec5d62 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Wed, 22 May 2024 17:24:08 +0800 Subject: [PATCH 10/16] fix --- src/app/api/reports/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 748561d..0691005 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -13,7 +13,8 @@ export interface ProjectPandLReportFilter { export interface ProjectPandLReportRequest { projectId: number; - dateType: string; + startMonth: string; + endMonth: string; } // - Project Cash Flow Report From 6e20b03c6d3cbc0742f2e5c0032fbbd51d7bdf0f Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Wed, 22 May 2024 18:25:32 +0800 Subject: [PATCH 11/16] use financial summary api --- src/app/api/clientprojects/index.ts | 62 ------ src/app/api/financialsummary/actions.ts | 54 +++++ src/app/api/financialsummary/index.ts | 27 +++ src/app/api/teamprojects/actions.ts | 29 +++ src/app/api/teamprojects/index.ts | 35 +--- .../ProgressByClient/ProgressByClient.tsx | 124 ------------ .../ProgressByTeam/ProgressByTeam.tsx | 108 ++++++++-- .../ProgressByTeamSearch.tsx | 36 +++- .../ProjectFinancialCard.tsx | 26 +-- .../ProjectFinancialSummary.tsx | 186 +++++++----------- 10 files changed, 324 insertions(+), 363 deletions(-) create mode 100644 src/app/api/financialsummary/actions.ts create mode 100644 src/app/api/financialsummary/index.ts create mode 100644 src/app/api/teamprojects/actions.ts diff --git a/src/app/api/clientprojects/index.ts b/src/app/api/clientprojects/index.ts index 56065d2..e1130a5 100644 --- a/src/app/api/clientprojects/index.ts +++ b/src/app/api/clientprojects/index.ts @@ -15,72 +15,10 @@ export interface ClientProjectResult { projectNo: number; } -// export interface ClientSubsidiaryProjectResult { -// projectId: number; -// projectCode: string; -// projectName: string; -// team: string; -// teamLead: string; -// expectedStage: string; -// budgetedManhour: number; -// spentManhour: number; -// remainedManhour: number; -// manhourConsumptionPercentage: number; -// comingPaymentMilestone: string; -// } - export const preloadClientProjects = () => { fetchAllClientProjects(); }; -// export const fetchClientProjects = cache(async () => { -// return mockProjects; -// }); - export const fetchAllClientProjects = cache(async () => { return serverFetchJson(`${BASE_API_URL}/dashboard/searchCustomerSubsidiary`); }); - -// export const fetchAllClientSubsidiaryProjects = cache(async (customerId: number, subsidiaryId: number) => { -// return serverFetchJson( -// `${BASE_API_URL}/dashboard/searchCustomerSubsidiaryProject/?${customerId}&${subsidiaryId}`, -// { -// next: { tags: [`allClientSubsidiaryProjects`] }, -// }, -// ); -// }); - -// const mockProjects: ClientProjectResult[] = [ -// { -// id: 1, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "N/A", -// SubsidiaryClientName: "N/A", -// NoOfProjects: 5, -// }, -// { -// id: 2, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "SUBS-001", -// SubsidiaryClientName: "Subsidiary A", -// NoOfProjects: 5, -// }, -// { -// id: 3, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "SUBS-002", -// SubsidiaryClientName: "Subsidiary B", -// NoOfProjects: 3, -// }, -// { -// id: 4, -// clientCode: "CUST-001", -// clientName: "Client A", -// SubsidiaryClientCode: "SUBS-003", -// SubsidiaryClientName: "Subsidiary C", -// NoOfProjects: 1, -// }, -// ]; diff --git a/src/app/api/financialsummary/actions.ts b/src/app/api/financialsummary/actions.ts new file mode 100644 index 0000000..bb5b82b --- /dev/null +++ b/src/app/api/financialsummary/actions.ts @@ -0,0 +1,54 @@ +"use server"; + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { Dayjs } from "dayjs"; +import { cache } from "react"; + + +export interface FinancialSummaryByClientResult { + teamId:number; + id:number; + customerCode: string; + customerName: string; + projectNo: number; + totalFee: number; + totalBudget: number; + cumulativeExpenditure: number; + totalInvoiced: number; + totalReceived: number; + cashFlowStatus: string; + cpi: number; + totalUninvoiced: number; +} + +export interface FinancialSummaryByProjectResult { + teamId:number; + id:number; + projectCode: string; + projectName: string; + customerName: string; + projectNo: number; + totalFee: number; + totalBudget: number; + cumulativeExpenditure: number; + totalInvoiced: number; + totalReceived: number; + cashFlowStatus: string; + cpi: number; + totalUninvoiced: number; +} + +export const searchFinancialSummaryByClient = cache(async (teamId: number) => { + + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByClient?teamId=${teamId}` + ); +}); + +export const searchFinancialSummaryByProject = cache(async (teamId: number) => { + + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByProject?teamId=${teamId}` + ); +}); diff --git a/src/app/api/financialsummary/index.ts b/src/app/api/financialsummary/index.ts new file mode 100644 index 0000000..7e9d78a --- /dev/null +++ b/src/app/api/financialsummary/index.ts @@ -0,0 +1,27 @@ +"use server"; +import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +// import "server-only"; + + +export interface FinancialSummaryCardResult { + teamId: number; + teamName: string; + projectNo: number; + totalFee: number; + totalBudget: number; + cumulativeExpenditure: number; + totalInvoiced: number; + totalReceived: number; + cashFlowStatus: string; + cpi: number; +} + +export const preloadFinancialSummaryCard = () => { + fetchFinancialSummaryCard(); +}; + +export const fetchFinancialSummaryCard = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/dashboard/searchFinancialSummaryCard`); +}); diff --git a/src/app/api/teamprojects/actions.ts b/src/app/api/teamprojects/actions.ts new file mode 100644 index 0000000..2460a6b --- /dev/null +++ b/src/app/api/teamprojects/actions.ts @@ -0,0 +1,29 @@ +"use server"; + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { Dayjs } from "dayjs"; +import { cache } from "react"; + + +export interface ClientSubsidiaryProjectResult { + color: string; + projectId: number; + projectCode: string; + projectName: string; + team: string; + teamLead: string; + expectedStage: string; + budgetedManhour: number; + spentManhour: number; + remainedManhour: number; + manhourConsumptionPercentage: number; + comingPaymentMilestone: string; +} + +export const fetchAllTeamProjects = cache(async (teamLeadId: number) => { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchTeamProject?teamLeadId=${teamLeadId}` + ); + +}); diff --git a/src/app/api/teamprojects/index.ts b/src/app/api/teamprojects/index.ts index bba7ba9..59b2a3e 100644 --- a/src/app/api/teamprojects/index.ts +++ b/src/app/api/teamprojects/index.ts @@ -1,10 +1,15 @@ import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import "server-only"; export interface TeamProjectResult { id: number; + teamId: number; + teamLeadId: number; teamCode: string; teamName: string; - NoOfProjects: number; + projectNo: number; } export const preloadProjects = () => { @@ -12,32 +17,6 @@ export const preloadProjects = () => { }; export const fetchTeamProjects = cache(async () => { - return mockProjects; + return serverFetchJson(`${BASE_API_URL}/dashboard/searchTeamProjectNo`); }); -const mockProjects: TeamProjectResult[] = [ - { - id: 1, - teamCode: "TEAM-001", - teamName: "Team A", - NoOfProjects: 5, - }, - { - id: 2, - teamCode: "TEAM-002", - teamName: "Team B", - NoOfProjects: 5, - }, - { - id: 3, - teamCode: "TEAM-003", - teamName: "Team C", - NoOfProjects: 3, - }, - { - id: 4, - teamCode: "TEAM-004", - teamName: "Team D", - NoOfProjects: 1, - }, -]; diff --git a/src/components/ProgressByClient/ProgressByClient.tsx b/src/components/ProgressByClient/ProgressByClient.tsx index 3d4ecd2..8961472 100644 --- a/src/components/ProgressByClient/ProgressByClient.tsx +++ b/src/components/ProgressByClient/ProgressByClient.tsx @@ -303,130 +303,6 @@ const ProgressByClient: React.FC = () => { flex:0.1 }, ]; - const optionstest: ApexOptions = { - chart: { - height: 350, - type: "line", - }, - stroke: { - width: [0, 0, 2, 2], - }, - plotOptions: { - bar: { - horizontal: false, - distributed: false, - }, - }, - dataLabels: { - enabled: false, - }, - xaxis: { - categories: [ - "Q1", - "Q2", - "Q3", - "Q4", - "Q5", - "Q6", - "Q7", - "Q8", - "Q9", - "Q10", - "Q11", - "Q12", - ], - }, - yaxis: [ - { - title: { - text: "Monthly Income and Expenditure(HKD)", - }, - min: 0, - max: 350000, - tickAmount: 5, - labels: { - formatter: function (val) { - return val.toLocaleString() - } - } - }, - { - show: false, - seriesName: "Monthly_Expenditure", - title: { - text: "Monthly Expenditure (HKD)", - }, - min: 0, - max: 350000, - tickAmount: 5, - }, - { - seriesName: "Cumulative_Income", - opposite: true, - title: { - text: "Cumulative Income and Expenditure(HKD)", - }, - min: 0, - max: 850000, - tickAmount: 5, - labels: { - formatter: function (val) { - return val.toLocaleString() - } - } - }, - { - show: false, - seriesName: "Cumulative_Expenditure", - opposite: true, - title: { - text: "Cumulative Expenditure (HKD)", - }, - min: 0, - max: 850000, - tickAmount: 5, - }, - ], - grid: { - borderColor: "#f1f1f1", - }, - annotations: {}, - series: [ - { - name: "Monthly_Income", - type: "column", - color: "#ffde91", - data: [0, 110000, 0, 0, 185000, 0, 0, 189000, 0, 0, 300000, 0], - }, - { - name: "Monthly_Expenditure", - type: "column", - color: "#82b59a", - data: [ - 0, 160000, 120000, 120000, 55000, 55000, 55000, 55000, 55000, 70000, - 55000, 55000, - ], - }, - { - name: "Cumulative_Income", - type: "line", - color: "#EE6D7A", - data: [ - 0, 100000, 100000, 100000, 300000, 300000, 300000, 500000, 500000, - 500000, 800000, 800000, - ], - }, - { - name: "Cumulative_Expenditure", - type: "line", - color: "#7cd3f2", - data: [ - 0, 198000, 240000, 400000, 410000, 430000, 510000, 580000, 600000, - 710000, 730000, 790000, - ], - }, - ], - }; const options2: ApexOptions = { chart: { diff --git a/src/components/ProgressByTeam/ProgressByTeam.tsx b/src/components/ProgressByTeam/ProgressByTeam.tsx index fcb351a..7a573c9 100644 --- a/src/components/ProgressByTeam/ProgressByTeam.tsx +++ b/src/components/ProgressByTeam/ProgressByTeam.tsx @@ -18,9 +18,13 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByTeamSearch from "@/components/ProgressByTeamSearch"; import { Suspense } from "react"; +import { useSearchParams } from 'next/navigation'; +import { fetchAllTeamProjects} from "@/app/api/teamprojects/actions"; // const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByTeam: React.FC = () => { + const searchParams = useSearchParams(); + const teamLeadId = searchParams.get('teamLeadId'); const [activeTab, setActiveTab] = useState("financialSummary"); const [SearchCriteria, setSearchCriteria] = React.useState({}); const { t } = useTranslation("dashboard"); @@ -44,6 +48,64 @@ const ProgressByTeam: React.FC = () => { const [receiptFromDate, setReceiptFromDate] = useState(null); const [receiptToDate, setReceiptToDate] = useState(null); const [selectedRows, setSelectedRows] = useState([]); + const [chartProjectName, setChartProjectName]:any[] = useState([]); + const [chartManhourConsumptionPercentage, setChartManhourConsumptionPercentage]:any[] = useState([]); + const color = ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b", + "#f58a9b", "#8ef4d1", "#92caf9", "#a798f9", "#fad287", + "#f595a6", "#88f1cc", "#9dcff5", "#a39bf5", "#f8de83", + "#f5a0b1", "#82eec7", "#a8d4f1", "#9f9ef1", "#f6ea7f", + "#f5abb4", "#7cebca", "#b3d9ed", "#9ba1ed", "#f4f67b", + "#f5b6b7", "#76e8cd", "#bed6e9", "#97a4e9", "#f2fa77", + "#f5c1ba", "#70e5d0", "#c9d3e5", "#93a7e5", "#f0fe73", + "#f5ccbd", "#6ae2d3", "#d4d0e1", "#8faae1", "#eefe6f", + "#f5d7c0", "#64dfd6", "#dfc5dd", "#8badd5", "#ecfe6b", + "#f5e2c3", "#5edcd9", "#eabada", "#87b0c9", "#eafc67", + "#f5edc6", "#58d9dc", "#f5afd6", "#83b3bd", "#e8fc63", + "#f5f8c9", "#52d6df", "#ffacd2", "#7fb6b1", "#e6fc5f", + "#f5ffcc", "#4cd3e2", "#ffa9ce", "#7bb9a5", "#e4fc5b", + "#f2ffcf", "#46d0e5", "#ffa6ca", "#77bc99", "#e2fc57", + "#efffd2", "#40cde8", "#ffa3c6", "#73bf8d", "#e0fc53", + "#ecffd5", "#3acaeb", "#ffa0c2", "#6fc281", "#defb4f", + "#e9ffd8", "#34c7ee", "#ff9dbe", "#6bc575", "#dcfb4b", + "#e6ffdb", "#2ec4f1", "#ff9aba", "#67c869", "#dafb47", + "#e3ffde", "#28c1f4", "#ff97b6", "#63cb5d", "#d8fb43", + "#e0ffe1", "#22bef7", "#ff94b2", "#5fce51", "#d6fb3f", + "#ddfee4", "#1cbbfa", "#ff91ae", "#5bd145", "#d4fb3b", + "#dafee7", "#16b8fd", "#ff8eaa", "#57d439", "#d2fb37", + "#d7feea", "#10b5ff", "#ff8ba6", "#53d72d", "#d0fb33", + "#d4feed", "#0ab2ff", "#ff88a2", "#4fda21", "#cefb2f", + "#d1fef0", "#04afff", "#ff859e", "#4bdd15", "#ccfb2b"]; + const [teamProjectResult, setTeamProjectResult]:any[] = useState([]); + + const fetchData = async () => { + if (teamLeadId) { + try { + const clickResult = await fetchAllTeamProjects( + Number(teamLeadId)) + console.log(clickResult) + setTeamProjectResult(clickResult); + } catch (error) { + console.error('Error fetching team projects:', error); + } + } + } + + useEffect(() => { + const projectName = [] + const manhourConsumptionPercentage = [] + for (let i = 0; i < teamProjectResult.length; i++){ + teamProjectResult[i].color = color[i] + projectName.push(teamProjectResult[i].projectName) + manhourConsumptionPercentage.push(teamProjectResult[i].manhourConsumptionPercentage) + } + setChartProjectName(projectName) + setChartManhourConsumptionPercentage(manhourConsumptionPercentage) + }, [teamProjectResult]); + + useEffect(() => { + fetchData() + }, [teamLeadId]); + const rows = [ { id: 1, @@ -373,7 +435,35 @@ const ProgressByTeam: React.FC = () => { type: "bar", height: 350, }, - colors: ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b"], + series: [{ + name: "Project Resource Consumption Percentage", + data: chartManhourConsumptionPercentage, + },], + colors: ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b", + "#f58a9b", "#8ef4d1", "#92caf9", "#a798f9", "#fad287", + "#f595a6", "#88f1cc", "#9dcff5", "#a39bf5", "#f8de83", + "#f5a0b1", "#82eec7", "#a8d4f1", "#9f9ef1", "#f6ea7f", + "#f5abb4", "#7cebca", "#b3d9ed", "#9ba1ed", "#f4f67b", + "#f5b6b7", "#76e8cd", "#bed6e9", "#97a4e9", "#f2fa77", + "#f5c1ba", "#70e5d0", "#c9d3e5", "#93a7e5", "#f0fe73", + "#f5ccbd", "#6ae2d3", "#d4d0e1", "#8faae1", "#eefe6f", + "#f5d7c0", "#64dfd6", "#dfc5dd", "#8badd5", "#ecfe6b", + "#f5e2c3", "#5edcd9", "#eabada", "#87b0c9", "#eafc67", + "#f5edc6", "#58d9dc", "#f5afd6", "#83b3bd", "#e8fc63", + "#f5f8c9", "#52d6df", "#ffacd2", "#7fb6b1", "#e6fc5f", + "#f5ffcc", "#4cd3e2", "#ffa9ce", "#7bb9a5", "#e4fc5b", + "#f2ffcf", "#46d0e5", "#ffa6ca", "#77bc99", "#e2fc57", + "#efffd2", "#40cde8", "#ffa3c6", "#73bf8d", "#e0fc53", + "#ecffd5", "#3acaeb", "#ffa0c2", "#6fc281", "#defb4f", + "#e9ffd8", "#34c7ee", "#ff9dbe", "#6bc575", "#dcfb4b", + "#e6ffdb", "#2ec4f1", "#ff9aba", "#67c869", "#dafb47", + "#e3ffde", "#28c1f4", "#ff97b6", "#63cb5d", "#d8fb43", + "#e0ffe1", "#22bef7", "#ff94b2", "#5fce51", "#d6fb3f", + "#ddfee4", "#1cbbfa", "#ff91ae", "#5bd145", "#d4fb3b", + "#dafee7", "#16b8fd", "#ff8eaa", "#57d439", "#d2fb37", + "#d7feea", "#10b5ff", "#ff8ba6", "#53d72d", "#d0fb33", + "#d4feed", "#0ab2ff", "#ff88a2", "#4fda21", "#cefb2f", + "#d1fef0", "#04afff", "#ff859e", "#4bdd15", "#ccfb2b"], plotOptions: { bar: { horizontal: true, @@ -384,13 +474,7 @@ const ProgressByTeam: React.FC = () => { enabled: false, }, xaxis: { - categories: [ - "Consultancy Project 123", - "Consultancy Project 456", - "Construction Project A", - "Construction Project B", - "Construction Project C", - ], + categories: chartProjectName, }, yaxis: { title: { @@ -414,7 +498,7 @@ const ProgressByTeam: React.FC = () => { }; const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { - const selectedRowsData = rows2.filter((row) => + const selectedRowsData = teamProjectResult.filter((row:any) => newSelectionModel.includes(row.id), ); console.log(selectedRowsData); @@ -427,7 +511,7 @@ const ProgressByTeam: React.FC = () => { if (i === selectedRowsData.length && i > 0) { projectArray.push("Remained"); } else if (selectedRowsData.length > 0) { - projectArray.push(selectedRowsData[i].project); + projectArray.push(selectedRowsData[i].projectName); totalBudgetManhour += Number(selectedRowsData[i].budgetedManhour); totalSpent += Number(selectedRowsData[i].spentManhour); pieChartColorArray.push(selectedRowsData[i].color); @@ -485,7 +569,7 @@ const ProgressByTeam: React.FC = () => {
@@ -507,7 +591,7 @@ const ProgressByTeam: React.FC = () => { style={{ display: "inline-block", width: "99%", marginLeft: 10 }} > = ({ projects }) => { const { t } = useTranslation("projects"); - + const router = useRouter(); // If project searching is done on the server-side, then no need for this. const [filteredProjects, setFilteredProjects] = useState(projects); @@ -27,13 +29,31 @@ const ProgressByClientSearch: React.FC = ({ projects }) => { [t], ); + const onTaskClick = useCallback(async (teamProjectResult: TeamProjectResult) => { + try { + console.log(teamProjectResult) + router.push( + `/dashboard/ProjectStatusByTeam?teamLeadId=${teamProjectResult.teamLeadId}` + ); + } catch (error) { + console.error('Error fetching team projects:', error); + } + }, []); + + const columns = useMemo[]>( () => [ + { + name: "id", + label: t("Details"), + onClick: onTaskClick, + buttonIcon: , + }, { name: "teamCode", label: t("Team Code") }, { name: "teamName", label: t("Team Name") }, - { name: "NoOfProjects", label: t("No. of Projects") }, + { name: "projectNo", label: t("No. of Projects") }, ], - [t], + [onTaskClick, t], ); return ( @@ -41,7 +61,13 @@ const ProgressByClientSearch: React.FC = ({ projects }) => { { - console.log(query); + setFilteredProjects( + projects.filter( + (cp) => + cp.teamCode.toLowerCase().includes(query.teamCode.toLowerCase()) && + cp.teamName.toLowerCase().includes(query.teamName.toLowerCase()) + ), + ); }} /> diff --git a/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx b/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx index 25ff90e..859163d 100644 --- a/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx +++ b/src/components/ProjectFinancialSummary/ProjectFinancialCard.tsx @@ -20,14 +20,14 @@ import { Suspense } from "react"; interface Props { Title: string; - TotalActiveProjectNumber: string; - TotalFees: string; - TotalBudget: string; - TotalCumulative: string; - TotalInvoicedAmount: string; - TotalReceivedAmount: string; + TotalActiveProjectNumber: number; + TotalFees: number; + TotalBudget: number; + TotalCumulative: number; + TotalInvoicedAmount: number; + TotalReceivedAmount: number; CashFlowStatus: string; - CostPerformanceIndex: string; + CostPerformanceIndex: number; ClickedIndex: number; Index: number; } @@ -77,42 +77,42 @@ const ProjectFinancialCard: React.FC = ({ Total Active Project
- {TotalActiveProjectNumber} + {TotalActiveProjectNumber.toLocaleString()}

Total Fees
- {TotalFees} + {TotalFees.toLocaleString()}

Total Budget
- {TotalBudget} + {TotalBudget.toLocaleString()}

Total Cumulative Expenditure
- {TotalCumulative} + {TotalCumulative.toLocaleString()}

Total Invoiced Amount
- {TotalInvoicedAmount} + {TotalInvoicedAmount.toLocaleString()}

Total Received Amount
- {TotalReceivedAmount} + {TotalReceivedAmount.toLocaleString()}

diff --git a/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx b/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx index e0a2764..eafaba4 100644 --- a/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx +++ b/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx @@ -18,86 +18,32 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; +import { fetchFinancialSummaryCard } from "@/app/api/financialsummary"; +import { searchFinancialSummaryByClient,searchFinancialSummaryByProject } from "@/app/api/financialsummary/actions"; import ProjectFinancialCard from "./ProjectFinancialCard"; const ProjectFinancialSummary: React.FC = () => { const [SearchCriteria, setSearchCriteria] = React.useState({}); const { t } = useTranslation("dashboard"); const [selectionModel, setSelectionModel]: any[] = React.useState([]); - const projectFinancialData = [ - { - id: 1, - title: "All Teams", - activeProject: "147", - fees: "22,800,000.00", - budget: "18,240,000.00", - cumulativeExpenditure: "17,950,000.00", - invoicedAmount: "18,240,000.00", - receivedAmount: "10,900,000.00", - cashFlowStatus: "Negative", - CPI: "0.69", - }, - { - id: 2, - title: "XXX Team", - activeProject: "25", - fees: "1,500,000.00", - budget: "1,200,000.00", - cumulativeExpenditure: "1,250,000.00", - invoicedAmount: "900,000.00", - receivedAmount: "650,000.00", - cashFlowStatus: "Negative", - CPI: "0.72", - }, - { - id: 3, - title: "YYY Team", - activeProject: "35", - fees: "5,000,000.00", - budget: "4,000,000.00", - cumulativeExpenditure: "3,200,000.00", - invoicedAmount: "3,500,000.00", - receivedAmount: "3,500,000.00", - cashFlowStatus: "Positive", - CPI: "1.09", - }, - { - id: 4, - title: "ZZZ Team", - activeProject: "50", - fees: "3,500,000.00", - budget: "2,800,000.00", - cumulativeExpenditure: "5,600,000.00", - invoicedAmount: "2,500,000.00", - receivedAmount: "2,200,000.00", - cashFlowStatus: "Negative", - CPI: "0.45", - }, - { - id: 5, - title: "AAA Team", - activeProject: "15", - fees: "4,800,000.00", - budget: "3,840,000.00", - cumulativeExpenditure: "2,500,000.00", - invoicedAmount: "1,500,000.00", - receivedAmount: "750,000.00", - cashFlowStatus: "Negative", - CPI: "0.60", - }, - { - id: 6, - title: "BBB Team", - activeProject: "22", - fees: "8,000,000.00", - budget: "6,400,000.00", - cumulativeExpenditure: "5,400,000.00", - invoicedAmount: "4,000,000.00", - receivedAmount: "3,800,000.00", - cashFlowStatus: "Negative", - CPI: "0.74", - }, - ]; + const [projectFinancialData, setProjectFinancialData]: any[] = React.useState([]); + const [clientFinancialRows, setClientFinancialRows]: any[] = React.useState([]); + const [projectFinancialRows, setProjectFinancialRows]: any[] = React.useState([]); + const fetchData = async () => { + const financialSummaryCard = await fetchFinancialSummaryCard(); + setProjectFinancialData(financialSummaryCard) + } + const fetchTableData = async (teamId:any) => { + const financialSummaryByClient = await searchFinancialSummaryByClient(teamId); + const financialSummaryByProject = await searchFinancialSummaryByProject(teamId); + console.log(financialSummaryByClient) + console.log(financialSummaryByProject) + setClientFinancialRows(financialSummaryByClient) + setProjectFinancialRows(financialSummaryByProject) + } + useEffect(() => { + fetchData() + }, []); const rows0 = [{id: 1,projectCode:"M1201",projectName:"Consultancy Project C", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "01/05/2024", client:"Client A", subsidiary:"N/A"}, {id: 2,projectCode:"M1321",projectName:"Consultancy Project CCC", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "20/01/2024", client:"Client E", subsidiary:"Subsidiary B"}, @@ -115,45 +61,46 @@ const ProjectFinancialSummary: React.FC = () => { {id: 5,projectCode:"M1354",projectName:"Consultancy Project BBB", team:"YYY", teamLeader:"YYY", startDate:"01/02/2023", targetEndDate: "31/01/2024", client:"Client D", subsidiary:"Subsidiary C"} ] - const projectFinancialRows = [{id: 1,projectCode:"M1354",projectName:"Consultanct Project BBB",clientName:"Client D",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} - ] + // const projectFinancialRows = [{id: 1,projectCode:"M1354",projectName:"Consultanct Project BBB",clientName:"Client D",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} + // ] - const clientFinancialRows =[{id: 1,clientCode:"Cust-02",clientName:"Client B",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, - {id: 2,clientCode:"Cust-03",clientName:"Client C",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, - {id: 3,clientCode:"Cust-04",clientName:"Client D",totalProjectInvolved:"4",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} - ] + // const clientFinancialRows =[{id: 1,clientCode:"Cust-02",clientName:"Client B",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, + // {id: 2,clientCode:"Cust-03",clientName:"Client C",totalProjectInvolved:"1",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"}, + // {id: 3,clientCode:"Cust-04",clientName:"Client D",totalProjectInvolved:"4",cashFlowStatus:"Positive",cpi:"1.25", totalFees:"500,000.00", totalBudget:"400,000.00", totalCumulativeExpenditure:"280,000.00", totalInvoicedAmount: "350,000.00", totalUnInvoicedAmount:"150,000.00", totalReceivedAmount:"350,000.00"} + // ] const [isCardClickedIndex, setIsCardClickedIndex] = React.useState(0); const [selectedTeamData, setSelectedTeamData]: any[] = React.useState(rows0); const handleCardClick = (r: any) => { - setIsCardClickedIndex(r); - if (r === 0) { - setSelectedTeamData(rows0); - } else if (r === 1) { - setSelectedTeamData(rows1); - } else if (r === 2) { - setSelectedTeamData(rows2); - } + fetchTableData(r.teamId) + // setIsCardClickedIndex(r); + // if (r === 0) { + // setSelectedTeamData(rows0); + // } else if (r === 1) { + // setSelectedTeamData(rows1); + // } else if (r === 2) { + // setSelectedTeamData(rows2); + // } }; const columns = [ { - id: 'clientCode', - field: 'clientCode', + id: 'customerCode', + field: 'customerCode', headerName: "Client Code", flex: 0.7, }, { - id: 'clientName', - field: 'clientName', + id: 'customerName', + field: 'customerName', headerName: "Client Name", flex: 1, }, { - id: 'totalProjectInvolved', - field: 'totalProjectInvolved', + id: 'projectNo', + field: 'projectNo', headerName: "Total Project Involved", flex: 1, }, @@ -163,6 +110,7 @@ const ProjectFinancialSummary: React.FC = () => { headerName: "Cash Flow Status", flex: 1, renderCell: (params:any) => { + console.log(params.row) if (params.row.cashFlowStatus === "Positive") { return ( {params.row.cashFlowStatus} @@ -192,13 +140,13 @@ const ProjectFinancialSummary: React.FC = () => { }, }, { - id: 'totalFees', - field: 'totalFees', + id: 'totalFee', + field: 'totalFee', headerName: "Total Fees (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalFees} + ${params.row.totalFee} ) }, }, @@ -214,46 +162,46 @@ const ProjectFinancialSummary: React.FC = () => { }, }, { - id: 'totalCumulativeExpenditure', - field: 'totalCumulativeExpenditure', + id: 'cumulativeExpenditure', + field: 'cumulativeExpenditure', headerName: "Total Cumulative Expenditure (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalCumulativeExpenditure} + ${params.row.cumulativeExpenditure} ) }, }, { - id: 'totalInvoicedAmount', - field: 'totalInvoicedAmount', + id: 'totalInvoiced', + field: 'totalInvoiced', headerName: "Total Invoiced Amount (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalInvoicedAmount} + ${params.row.totalInvoiced} ) }, }, { - id: 'totalUnInvoicedAmount', - field: 'totalUnInvoicedAmount', + id: 'totalUnInvoiced', + field: 'totalUnInvoiced', headerName: "Total Un-invoiced Amount (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalUnInvoicedAmount} + ${params.row.totalUninvoiced} ) }, }, { - id: 'totalReceivedAmount', - field: 'totalReceivedAmount', + id: 'totalReceived', + field: 'totalReceived', headerName: "Total Received Amount (HKD)", flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalReceivedAmount} + ${params.row.totalReceived} ) }, }, @@ -322,8 +270,8 @@ const columns2 = [ flex: 1, }, { - id: 'clientName', - field: 'clientName', + id: 'customerName', + field: 'customerName', headerName: "Client Name", flex: 1, }, @@ -365,7 +313,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalFees} + ${params.row.totalFee} ) }, }, @@ -387,7 +335,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalCumulativeExpenditure} + ${params.row.cumulativeExpenditure} ) }, }, @@ -398,7 +346,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalInvoicedAmount} + ${params.row.totalInvoiced} ) }, }, @@ -409,7 +357,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalUnInvoicedAmount} + ${params.row.totalUninvoiced} ) }, }, @@ -420,7 +368,7 @@ const columns2 = [ flex: 1, renderCell: (params:any) => { return ( - ${params.row.totalReceivedAmount} + ${params.row.totalReceived} ) }, }, @@ -438,9 +386,9 @@ const columns2 = [
- {projectFinancialData.map((record, index) => ( -
handleCardClick(index)}> - + {projectFinancialData.map((record:any, index:any) => ( +
handleCardClick(record)}> +
))}
From 366865368b1992958a8d59ea2b69976bcc5d543b Mon Sep 17 00:00:00 2001 From: Wayne Date: Wed, 22 May 2024 23:37:39 +0900 Subject: [PATCH 12/16] Add more validation for timesheet entry --- src/app/api/timesheets/utils.ts | 68 ++++++++++++++++++- .../DateHoursTable/DateHoursList.tsx | 38 +++++++++-- .../DateHoursTable/DateHoursTable.tsx | 38 ++++++++--- src/components/ErrorAlert/ErrorAlert.tsx | 28 ++++++++ src/components/ErrorAlert/index.ts | 1 + src/components/LeaveTable/LeaveEditModal.tsx | 8 ++- .../TimesheetModal/TimesheetModal.tsx | 33 ++++++++- .../TimesheetTable/EntryInputTable.tsx | 6 +- .../TimesheetTable/MobileTimesheetEntry.tsx | 8 ++- .../TimesheetTable/MobileTimesheetTable.tsx | 3 + 10 files changed, 209 insertions(+), 22 deletions(-) create mode 100644 src/components/ErrorAlert/ErrorAlert.tsx create mode 100644 src/components/ErrorAlert/index.ts diff --git a/src/app/api/timesheets/utils.ts b/src/app/api/timesheets/utils.ts index 22d31ce..7a6dad3 100644 --- a/src/app/api/timesheets/utils.ts +++ b/src/app/api/timesheets/utils.ts @@ -1,4 +1,13 @@ -import { LeaveEntry, TimeEntry } from "./actions"; +import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; +import { HolidaysResult } from "../holidays"; +import { + LeaveEntry, + RecordLeaveInput, + RecordTimesheetInput, + TimeEntry, +} from "./actions"; +import { convertDateArrayToString } from "@/app/utils/formatUtil"; +import compact from "lodash/compact"; export type TimeEntryError = { [field in keyof TimeEntry]?: string; @@ -6,7 +15,7 @@ export type TimeEntryError = { /** * @param entry - the time entry - * @returns the field where there is an error, or an empty string if there is none + * @returns an object where the keys are the error fields and the values the error message, and undefined if there are no errors */ export const validateTimeEntry = ( entry: Partial, @@ -58,6 +67,61 @@ export const isValidLeaveEntry = (entry: Partial): string => { return error; }; +export const validateTimesheet = ( + timesheet: RecordTimesheetInput, + leaveRecords: RecordLeaveInput, + companyHolidays: HolidaysResult[], +): { [date: string]: string } | undefined => { + const errors: { [date: string]: string } = {}; + + const holidays = new Set( + compact([ + ...getPublicHolidaysForNYears(2).map((h) => h.date), + ...companyHolidays.map((h) => convertDateArrayToString(h.date)), + ]), + ); + + Object.keys(timesheet).forEach((date) => { + const timeEntries = timesheet[date]; + + // Check each entry + for (const entry of timeEntries) { + const entryErrors = validateTimeEntry(entry, holidays.has(date)); + + if (entryErrors) { + errors[date] = "There are errors in the entries"; + return; + } + } + + // Check total hours + const leaves = leaveRecords[date]; + const leaveHours = + leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; + + const totalNormalHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.inputHours || 0); + }, 0); + + const totalOtHours = timeEntries.reduce((acc, entry) => { + return acc + (entry.otHours || 0); + }, 0); + + if (totalNormalHours > DAILY_NORMAL_MAX_HOURS) { + errors[date] = + "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours."; + } else if ( + totalNormalHours + totalOtHours + leaveHours > + TIMESHEET_DAILY_MAX_HOURS + ) { + errors[date] = + "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; + } + }); + + return Object.keys(errors).length > 0 ? errors : undefined; +}; + export const DAILY_NORMAL_MAX_HOURS = 8; export const LEAVE_DAILY_MAX_HOURS = 8; export const TIMESHEET_DAILY_MAX_HOURS = 20; diff --git a/src/components/DateHoursTable/DateHoursList.tsx b/src/components/DateHoursTable/DateHoursList.tsx index 685aef9..d54e930 100644 --- a/src/components/DateHoursTable/DateHoursList.tsx +++ b/src/components/DateHoursTable/DateHoursList.tsx @@ -17,6 +17,7 @@ import dayjs from "dayjs"; import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { + DAILY_NORMAL_MAX_HOURS, LEAVE_DAILY_MAX_HOURS, TIMESHEET_DAILY_MAX_HOURS, } from "@/app/api/timesheets/utils"; @@ -32,6 +33,7 @@ interface Props { EntryComponentProps & { date: string } >; entryComponentProps: EntryComponentProps; + errorComponent?: React.ReactNode; } function DateHoursList({ @@ -41,6 +43,7 @@ function DateHoursList({ EntryComponent, entryComponentProps, companyHolidays, + errorComponent, }: Props) { const { t, @@ -83,15 +86,22 @@ function DateHoursList({ leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; const timesheet = timesheetEntries[day]; - const timesheetHours = + const timesheetNormalHours = timesheet?.reduce( - (acc, entry) => - acc + (entry.inputHours || 0) + (entry.otHours || 0), + (acc, entry) => acc + (entry.inputHours || 0), 0, ) || 0; + const timesheetOtHours = + timesheet?.reduce( + (acc, entry) => acc + (entry.otHours || 0), + 0, + ) || 0; + const timesheetHours = timesheetNormalHours + timesheetOtHours; const dailyTotal = leaveHours + timesheetHours; + const normalHoursExceeded = + timesheetNormalHours > DAILY_NORMAL_MAX_HOURS; const leaveExceeded = leaveHours > LEAVE_DAILY_MAX_HOURS; const dailyTotalExceeded = dailyTotal > TIMESHEET_DAILY_MAX_HOURS; @@ -122,7 +132,9 @@ function DateHoursList({ sx={{ display: "flex", justifyContent: "space-between", + flexWrap: "wrap", alignItems: "baseline", + color: normalHoursExceeded ? "error.main" : undefined, }} > @@ -131,6 +143,21 @@ function DateHoursList({ {manhourFormatter.format(timesheetHours)} + {normalHoursExceeded && ( + + {t( + "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours.", + { + DAILY_NORMAL_MAX_HOURS, + }, + )} + + )} ({ variant="caption" > {t( - "The daily total hours cannot be more than {{hours}}", + "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}", { - hours: TIMESHEET_DAILY_MAX_HOURS, + TIMESHEET_DAILY_MAX_HOURS, }, )} @@ -198,6 +225,7 @@ function DateHoursList({ })} )} + {errorComponent} {isDateSelected ? ( - + }> diff --git a/src/components/ChangePassword/ChangePasswordForm.tsx b/src/components/ChangePassword/ChangePasswordForm.tsx index 9801ec7..ff2965e 100644 --- a/src/components/ChangePassword/ChangePasswordForm.tsx +++ b/src/components/ChangePassword/ChangePasswordForm.tsx @@ -14,7 +14,7 @@ import { Visibility, VisibilityOff } from "@mui/icons-material"; import { IconButton, InputAdornment } from "@mui/material"; const ChagnePasswordForm: React.FC = () => { - const { t } = useTranslation(); + const { t } = useTranslation("changePassword"); const [showNewPassword, setShowNewPassword] = useState(false); const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword); @@ -38,7 +38,7 @@ const ChagnePasswordForm: React.FC = () => { - {t("Please Fill in all the Fields")} + {t("Please Fill in All Fields")} diff --git a/src/components/CreateGroup/AuthorityAllocation.tsx b/src/components/CreateGroup/AuthorityAllocation.tsx index bdd4ccb..794b98d 100644 --- a/src/components/CreateGroup/AuthorityAllocation.tsx +++ b/src/components/CreateGroup/AuthorityAllocation.tsx @@ -84,8 +84,8 @@ const AuthorityAllocation: React.FC = ({ auth }) => { onClick: addAuth, buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, // { label: t("Current Position"), name: "currentPosition" }, ], [addAuth, t] @@ -97,10 +97,10 @@ const AuthorityAllocation: React.FC = ({ auth }) => { label: t("Remove"), name: "id", onClick: removeAuth, - buttonIcon: , + buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, ], [removeAuth, selectedAuths, t] ); @@ -115,16 +115,13 @@ const AuthorityAllocation: React.FC = ({ auth }) => { }, []); 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)) - // }) - // ); + setFilteredAuths( + initialAuths.filter( + (a) => + a.authority.toLowerCase().includes(query.toLowerCase()) || + a.name?.toLowerCase().includes(query.toLowerCase()) + ) + ); }, [auth, query]); const resetAuth = React.useCallback(() => { @@ -152,7 +149,7 @@ const AuthorityAllocation: React.FC = ({ auth }) => { > - {t("Authority")} + {/* {t("Authority")} */} @@ -162,7 +159,7 @@ const AuthorityAllocation: React.FC = ({ auth }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ") + t("Authority") + " / " + t("Description")} InputProps={{ endAdornment: query && ( @@ -178,18 +175,20 @@ const AuthorityAllocation: React.FC = ({ auth }) => { - {tabIndex === 0 && ( + {tabIndex === 0 && ( )} - {tabIndex === 1 && ( + {tabIndex === 1 && ( = ({ auth, users }) => { const [serverError, setServerError] = useState(""); const router = useRouter(); const [tabIndex, setTabIndex] = useState(0); - const { t } = useTranslation(); + const { t } = useTranslation("group"); const errors = formProps.formState.errors; diff --git a/src/components/CreateGroup/GroupInfo.tsx b/src/components/CreateGroup/GroupInfo.tsx index d9141bc..5792050 100644 --- a/src/components/CreateGroup/GroupInfo.tsx +++ b/src/components/CreateGroup/GroupInfo.tsx @@ -51,13 +51,13 @@ const GroupInfo: React.FC = () => { Boolean(errors.name) && (errors.name?.message ? t(errors.name.message) - : t("Please input correct name")) + : t("Please input correct ") + t("Group Name")) } /> { Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) - : t("Please input correct description")) + : t("Please input correct ") + t("Description")) } /> diff --git a/src/components/CreateGroup/UserAllocation.tsx b/src/components/CreateGroup/UserAllocation.tsx index ff13c52..351303a 100644 --- a/src/components/CreateGroup/UserAllocation.tsx +++ b/src/components/CreateGroup/UserAllocation.tsx @@ -85,8 +85,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: addUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [addUser, t] ); @@ -99,8 +99,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: removeUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [removeUser, selectedUsers, t] ); @@ -116,16 +116,13 @@ const UserAllocation: React.FC = ({ users }) => { }, []); 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)) - // }) - // ); + const q = query.toLowerCase(); + setFilteredUsers( + initialUsers.filter((u) => ( + u.username.toLowerCase().includes(q) || + u.name.toLowerCase().includes(q) + )) + ); }, [users, query]); const resetUser = React.useCallback(() => { @@ -153,7 +150,7 @@ const UserAllocation: React.FC = ({ users }) => { > - {t("User")} + {/* {t("User")} */} @@ -163,7 +160,7 @@ const UserAllocation: React.FC = ({ users }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ") + t("Username") + " / " + t("Staff Name")} InputProps={{ endAdornment: query && ( diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx index 04b880c..6f267de 100644 --- a/src/components/CreateTeam/StaffAllocation.tsx +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -48,7 +48,6 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { } = 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)) @@ -158,15 +157,13 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, []); React.useEffect(() => { + const q = query.toLowerCase(); setFilteredStaff( - initialStaffs.filter((i) => { - const q = query.toLowerCase(); - return ( + initialStaffs.filter((i) => ( i.staffId.toLowerCase().includes(q) || i.name.toLowerCase().includes(q) || i.currentPosition.toLowerCase().includes(q) - ); - }) + )) ); }, [staff, query]); diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index 947a116..1d3c473 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -50,7 +50,7 @@ interface Props { } const EditUser: React.FC = async ({ user, rules, auths }) => { - const { t } = useTranslation(); + const { t } = useTranslation("user"); const formProps = useForm(); const searchParams = useSearchParams(); const id = parseInt(searchParams.get("id") || "0"); diff --git a/src/components/EditUserGroup/AuthorityAllocation.tsx b/src/components/EditUserGroup/AuthorityAllocation.tsx index da502da..856ca3b 100644 --- a/src/components/EditUserGroup/AuthorityAllocation.tsx +++ b/src/components/EditUserGroup/AuthorityAllocation.tsx @@ -41,7 +41,6 @@ const AuthorityAllocation: React.FC = ({ auth }) => { reset, resetField, } = useFormContext(); - console.log(auth) const initialAuths = auth.map((a) => ({ ...a })).sort((a, b) => a.id - b.id); const [filteredAuths, setFilteredAuths] = useState(initialAuths); const [selectedAuths, setSelectedAuths] = useState( @@ -86,8 +85,8 @@ const AuthorityAllocation: React.FC = ({ auth }) => { onClick: addAuth, buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, // { label: t("Current Position"), name: "currentPosition" }, ], [addAuth, t] @@ -101,8 +100,8 @@ const AuthorityAllocation: React.FC = ({ auth }) => { onClick: removeAuth, buttonIcon: , }, - { label: t("authority"), name: "authority" }, - { label: t("Auth Name"), name: "name" }, + { label: t("Authority"), name: "authority" }, + { label: t("Description"), name: "name" }, ], [removeAuth, selectedAuths, t] ); @@ -117,16 +116,13 @@ const AuthorityAllocation: React.FC = ({ auth }) => { }, []); 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)) - // }) - // ); + const q = query.toLowerCase(); + setFilteredAuths( + initialAuths.filter((a) => ( + a.authority.toLowerCase().includes(q) || + a.name.toLowerCase().includes(q) + )) + ); }, [auth, query]); const resetAuth = React.useCallback(() => { @@ -164,7 +160,7 @@ const AuthorityAllocation: React.FC = ({ auth }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ")+ t("Authority") + " / " + t("Description")} InputProps={{ endAdornment: query && ( diff --git a/src/components/EditUserGroup/GroupInfo.tsx b/src/components/EditUserGroup/GroupInfo.tsx index d9141bc..3817cec 100644 --- a/src/components/EditUserGroup/GroupInfo.tsx +++ b/src/components/EditUserGroup/GroupInfo.tsx @@ -51,13 +51,13 @@ const GroupInfo: React.FC = () => { Boolean(errors.name) && (errors.name?.message ? t(errors.name.message) - : t("Please input correct name")) + : t("Please input correct ") + t("name")) } /> { Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) - : t("Please input correct description")) + : t("Please input correct ") + t("Description")) } /> diff --git a/src/components/EditUserGroup/UserAllocation.tsx b/src/components/EditUserGroup/UserAllocation.tsx index 14ed975..5173311 100644 --- a/src/components/EditUserGroup/UserAllocation.tsx +++ b/src/components/EditUserGroup/UserAllocation.tsx @@ -92,8 +92,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: addUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [addUser, t] ); @@ -106,8 +106,8 @@ const UserAllocation: React.FC = ({ users }) => { onClick: removeUser, buttonIcon: , }, - { label: t("User Name"), name: "username" }, - { label: t("name"), name: "name" }, + { label: t("Username"), name: "username" }, + { label: t("Staff Name"), name: "name" }, ], [removeUser, selectedUsers, t] ); @@ -123,16 +123,13 @@ const UserAllocation: React.FC = ({ users }) => { }, []); 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)) - // }) - // ); + const q = query.toLowerCase(); + setFilteredUsers( + initialUsers.filter((item) => ( + item.username.toLowerCase().includes(q) || + item.name.toLowerCase().includes(q) + )) + ); }, [users, query]); const resetUser = React.useCallback(() => { @@ -170,7 +167,7 @@ const UserAllocation: React.FC = ({ users }) => { fullWidth onChange={onQueryInputChange} value={query} - placeholder={t("Search by staff ID, name or position.")} + placeholder={t("Search by ") + t("Username") + " / " + t("Staff Name")} InputProps={{ endAdornment: query && ( diff --git a/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx b/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx index 072e297..a858406 100644 --- a/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx +++ b/src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx @@ -3,61 +3,66 @@ import React, { useMemo } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import { ProjectResult } from "@/app/api/projects"; -import { fetchMonthlyWorkHoursReport, fetchProjectCashFlowReport } from "@/app/api/reports/actions"; +import { + fetchMonthlyWorkHoursReport, + fetchProjectCashFlowReport, +} from "@/app/api/reports/actions"; import { downloadFile } from "@/app/utils/commonUtil"; import { BASE_API_URL } from "@/config/api"; import { MonthlyWorkHoursReportFilter } from "@/app/api/reports"; import { records } from "@/app/api/staff/actions"; import { StaffResult } from "@/app/api/staff"; +import dayjs from "dayjs"; interface Props { - staffs: StaffResult[] + staffs: StaffResult[]; } type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const GenerateMonthlyWorkHoursReport: React.FC = ({ staffs }) => { - const { t } = useTranslation(); - const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`) - console.log(staffs) + const { t } = useTranslation("report"); + const staffCombo = staffs.map((staff) => `${staff.name} - ${staff.staffId}`); - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Staff"), - paramName: "staff", - type: "select", - options: staffCombo, - needAll: false - }, - { - label: t("date"), - paramName: "date", - type: "monthYear", - }, - ], - [t], - ); + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Staff"), + paramName: "staff", + type: "select", + options: staffCombo, + needAll: false, + }, + { + label: t("Date"), + paramName: "date", + type: "monthYear", + }, + ], + [t] + ); -return ( + return ( <> - { - const index = staffCombo.findIndex(staff => staff === query.staff) - if (query.staff.length > 0 && query.staff.toLocaleLowerCase() !== "all" && query.date.length > 0) { - const index = staffCombo.findIndex(staff => staff === query.staff) - const response = await fetchMonthlyWorkHoursReport({ id: staffs[index].id, yearMonth: query.date }) - if (response) { - downloadFile(new Uint8Array(response.blobValue), response.filename!!) - } - } - } - } - /> - - ) -} + const index = staffCombo.findIndex((staff) => staff === query.staff); + const response = await fetchMonthlyWorkHoursReport({ + id: staffs[index].id, + yearMonth: query.date, + }); + if (response) { + downloadFile( + new Uint8Array(response.blobValue), + response.filename!! + ); + } + }} + /> + + ); +}; -export default GenerateMonthlyWorkHoursReport \ No newline at end of file +export default GenerateMonthlyWorkHoursReport; diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx index 5e6e76a..fb49dba 100644 --- a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx +++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx @@ -20,10 +20,10 @@ type SearchQuery = Partial = ({ team, customer }) => { - const { t } = useTranslation(); + const { t } = useTranslation("report"); const teamCombo = team.map(t => `${t.name} - ${t.code}`) const custCombo = customer.map(c => `${c.name} - ${c.code}`) - const statusCombo = ["Within Budget, Overconsumption", "Potential Overconsumption"] + const statusCombo = ["Overconsumption", "Potential Overconsumption"] // const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`) // console.log(staffs) diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 0f342d0..ad6cbeb 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -226,19 +226,18 @@ function SearchBox({ adapterLocale="zh-hk" > - - {c.label} - - + + + )} diff --git a/src/components/UserGroupSearch/UserGroupSearch.tsx b/src/components/UserGroupSearch/UserGroupSearch.tsx index 7a8f7dd..85f01c6 100644 --- a/src/components/UserGroupSearch/UserGroupSearch.tsx +++ b/src/components/UserGroupSearch/UserGroupSearch.tsx @@ -26,10 +26,15 @@ const UserGroupSearch: React.FC = ({ users }) => { const searchCriteria: Criterion[] = useMemo( () => [ { - label: t("User Name"), + label: t("Group Name"), paramName: "name", type: "text", }, + { + label: t("Description"), + paramName: "description", + type: "text", + }, ], [t] ); @@ -75,14 +80,13 @@ const UserGroupSearch: React.FC = ({ users }) => { { - // setFilteredUser( - // users.filter( - // (t) => - // t.name.toLowerCase().includes(query.name.toLowerCase()) && - // t.code.toLowerCase().includes(query.code.toLowerCase()) && - // t.description.toLowerCase().includes(query.description.toLowerCase()) - // ) - // ) + setFilteredUser( + users.filter( + (u) => + u.name.toLowerCase().includes(query.name.toLowerCase()) && + u.description.toLowerCase().includes(query.description.toLowerCase()) + ) + ) }} /> items={filteredUser} columns={columns} /> diff --git a/src/i18n/zh/changePassword.json b/src/i18n/zh/changePassword.json new file mode 100644 index 0000000..6ddba8e --- /dev/null +++ b/src/i18n/zh/changePassword.json @@ -0,0 +1,7 @@ +{ + "Change Password": "更改密碼", + "Please Fill in All Fields": "請填寫以下項目", + "Input Old Password": "舊密碼", + "Input New Password": "新密碼", + "Input New Password Again": "重複輸入新密碼" +} \ No newline at end of file diff --git a/src/i18n/zh/group.json b/src/i18n/zh/group.json new file mode 100644 index 0000000..d976095 --- /dev/null +++ b/src/i18n/zh/group.json @@ -0,0 +1,27 @@ +{ + "User Group": "用戶群組", + "Create User Group": "建立用戶群組", + "Edit User Group": "編輯用戶群組", + "Edit": "編輯", + "Group Name": "名稱", + "Description": "描述", + "Group Info": "群組資料", + "Add": "新增", + "User": "用戶", + "Remove": "移除", + "Confirm": "確定", + "Cancel": "取消", + "Delete": "刪除", + "Search by ": "搜尋", + "Delete Success": "刪除成功", + "Please input correct ": "請輸入正確", + "Authority Allocation": "權限分配", + "Authority Pool": "權限池", + "Allocated Authority": "已分配權限", + "User Allocation": "用戶分配", + "User Pool": "用戶池", + "Allocated Users": "已分配用戶", + "Username": "用戶名", + "Staff Name": "員工名字", + "Authority": "權限" +} \ No newline at end of file diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json index 5b2e784..d15eec9 100644 --- a/src/i18n/zh/report.json +++ b/src/i18n/zh/report.json @@ -1,4 +1,12 @@ { + "Staff Monthly Work Hours Analysis Report": "Staff Monthly Work Hours Analysis Report", + "Project Resource Overconsumption Report": "Project Resource Overconsumption Report", + "Project": "項目", - "Date Type": "日期類型" + "Date Type": "日期類型", + "Date": "日期", + "Team": "隊伍", + "Client": "客戶", + "Status": "狀態", + "Staff": "員工" } \ No newline at end of file diff --git a/src/i18n/zh/user.json b/src/i18n/zh/user.json new file mode 100644 index 0000000..e92a8ad --- /dev/null +++ b/src/i18n/zh/user.json @@ -0,0 +1,7 @@ +{ + "Edit User": "編輯用戶", + "User Detail": "用戶資料", + "User Authority": "權限", + "username": "用戶名", + "password": "更改密碼" +} \ No newline at end of file From f5227e5d4e81232d12fd1d04d33d7d8a33131048 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Thu, 23 May 2024 18:07:36 +0800 Subject: [PATCH 14/16] update completion reports --- .../ProjectCompletionReport/page.tsx | 28 +++--- src/app/api/reports/actions.ts | 15 ++- src/app/api/reports/index.ts | 12 +++ .../NavigationContent/NavigationContent.tsx | 10 +- .../ProjectCompletionReport.tsx | 95 +++++++++++++++++++ .../ProjectCompletionReportLoading.tsx | 41 ++++++++ .../ProjectCompletionReportWrapper.tsx | 18 ++++ .../ProjectCompletionReport/index.ts | 1 + 8 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 src/components/ProjectCompletionReport/ProjectCompletionReport.tsx create mode 100644 src/components/ProjectCompletionReport/ProjectCompletionReportLoading.tsx create mode 100644 src/components/ProjectCompletionReport/ProjectCompletionReportWrapper.tsx create mode 100644 src/components/ProjectCompletionReport/index.ts diff --git a/src/app/(main)/analytics/ProjectCompletionReport/page.tsx b/src/app/(main)/analytics/ProjectCompletionReport/page.tsx index 8a13941..e277d3a 100644 --- a/src/app/(main)/analytics/ProjectCompletionReport/page.tsx +++ b/src/app/(main)/analytics/ProjectCompletionReport/page.tsx @@ -1,24 +1,28 @@ //src\app\(main)\analytics\ProjectCompletionReport\page.tsx import { Metadata } from "next"; -import { I18nProvider } from "@/i18n"; +import { I18nProvider, getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; -import ProjectCompletionReportComponent from "@/components/Report/ProjectCompletionReport"; +import { Suspense } from "react"; +import ProjectCompletionReport from "@/components/ProjectCompletionReport"; export const metadata: Metadata = { title: "Project Completion Report", }; -const ProjectCompletionReport: React.FC = () => { +const ProjectCompletionReportPage: React.FC = async () => { + const { t } = await getServerI18n("report"); + return ( - - - Project Completion Report + <> + + {t("Project Completion Report")} - {/* }> - - */} - - + + }> + + + + ); }; -export default ProjectCompletionReport; +export default ProjectCompletionReportPage; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index ff874ba..2d3c09e 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -1,7 +1,7 @@ "use server"; import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; -import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest } from "."; +import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest, LateStartReportRequest, ProjectResourceOverconsumptionReportRequest, ProjectPandLReportRequest, ProjectCompletionReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; export interface FileResponse { @@ -48,6 +48,19 @@ export const fetchProjectResourceOverconsumptionReport = async (data: ProjectRes return reportBlob }; +export const fetchProjectCompletionReport = async (data: ProjectCompletionReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/ProjectCompletionReportwithOutstandingAccountsReceivable`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; + // export const fetchLateStartReport = async (data: LateStartReportRequest) => { // const response = await serverFetchBlob( // `${BASE_API_URL}/reports/downloadLateStartReport`, diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 0691005..7779e14 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -65,3 +65,15 @@ export interface LateStartReportRequest { client: string; date: any; } + +export interface ProjectCompletionReportFilter { + startDate: String; + startDateTo: String; + Outstanding: String; +} + +export interface ProjectCompletionReportRequest { + startDate: String; + endDate: String; + outstanding: Boolean; +} diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 0ed436c..0d8521f 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -162,11 +162,11 @@ const NavigationContent: React.FC = ({ abilities }) => { label: "Completion Report", path: "/analytics/ProjectCompletionReport", }, - { - icon: , - label: "Completion Report with Outstanding Un-billed Hours Report", - path: "/analytics/ProjectCompletionReportWO", - }, + // { + // icon: , + // label: "Completion Report with Outstanding Un-billed Hours Report", + // path: "/analytics/ProjectCompletionReportWO", + // }, { icon: , label: "Project Claims Report", diff --git a/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx b/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx new file mode 100644 index 0000000..397c27a --- /dev/null +++ b/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx @@ -0,0 +1,95 @@ +"use client"; +import { + ProjectCompletionReportFilter, + ProjectCompletionReportRequest, +} from "@/app/api/reports"; +import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import SearchBox, { Criterion } from "../SearchBox"; +import dayjs from "dayjs"; +import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { fetchProjectCompletionReport } from "@/app/api/reports/actions"; + +interface Props { + // team: TeamResult[] + // customer: Customer[] +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ProjectCompletionReport: React.FC = ( + { + // team, + // customer + } +) => { + const { t } = useTranslation("report"); + const [error, setError] = useState(""); + const outstandingList = ["Regular", "Outstanding Accounts Receivable"] + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("startDate"), + label2: t("endDate"), + paramName: "startDate", + type: "dateRange", + }, + { + label: t("Type"), + paramName: "Outstanding", + type: "select", + needAll: false, + options: outstandingList + }, + ], + [t] + ); + + return ( + <> + { + console.log(query); + let postData: ProjectCompletionReportRequest = { + startDate: "", + endDate: dayjs().format(INPUT_DATE_FORMAT).toString(), + oustanding: false + }; + if (query.endDate && query.endDate.length > 0) { + postData.endDate = query.endDate; + } + + // check if start date exist + if (query.startDate.length === 0) { + setError(t("Start Date cant be empty")); + } else { + postData.startDate = query.startDate; + if (query.Outstanding && query.Outstanding === "Outstanding Accounts Receivable") { + // outstanding report + postData.outstanding = true + } + console.log(postData) + const response = + await fetchProjectCompletionReport( + postData + ); + // normal report + + if (response) { + downloadFile( + new Uint8Array(response.blobValue), + response.filename!! + ); + } + } + }} + /> + + ); +}; + +export default ProjectCompletionReport; diff --git a/src/components/ProjectCompletionReport/ProjectCompletionReportLoading.tsx b/src/components/ProjectCompletionReport/ProjectCompletionReportLoading.tsx new file mode 100644 index 0000000..1c5355c --- /dev/null +++ b/src/components/ProjectCompletionReport/ProjectCompletionReportLoading.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 ProjectCompletionReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectCompletionReportLoading; diff --git a/src/components/ProjectCompletionReport/ProjectCompletionReportWrapper.tsx b/src/components/ProjectCompletionReport/ProjectCompletionReportWrapper.tsx new file mode 100644 index 0000000..0dd10cb --- /dev/null +++ b/src/components/ProjectCompletionReport/ProjectCompletionReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { fetchAllCustomers } from "@/app/api/customer"; +import { fetchTeam } from "@/app/api/team"; +import ProjectCompletionReportLoading from "./ProjectCompletionReportLoading"; +import ProjectCompletionReport from "./ProjectCompletionReport"; + +interface SubComponents { + Loading: typeof ProjectCompletionReportLoading; +} + +const ProjectCompletionReportWrapper: React.FC & SubComponents = async () => { + + return +}; + +ProjectCompletionReportWrapper.Loading = ProjectCompletionReportLoading; + +export default ProjectCompletionReportWrapper; \ No newline at end of file diff --git a/src/components/ProjectCompletionReport/index.ts b/src/components/ProjectCompletionReport/index.ts new file mode 100644 index 0000000..8555938 --- /dev/null +++ b/src/components/ProjectCompletionReport/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectCompletionReportWrapper"; \ No newline at end of file From 898e74b819708733fed118006658bacb99aab5cd Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Fri, 24 May 2024 00:17:39 +0800 Subject: [PATCH 15/16] update dashboard --- src/app/api/cashflow/index.ts | 21 ++------- src/app/api/financialsummary/actions.ts | 30 ++++++++---- src/components/Breadcrumb/Breadcrumb.tsx | 1 + .../CustomDatagrid/CustomDatagrid.tsx | 6 +++ .../ProjectCashFlow/ProjectCashFlow.tsx | 47 ++++++++++++++++--- .../ProjectFinancialCard.tsx | 12 ++--- .../ProjectFinancialSummary.tsx | 34 ++++++++------ 7 files changed, 97 insertions(+), 54 deletions(-) diff --git a/src/app/api/cashflow/index.ts b/src/app/api/cashflow/index.ts index 978a61e..f71ba74 100644 --- a/src/app/api/cashflow/index.ts +++ b/src/app/api/cashflow/index.ts @@ -1,4 +1,7 @@ +"use server"; import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; export interface CashFlow { id: number; @@ -19,21 +22,5 @@ export const preloadProjects = () => { }; export const fetchProjectsCashFlow = cache(async () => { - return mockProjects; + return serverFetchJson(`${BASE_API_URL}/dashboard/searchCashFlowProject`); }); - -const mockProjects: CashFlow[] = [ - { - id: 1, - projectCode: "CUST-001", - projectName: "Client A", - team: "N/A", - teamLeader: "N/A", - startDate: "5", - startDateFrom: "5", - startDateTo: "5", - targetEndDate: "s", - client: "ss", - subsidiary: "ss", - }, -]; diff --git a/src/app/api/financialsummary/actions.ts b/src/app/api/financialsummary/actions.ts index bb5b82b..c1dda65 100644 --- a/src/app/api/financialsummary/actions.ts +++ b/src/app/api/financialsummary/actions.ts @@ -39,16 +39,28 @@ export interface FinancialSummaryByProjectResult { totalUninvoiced: number; } -export const searchFinancialSummaryByClient = cache(async (teamId: number) => { - - return serverFetchJson( - `${BASE_API_URL}/dashboard/searchFinancialSummaryByClient?teamId=${teamId}` - ); +export const searchFinancialSummaryByClient = cache(async (teamId?: number) => { + if (teamId === undefined) { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByClient` + ); + } else { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByClient?teamId=${teamId}` + ); + } + }); -export const searchFinancialSummaryByProject = cache(async (teamId: number) => { +export const searchFinancialSummaryByProject = cache(async (teamId?: number, customerId?:number) => { + if (teamId === undefined) { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByProject` + ); + } else { + return serverFetchJson( + `${BASE_API_URL}/dashboard/searchFinancialSummaryByProject?teamId=${teamId}&customerId=${customerId}` + ); + } - return serverFetchJson( - `${BASE_API_URL}/dashboard/searchFinancialSummaryByProject?teamId=${teamId}` - ); }); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 73f41f6..77a4eda 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -13,6 +13,7 @@ import { I18nProvider } from "@/i18n"; const pathToLabelMap: { [path: string]: string } = { "": "Overview", "/home": "User Workspace", + "/dashboard": "Dashboard", "/projects": "Projects", "/projects/create": "Create Project", "/projects/createSub": "Sub Project", diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index c346874..83f395d 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/src/components/CustomDatagrid/CustomDatagrid.tsx @@ -15,6 +15,7 @@ interface CustomDatagridProps { dataGridHeight?: number | string; [key: string]: any; checkboxSelection?: boolean; + onRowClick?: any; onRowSelectionModelChange?: ( newSelectionModel: GridRowSelectionModel, ) => void; @@ -34,6 +35,7 @@ const CustomDatagrid: React.FC = ({ checkboxSelection, // Destructure the new prop onRowSelectionModelChange, // Destructure the new prop selectionModel, + onRowClick, columnGroupingModel, pageSize, ...props @@ -195,6 +197,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} experimentalFeatures={{ columnGrouping: true }} @@ -226,6 +229,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} experimentalFeatures={{ columnGrouping: true }} @@ -257,6 +261,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} experimentalFeatures={{ columnGrouping: true }} @@ -289,6 +294,7 @@ const CustomDatagrid: React.FC = ({ rows={rowsWithDefaultValues} columns={modifiedColumns} editMode="row" + onRowClick={onRowClick} style={{ marginRight: 0 }} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} diff --git a/src/components/ProjectCashFlow/ProjectCashFlow.tsx b/src/components/ProjectCashFlow/ProjectCashFlow.tsx index ef81aff..4a78c62 100644 --- a/src/components/ProjectCashFlow/ProjectCashFlow.tsx +++ b/src/components/ProjectCashFlow/ProjectCashFlow.tsx @@ -19,17 +19,34 @@ import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; import ProgressCashFlowSearch from "@/components/ProgressCashFlowSearch"; +import { fetchProjectsCashFlow} from "@/app/api/cashflow"; import { Input, Label } from "reactstrap"; +import { CashFlow } from "@/app/api/cashflow"; + +interface Props { + projects: CashFlow[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; const ProjectCashFlow: React.FC = () => { + const { t } = useTranslation("projects"); const todayDate = new Date(); const [selectionModel, setSelectionModel]: any[] = React.useState([]); + const [projectData, setProjectData]: any[] = React.useState([]); const [cashFlowYear, setCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); const [anticipateCashFlowYear, setAnticipateCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); + const fetchData = async () => { + const cashFlowProject = await fetchProjectsCashFlow(); + setProjectData(cashFlowProject) + } + useEffect(() => { + fetchData() + }, []); const columns = [ { id: "projectCode", @@ -50,8 +67,8 @@ const ProjectCashFlow: React.FC = () => { flex: 1, }, { - id: "teamLeader", - field: "teamLeader", + id: "teamLead", + field: "teamLead", headerName: "Team Leader", flex: 1, }, @@ -530,8 +547,6 @@ const ProjectCashFlow: React.FC = () => { remarks: "Monthly Manpower Expenditure", }, ]; - - const [projectData, setProjectData]: any[] = React.useState(rows); const [ledgerData, setLedgerData]: any[] = React.useState(ledgerRows); const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { const selectedRowsData = projectData.filter((row: any) => @@ -540,11 +555,31 @@ const ProjectCashFlow: React.FC = () => { console.log(selectedRowsData); }; + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "Project Code", paramName: "projectCode", type: "text" }, + { label: "Project Name", paramName: "projectName", type: "text" }, + { + label: "Start Date From", + label2: "Start Date To", + paramName: "startDateFrom", + type: "dateRange", + }, + ], + [t], + ); + return ( <> - }> + {/* }> - + */} + { + console.log(query); + }} + /> = ({ : "border-green-200 border-solid"; const selectedBackgroundColor = ClickedIndex === Index ? "rgb(235 235 235)" : "rgb(255 255 255)"; - console.log(ClickedIndex); - console.log(Index); return ( = ({ Total Fees
- {TotalFees.toLocaleString()} + {TotalFees.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Total Budget
- {TotalBudget.toLocaleString()} + {TotalBudget.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Total Cumulative Expenditure
- {TotalCumulative.toLocaleString()} + {TotalCumulative.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Total Invoiced Amount
- {TotalInvoicedAmount.toLocaleString()} + {TotalInvoicedAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

Total Received Amount
- {TotalReceivedAmount.toLocaleString()} + {TotalReceivedAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

diff --git a/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx b/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx index eafaba4..c803b59 100644 --- a/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx +++ b/src/components/ProjectFinancialSummary/ProjectFinancialSummary.tsx @@ -21,6 +21,7 @@ import { Suspense } from "react"; import { fetchFinancialSummaryCard } from "@/app/api/financialsummary"; import { searchFinancialSummaryByClient,searchFinancialSummaryByProject } from "@/app/api/financialsummary/actions"; import ProjectFinancialCard from "./ProjectFinancialCard"; +import VisibilityIcon from '@mui/icons-material/Visibility'; const ProjectFinancialSummary: React.FC = () => { const [SearchCriteria, setSearchCriteria] = React.useState({}); @@ -33,16 +34,16 @@ const ProjectFinancialSummary: React.FC = () => { const financialSummaryCard = await fetchFinancialSummaryCard(); setProjectFinancialData(financialSummaryCard) } - const fetchTableData = async (teamId:any) => { + const fetchTableData = async (teamId?:any) => { const financialSummaryByClient = await searchFinancialSummaryByClient(teamId); - const financialSummaryByProject = await searchFinancialSummaryByProject(teamId); + console.log(financialSummaryByClient) - console.log(financialSummaryByProject) + // console.log(financialSummaryByProject) setClientFinancialRows(financialSummaryByClient) - setProjectFinancialRows(financialSummaryByProject) } useEffect(() => { fetchData() + fetchTableData(undefined) }, []); const rows0 = [{id: 1,projectCode:"M1201",projectName:"Consultancy Project C", team:"XXX", teamLeader:"XXX", startDate:"01/08/2022", targetEndDate: "01/05/2024", client:"Client A", subsidiary:"N/A"}, @@ -73,16 +74,9 @@ const ProjectFinancialSummary: React.FC = () => { const [selectedTeamData, setSelectedTeamData]: any[] = React.useState(rows0); - const handleCardClick = (r: any) => { + const handleCardClick = (r: any, index:any) => { fetchTableData(r.teamId) - // setIsCardClickedIndex(r); - // if (r === 0) { - // setSelectedTeamData(rows0); - // } else if (r === 1) { - // setSelectedTeamData(rows1); - // } else if (r === 2) { - // setSelectedTeamData(rows2); - // } + setIsCardClickedIndex(index) }; const columns = [ @@ -380,6 +374,16 @@ const columns2 = [ ); console.log(selectedRowsData); }; + + const fetchProjectTableData = async (teamId?:any,customerId?:any) => { + const financialSummaryByProject = await searchFinancialSummaryByProject(teamId,customerId); + setProjectFinancialRows(financialSummaryByProject) + } + + const handleRowClick = (params:any) => { + console.log(params.row.teamId); + fetchProjectTableData(params.row.teamId,params.row.cid) + }; return ( @@ -387,7 +391,7 @@ const columns2 = [
{projectFinancialData.map((record:any, index:any) => ( -
handleCardClick(record)}> +
handleCardClick(record,index)}>
))} @@ -397,7 +401,7 @@ const columns2 = [
{/* */} - +
From f1b0fd34ef5a5f103a77e457785c646e33d39721 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Fri, 24 May 2024 11:06:22 +0800 Subject: [PATCH 16/16] update --- src/app/api/reports/index.ts | 2 +- .../ProjectCompletionReport/ProjectCompletionReport.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 7779e14..1648111 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -69,7 +69,7 @@ export interface LateStartReportRequest { export interface ProjectCompletionReportFilter { startDate: String; startDateTo: String; - Outstanding: String; + outstanding: String; } export interface ProjectCompletionReportRequest { diff --git a/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx b/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx index 397c27a..66bf505 100644 --- a/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx +++ b/src/components/ProjectCompletionReport/ProjectCompletionReport.tsx @@ -39,7 +39,7 @@ const ProjectCompletionReport: React.FC = ( }, { label: t("Type"), - paramName: "Outstanding", + paramName: "outstanding", type: "select", needAll: false, options: outstandingList @@ -57,7 +57,7 @@ const ProjectCompletionReport: React.FC = ( let postData: ProjectCompletionReportRequest = { startDate: "", endDate: dayjs().format(INPUT_DATE_FORMAT).toString(), - oustanding: false + outstanding: false }; if (query.endDate && query.endDate.length > 0) { postData.endDate = query.endDate; @@ -68,7 +68,7 @@ const ProjectCompletionReport: React.FC = ( setError(t("Start Date cant be empty")); } else { postData.startDate = query.startDate; - if (query.Outstanding && query.Outstanding === "Outstanding Accounts Receivable") { + if (query.outstanding && query.outstanding === "Outstanding Accounts Receivable") { // outstanding report postData.outstanding = true }