| @@ -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 ( | |||||
| <> | |||||
| <I18nProvider namespaces={["report", "common"]}> | |||||
| <Suspense fallback={<GenerateEX02ProjectCashFlowReport.Loading />}> | |||||
| <GenerateEX02ProjectCashFlowReport /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default ProjectCashFlowReport; | |||||
| @@ -21,7 +21,7 @@ export interface ClaimDetailTable { | |||||
| id: number; | id: number; | ||||
| invoiceDate: Date; | invoiceDate: Date; | ||||
| description: string; | description: string; | ||||
| project: ProjectCombo; | |||||
| project: number; | |||||
| amount: number; | amount: number; | ||||
| supportingDocumentName: string; | supportingDocumentName: string; | ||||
| oldSupportingDocument: SupportingDocument; | oldSupportingDocument: SupportingDocument; | ||||
| @@ -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 | |||||
| }; | |||||
| @@ -0,0 +1,8 @@ | |||||
| // EX02 - Project Cash Flow Report | |||||
| export interface EX02ProjectCashFlowReportFilter { | |||||
| project: string[]; | |||||
| } | |||||
| export interface EX02ProjectCashFlowReportRequest { | |||||
| projectId: number; | |||||
| } | |||||
| @@ -20,4 +20,20 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str | |||||
| return true | 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(); | |||||
| } | } | ||||
| @@ -14,9 +14,9 @@ export const serverFetch: typeof fetch = async (input, init) => { | |||||
| ...init?.headers, | ...init?.headers, | ||||
| ...(accessToken | ...(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 = () => { | export const signOutUser = () => { | ||||
| const headersList = headers(); | const headersList = headers(); | ||||
| const referer = headersList.get("referer"); | const referer = headersList.get("referer"); | ||||
| @@ -28,6 +28,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
| "/settings/position": "Position", | "/settings/position": "Position", | ||||
| "/settings/position/new": "Create Position", | "/settings/position/new": "Create Position", | ||||
| "/settings/salarys": "Salary", | "/settings/salarys": "Salary", | ||||
| "/analytics/EX02ProjectCashFlowReport": "EX02 - Project Cash Flow Report", | |||||
| }; | }; | ||||
| const Breadcrumb = () => { | const Breadcrumb = () => { | ||||
| @@ -75,9 +75,10 @@ const ClaimDetail: React.FC<Props> = ({ projectCombo }) => { | |||||
| const formData = new FormData() | const formData = new FormData() | ||||
| formData.append("expenseType", data.expenseType) | formData.append("expenseType", data.expenseType) | ||||
| data.addClaimDetails.forEach((claimDetail) => { | data.addClaimDetails.forEach((claimDetail) => { | ||||
| console.log(claimDetail) | |||||
| formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) | formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) | ||||
| formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) | 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("addClaimDetailDescriptions", claimDetail.description) | ||||
| formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) | formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) | ||||
| formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) | formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) | ||||
| @@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC<ClaimFormInputGridProps> = ({ | |||||
| flex: 1, | flex: 1, | ||||
| editable: true, | editable: true, | ||||
| type: "singleSelect", | type: "singleSelect", | ||||
| getOptionLabel: (value: any) => { | |||||
| getOptionLabel: (value: ProjectCombo) => { | |||||
| return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; | return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; | ||||
| }, | }, | ||||
| getOptionValue: (value: any) => value, | |||||
| getOptionValue: (value: ProjectCombo) => value.id, | |||||
| valueOptions: () => { | valueOptions: () => { | ||||
| const options = projectCombo ?? [] | const options = projectCombo ?? [] | ||||
| if (options.length === 0) { | if (options.length === 0) { | ||||
| options.push({ id: -1, code: "", name: "No Projects" }) | options.push({ id: -1, code: "", name: "No Projects" }) | ||||
| } | } | ||||
| return options; | |||||
| return options as ProjectCombo[]; | |||||
| }, | }, | ||||
| valueGetter: (params) => { | valueGetter: (params) => { | ||||
| return params.value ?? projectCombo[0].id ?? -1 | |||||
| return params.value ?? projectCombo[0] ?? { id: -1, code: "", name: "No Projects" } as ProjectCombo | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -50,12 +50,12 @@ const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||||
| const columns = useMemo<Column<Claim>[]>( | const columns = useMemo<Column<Claim>[]>( | ||||
| () => [ | () => [ | ||||
| // { | |||||
| // name: "action", | |||||
| // label: t("Actions"), | |||||
| // onClick: onClaimClick, | |||||
| // buttonIcon: <EditNote />, | |||||
| // }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Details"), | |||||
| onClick: onClaimClick, | |||||
| buttonIcon: <EditNote />, | |||||
| }, | |||||
| { name: "created", label: t("Creation Date"), type: "date" }, | { name: "created", label: t("Creation Date"), type: "date" }, | ||||
| { name: "code", label: t("Claim Code") }, | { name: "code", label: t("Claim Code") }, | ||||
| // { name: "project", label: t("Related Project Name") }, | // { name: "project", label: t("Related Project Name") }, | ||||
| @@ -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<Omit<EX02ProjectCashFlowReportFilter, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const GenerateEX02ProjectCashFlowReport: React.FC<Props> = ({ projects }) => { | |||||
| const { t } = useTranslation(); | |||||
| const projectCombo = projects.map(project => `${project.code} - ${project.name}`) | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: t("Project"), paramName: "project", type: "select", options: projectCombo}, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={async (query) => { | |||||
| 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; | |||||
| @@ -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 ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default GenerateEX02ProjectCashFlowReportLoading; | |||||
| @@ -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 <GenerateEX02ProjectCashFlowReport projects={projects} />; | |||||
| }; | |||||
| GenerateEX02ProjectCashFlowReportWrapper.Loading = GenerateEX02ProjectCashFlowReportLoading; | |||||
| export default GenerateEX02ProjectCashFlowReportWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./GenerateEX02ProjectCashFlowReportWrapper"; | |||||
| @@ -110,6 +110,7 @@ const navigationItems: NavigationItem[] = [ | |||||
| {icon: <Analytics />, label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, | {icon: <Analytics />, label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, | ||||
| {icon: <Analytics />, label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, | {icon: <Analytics />, label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, | ||||
| {icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, | {icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, | ||||
| {icon: <Analytics />, label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"}, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -31,6 +31,7 @@ | |||||
| "Please ensure the projects are selected": "Please ensure the projects are selected", | "Please ensure the projects are selected": "Please ensure the projects are selected", | ||||
| "Please ensure the amount are correct": "Please ensure the amount are correct", | "Please ensure the amount are correct": "Please ensure the amount are correct", | ||||
| "Details": "Details", | |||||
| "Description": "Description", | "Description": "Description", | ||||
| "Actions": "Actions" | "Actions": "Actions" | ||||
| } | } | ||||
| @@ -0,0 +1,3 @@ | |||||
| { | |||||
| "Project": "Project" | |||||
| } | |||||
| @@ -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": "詳請", | |||||
| "Description": "描述", | "Description": "描述", | ||||
| "Actions": "行動" | "Actions": "行動" | ||||
| } | } | ||||
| @@ -0,0 +1,3 @@ | |||||
| { | |||||
| "Project": "項目" | |||||
| } | |||||