@@ -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": "項目" | |||||
} |