From dedeff4b413fd2c2e2aa6f3f52b1e48fd76ad647 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Apr 2024 16:41:03 +0800 Subject: [PATCH] update claim, add EX02 report --- .../EX02ProjectCashFlowReport/page.tsx | 25 +++++++++ src/app/api/claims/actions.ts | 2 +- src/app/api/reports/actions.ts | 18 +++++++ src/app/api/reports/index.ts | 8 +++ src/app/utils/commonUtil.ts | 16 ++++++ src/app/utils/fetchUtil.ts | 26 ++++++++-- src/components/Breadcrumb/Breadcrumb.tsx | 1 + src/components/ClaimDetail/ClaimDetail.tsx | 3 +- .../ClaimDetail/ClaimFormInputGrid.tsx | 9 ++-- src/components/ClaimSearch/ClaimSearch.tsx | 12 ++--- .../GenerateEX02ProjectCashFlowReport.tsx | 52 +++++++++++++++++++ ...nerateEX02ProjectCashFlowReportLoading.tsx | 38 ++++++++++++++ ...nerateEX02ProjectCashFlowReportWrapper.tsx | 18 +++++++ .../index.ts | 1 + .../NavigationContent/NavigationContent.tsx | 1 + src/i18n/en/claim.json | 1 + src/i18n/en/report.json | 3 ++ src/i18n/zh/claim.json | 1 + src/i18n/zh/report.json | 3 ++ 19 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx create mode 100644 src/app/api/reports/actions.ts create mode 100644 src/app/api/reports/index.ts create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/index.ts create mode 100644 src/i18n/en/report.json create mode 100644 src/i18n/zh/report.json diff --git a/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx new file mode 100644 index 0000000..d1d54d8 --- /dev/null +++ b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx @@ -0,0 +1,25 @@ +import { Metadata } from "next"; +import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "@/components/GenerateEX02ProjectCashFlowReport"; + +export const metadata: Metadata = { + title: "EX02 - Project Cash Flow Report", +}; + +const ProjectCashFlowReport: React.FC = async () => { + fetchProjects(); + + return ( + <> + + }> + + + + + ); +}; + +export default ProjectCashFlowReport; diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts index d607c48..542effe 100644 --- a/src/app/api/claims/actions.ts +++ b/src/app/api/claims/actions.ts @@ -21,7 +21,7 @@ export interface ClaimDetailTable { id: number; invoiceDate: Date; description: string; - project: ProjectCombo; + project: number; amount: number; supportingDocumentName: string; oldSupportingDocument: SupportingDocument; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts new file mode 100644 index 0000000..e0db4c4 --- /dev/null +++ b/src/app/api/reports/actions.ts @@ -0,0 +1,18 @@ +"use server"; + +import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; +import { EX02ProjectCashFlowReportRequest } from "."; +import { BASE_API_URL } from "@/config/api"; + +export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; \ No newline at end of file diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts new file mode 100644 index 0000000..6baa7aa --- /dev/null +++ b/src/app/api/reports/index.ts @@ -0,0 +1,8 @@ +// EX02 - Project Cash Flow Report +export interface EX02ProjectCashFlowReportFilter { + project: string[]; +} + +export interface EX02ProjectCashFlowReportRequest { + projectId: number; +} \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index d4c71b6..f0cc208 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -20,4 +20,20 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str return true } } +} + +function s2ab(s: string) { + var buf = new ArrayBuffer(s.length); + var view = new Uint8Array(buf); + for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; + return buf; +} + +export const downloadFile = (blob: Blob | string, type: string, filename: string) => { + + const url = URL.createObjectURL(typeof blob === "string" ? new Blob([blob], { type: type }) : blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", filename); + link.click(); } \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 0aaa798..fa11529 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -14,9 +14,9 @@ export const serverFetch: typeof fetch = async (input, init) => { ...init?.headers, ...(accessToken ? { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } : {}), }, }); @@ -56,6 +56,26 @@ export async function serverFetchWithNoContent(...args: FetchParams) { } } +export async function serverFetchBlob(...args: FetchParams) { + const response = await serverFetch(...args); + + if (response.ok) { + console.log(response) + const blob = await response.blob() + const blobText = await blob.text(); + const blobType = await blob.type; + return {filename: response.headers.get("filename"), blobText: blobText, blobType: blobType}; + } else { + switch (response.status) { + case 401: + signOutUser(); + default: + console.error(await response.text()); + throw Error("Something went wrong fetching data in server."); + } + } +} + export const signOutUser = () => { const headersList = headers(); const referer = headersList.get("referer"); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 314ea63..3d6123a 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -28,6 +28,7 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/position": "Position", "/settings/position/new": "Create Position", "/settings/salarys": "Salary", + "/analytics/EX02ProjectCashFlowReport": "EX02 - Project Cash Flow Report", }; const Breadcrumb = () => { diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimDetail/ClaimDetail.tsx index db74447..54a82a5 100644 --- a/src/components/ClaimDetail/ClaimDetail.tsx +++ b/src/components/ClaimDetail/ClaimDetail.tsx @@ -75,9 +75,10 @@ const ClaimDetail: React.FC = ({ projectCombo }) => { const formData = new FormData() formData.append("expenseType", data.expenseType) data.addClaimDetails.forEach((claimDetail) => { + console.log(claimDetail) formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) - formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project.id)) + formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project)) formData.append("addClaimDetailDescriptions", claimDetail.description) formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) diff --git a/src/components/ClaimDetail/ClaimFormInputGrid.tsx b/src/components/ClaimDetail/ClaimFormInputGrid.tsx index 24807bd..6aac620 100644 --- a/src/components/ClaimDetail/ClaimFormInputGrid.tsx +++ b/src/components/ClaimDetail/ClaimFormInputGrid.tsx @@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC = ({ flex: 1, editable: true, type: "singleSelect", - getOptionLabel: (value: any) => { + getOptionLabel: (value: ProjectCombo) => { return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; }, - getOptionValue: (value: any) => value, + getOptionValue: (value: ProjectCombo) => value.id, valueOptions: () => { const options = projectCombo ?? [] if (options.length === 0) { options.push({ id: -1, code: "", name: "No Projects" }) } - return options; + + return options as ProjectCombo[]; }, valueGetter: (params) => { - return params.value ?? projectCombo[0].id ?? -1 + return params.value ?? projectCombo[0] ?? { id: -1, code: "", name: "No Projects" } as ProjectCombo }, }, { diff --git a/src/components/ClaimSearch/ClaimSearch.tsx b/src/components/ClaimSearch/ClaimSearch.tsx index c0ab01f..304993a 100644 --- a/src/components/ClaimSearch/ClaimSearch.tsx +++ b/src/components/ClaimSearch/ClaimSearch.tsx @@ -50,12 +50,12 @@ const ClaimSearch: React.FC = ({ claims }) => { const columns = useMemo[]>( () => [ - // { - // name: "action", - // label: t("Actions"), - // onClick: onClaimClick, - // buttonIcon: , - // }, + { + name: "id", + label: t("Details"), + onClick: onClaimClick, + buttonIcon: , + }, { name: "created", label: t("Creation Date"), type: "date" }, { name: "code", label: t("Claim Code") }, // { name: "project", label: t("Related Project Name") }, diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx new file mode 100644 index 0000000..d0bc75f --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { EX02ProjectCashFlowReportFilter } from "@/app/api/reports"; +import { fetchEX02ProjectCashFlowReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; + +interface Props { + projects: ProjectResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { + const { t } = useTranslation(); + const projectCombo = projects.map(project => `${project.code} - ${project.name}`) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Project"), paramName: "project", type: "select", options: projectCombo}, + ], + [t], + ); + + return ( + <> + { + const projectIndex = projectCombo.findIndex(project => project === query.project) + const response = await fetchEX02ProjectCashFlowReport({projectId: projects[projectIndex].id}) + console.log(response) + if (response) { + downloadFile(response.blobText, response.blobType, response.filename!!) + } + + // const url = URL.createObjectURL(response.blob); + // const link = document.createElement("a"); + // link.href = url; + // link.setAttribute("download", "abc.xlsx"); + // link.click(); + }} + /> + + ); +}; + +export default GenerateEX02ProjectCashFlowReport; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx new file mode 100644 index 0000000..1792221 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.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 GenerateEX02ProjectCashFlowReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default GenerateEX02ProjectCashFlowReportLoading; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx new file mode 100644 index 0000000..5bf1089 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import GenerateEX02ProjectCashFlowReportLoading from "./GenerateEX02ProjectCashFlowReportLoading"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "./GenerateEX02ProjectCashFlowReport"; + +interface SubComponents { + Loading: typeof GenerateEX02ProjectCashFlowReportLoading; +} + +const GenerateEX02ProjectCashFlowReportWrapper: React.FC & SubComponents = async () => { + const projects = await fetchProjects(); + + return ; +}; + +GenerateEX02ProjectCashFlowReportWrapper.Loading = GenerateEX02ProjectCashFlowReportLoading; + +export default GenerateEX02ProjectCashFlowReportWrapper; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/index.ts b/src/components/GenerateEX02ProjectCashFlowReport/index.ts new file mode 100644 index 0000000..b547e33 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateEX02ProjectCashFlowReportWrapper"; \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 9016052..aef7c0d 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -110,6 +110,7 @@ const navigationItems: NavigationItem[] = [ {icon: , label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, + {icon: , label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"}, ], }, { diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json index 51f657c..b5b5e42 100644 --- a/src/i18n/en/claim.json +++ b/src/i18n/en/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "Please ensure the projects are selected", "Please ensure the amount are correct": "Please ensure the amount are correct", + "Details": "Details", "Description": "Description", "Actions": "Actions" } \ No newline at end of file diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json new file mode 100644 index 0000000..e7e61fb --- /dev/null +++ b/src/i18n/en/report.json @@ -0,0 +1,3 @@ +{ + "Project": "Project" +} \ No newline at end of file diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json index 92e5b7e..9e2de7e 100644 --- a/src/i18n/zh/claim.json +++ b/src/i18n/zh/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "請確保所有項目欄位已選擇", "Please ensure the amount are correct": "請確保所有金額輸入正確", + "Details": "詳請", "Description": "描述", "Actions": "行動" } \ No newline at end of file diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json new file mode 100644 index 0000000..a6257cf --- /dev/null +++ b/src/i18n/zh/report.json @@ -0,0 +1,3 @@ +{ + "Project": "項目" +} \ No newline at end of file