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