diff --git a/public/temp/EX01_Financial Status Report.xlsx b/public/temp/EX01_Financial Status Report.xlsx new file mode 100644 index 0000000..bd1b55d Binary files /dev/null and b/public/temp/EX01_Financial Status Report.xlsx differ diff --git a/src/app/(main)/analytics/FinancialStatusReport/page.tsx b/src/app/(main)/analytics/FinancialStatusReport/page.tsx new file mode 100644 index 0000000..0a3865d --- /dev/null +++ b/src/app/(main)/analytics/FinancialStatusReport/page.tsx @@ -0,0 +1,24 @@ +//src\app\(main)\analytics\DelayReport\page.tsx +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import FinancialStatusReportComponent from "@/components/Report/FinancialStatusReport"; + +export const metadata: Metadata = { + title: "Financial Status Report", +}; + +const ProjectFinancialStatusReport: React.FC = () => { + return ( + + + Financial Status Report + + {/* }> + + */} + + + ); +}; +export default ProjectFinancialStatusReport; diff --git a/src/app/api/reporte1/index.ts b/src/app/api/reporte1/index.ts new file mode 100644 index 0000000..5e27648 --- /dev/null +++ b/src/app/api/reporte1/index.ts @@ -0,0 +1,42 @@ +//src\app\api\report\index.ts +import { cache } from "react"; + +export interface FinancialStatus { + 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 = () => { + fetchProjectsFinancialStatus(); +}; + +export const fetchProjectsFinancialStatus = cache(async () => { + return mockProjects; +}); + +const mockProjects: FinancialStatus[] = [ + { + 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", + status: "1", + }, +]; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index ad68823..45bfda9 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -108,6 +108,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:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, ], }, { diff --git a/src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx b/src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx new file mode 100644 index 0000000..893b0ba --- /dev/null +++ b/src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx @@ -0,0 +1,17 @@ +//src\components\DelayReport\DelayReport.tsx +"use client"; +import * as React from "react"; +import "../../../app/global.css"; +import { Suspense } from "react"; +import FinancialStatusReportGen from "@/components/Report/FinancialStatusReportGen"; + +const FinancialStatusReport: React.FC = () => { + + return ( + }> + + + ); +}; + +export default FinancialStatusReport; \ No newline at end of file diff --git a/src/components/Report/FinancialStatusReport/index.ts b/src/components/Report/FinancialStatusReport/index.ts new file mode 100644 index 0000000..4500704 --- /dev/null +++ b/src/components/Report/FinancialStatusReport/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReport\index.ts +export { default } from "./FinancialStatusReport"; diff --git a/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx new file mode 100644 index 0000000..bf4a7cd --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx @@ -0,0 +1,43 @@ +//src\components\LateStartReportGen\LateStartReportGen.tsx +"use client"; +import React, { useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../ReportSearchBoxe1"; +import { useTranslation } from "react-i18next"; +import { FinancialStatus } from "@/app/api/reporte1"; +//import { DownloadReportButton } from './DownloadReportButton'; +interface Props { + projects: FinancialStatus[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ProgressByClientSearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "{Project Code}", paramName: "projectCode", type: "select", options: ["M1234", "M1268", "M1352", "M1393"] }, + // { + // label: "Status", + // label2: "Remained Date To", + // paramName: "targetEndDate", + // type: "dateRange", + // }, + ], + [t], + ); + + return ( + <> + { + console.log(query); + }} + /> + {/* */} + + ); +}; + +export default ProgressByClientSearch; diff --git a/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx new file mode 100644 index 0000000..6d992f9 --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.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 FinancialStatusReportGenLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default FinancialStatusReportGenLoading; diff --git a/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx new file mode 100644 index 0000000..e82ea4b --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx @@ -0,0 +1,19 @@ +//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx +import { fetchProjectsFinancialStatus } from "@/app/api/reporte1"; +import React from "react"; +import FinancialStatusReportGen from "./FinancialStatusReportGen"; +import FinancialStatusReportGenLoading from "./FinancialStatusReportGenLoading"; + +interface SubComponents { + Loading: typeof FinancialStatusReportGenLoading; +} + +const FinancialStatusReportGenWrapper: React.FC & SubComponents = async () => { + const clentprojects = await fetchProjectsFinancialStatus(); + + return ; +}; + +FinancialStatusReportGenWrapper.Loading = FinancialStatusReportGenLoading; + +export default FinancialStatusReportGenWrapper; \ No newline at end of file diff --git a/src/components/Report/FinancialStatusReportGen/index.ts b/src/components/Report/FinancialStatusReportGen/index.ts new file mode 100644 index 0000000..d53d85b --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/index.ts @@ -0,0 +1,2 @@ +//src\components\DelayReportGen\index.ts +export { default } from "./FinancialStatusReportGenWrapper"; diff --git a/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx b/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx new file mode 100644 index 0000000..ea7afb6 --- /dev/null +++ b/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx @@ -0,0 +1,482 @@ +//src\components\ReportSearchBox\SearchBox2.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"), + })); + }; + }, []); + + interface CellValue { + v: number | string; // Value of the cell + t: 'n' | 's'; // Type of the cell value: 'n' for number, 's' for string + s?: XLSX.CellStyle; // Optional style for the cell + } + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + // Function to merge cells from A2:B2 to A14:B14 +function mergeCells(worksheet: XLSX.WorkSheet) { + // Ensure the 'merges' array exists in the worksheet + if (!worksheet['!merges']) worksheet['!merges'] = []; + + // Loop through rows 2 to 14 (0-indexed + 1) + for (let row = 1; row <= 13; row++) { + // Define the range for current row to merge A and B columns + const mergeRange = { + s: { c: 0, r: row }, // Start cell (Column A) + e: { c: 1, r: row } // End cell (Column B) + }; + // Add the range to the 'merges' array in the worksheet + worksheet['!merges'].push(mergeRange); + // Apply center alignment to the merged cell + const mergedCellRef = XLSX.utils.encode_cell({ c: 0, r: row }); + if (!worksheet[mergedCellRef]) { + worksheet[mergedCellRef] = {}; // Create the cell if it doesn't exist + } + worksheet[mergedCellRef].s = { + alignment: { horizontal: "left", wrapText: true } + }; + } +} + +// Processing and inserting table data with calculations +function processDataAndInsert(worksheet: XLSX.WorkSheet, startRow:number, data:(string|number)[][]) { + data.forEach((row, rowIndex) => { + const r = startRow + rowIndex; + + // Direct assignments for columns A-F as strings + const stringCols = ['A', 'B', 'C', 'D', 'E', 'F']; + stringCols.forEach((col, index) => { + const cellRef = col + r; + worksheet[cellRef] = { v: row[index], t: 's' }; // Force type as string + }); + + // Assignments for columns G-O as numbers + const numberCols = ['G', 'H', 'I', 'K', 'N']; + const colIndices = [6, 7, 8, 9, 10]; // Indices in the data array corresponding to G, H, I, K, N + numberCols.forEach((col, index) => { + const cellRef = col + r; + worksheet[cellRef] = { v: row[colIndices[index]], t: 'n' }; // Force type as number + }); + + // Calculations for columns J, L, M, O + const h = row[6] as number; + const i = row[7] as number; + const k = row[9] as number; + const n = row[10] as number; + + // Column J: H - I + worksheet['J' + r] = { v: h - i, t: 'n' }; + + // Column L: IF(H { + const cellRefs = data.map((_, index) => col + (startRow + index)); + const formula = `=SUM(${cellRefs.join(',')})`; + worksheet[col + sumRow] = { f: formula, t: 'n', s: { + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}} + } + }}; + }); + XLSX.utils.sheet_add_aoa(worksheet, [['Sub-total']], { origin: { c: 0, r: (sumRow-1) } }); + +// const mergedCellRefA1 = XLSX.utils.encode_cell({ c: 0, r: sumRow-1}); +// if (!worksheet[mergedCellRefA1]) { +// worksheet[mergedCellRefA1] = {}; // Create the cell if it doesn't exist +// } +// // Apply right alignment, center vertical alignment, wrap text, and border styles to the 'Sub-total' cell +// worksheet[mergedCellRefA1].s = { +// alignment: {horizontal: "right", vertical: "center", wrapText: true}, +// border: { +// top: {style: 'thin', color: {auto: 1}}, +// bottom: {style: 'double', color: {auto: 1}} +// } +// }; +// Define the range of cells to merge for 'Sub-total' +const mergeRangeSubTotal = { + s: { c: 0, r: sumRow-1}, // Start at column A + e: { c: 5, r: sumRow-1} // End at column F +}; +// // Add the range to the 'merges' array in the worksheet if it doesn't exist +// if (!worksheet['!merges']) worksheet['!merges'] = []; +// worksheet['!merges'].push(mergeRangeSubTotal); + +// Update styles for the merged cell range where 'Sub-total' is located +const mergedCellRefSubTotal = XLSX.utils.encode_cell({ c: 0, r: sumRow-1 }); +if (!worksheet[mergedCellRefSubTotal]) { + worksheet[mergedCellRefSubTotal] = {}; // Create the cell if it doesn't exist +} +worksheet[mergedCellRefSubTotal].s = { + alignment: {horizontal: "right", vertical: "center", wrapText: true}, + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}}} +}; +// Add the range to the 'merges' array in the worksheet if it doesn't exist +if (!worksheet['!merges']) worksheet['!merges'] = []; +worksheet['!merges'].push(mergeRangeSubTotal) + + +const mergedCellRefM1 = XLSX.utils.encode_cell({ c: 12, r: sumRow}); +if (!worksheet[mergedCellRefM1]) { + worksheet[mergedCellRefM1] = {}; // Create the cell if it doesn't exist +} +worksheet[mergedCellRefM1].s = { + alignment: {horizontal: "right", vertical: "center", wrapText: true}, + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}} + } +}; + +} +const firstTableData = [ + ['Code1', 'PJName1', 'Client1','Team1','2011/01/01','2011/02/01','625','500','350','350','171'], // Row 1 + ['Code2', 'PJName2', 'Client2','Team2','2011/03/01','2011/04/01','1000','800','565','565','565'],// Row 2 + ['Code2', 'PJName2', 'Client2','Team2','2011/03/01','2011/04/01','1000','800','565','565','565'],// Row 3 + // ... more rows as needed +]; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/EX01_Financial Status Report.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 }); + + mergeCells(worksheet); + + // Style for cell A1: Font size 16 and bold + if (worksheet['A1']) { + worksheet['A1'].s = { + font: {bold: true,sz: 16,},alignment: { horizontal: 'center' } // Font size 16 //name: 'Times New Roman' // Specify font + }; + } + + // Apply styles from A2 A3 A5 (bold) + ['A2', 'A3', 'A5','A14'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: true },alignment: { horizontal: 'left' } }; + } + }); + + // Apply styles from A2 A3 A5 (bold) + ['A6', 'A7', 'A8','A9','A10','A11','A12'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: false },alignment: { horizontal: 'left' } }; + } + }); + + // Formatting from A15 to O15 + // Apply styles from A6 to K6 (bold, bottom border, center alignment) + for (let col = 0; col < 15; col++) { // Columns A to O + const cellRef = XLSX.utils.encode_col(col) + '15'; + if (worksheet[cellRef]) { + worksheet[cellRef].s = { + font: { bold: true }, + alignment: { horizontal: 'center' }, + border: { + bottom: { style: 'thin', color: { auto: 1 } } + } + }; + } + } + + // Find the last row of the first table + let lastRowOfFirstTable = 16; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Insert the first data form into the worksheet at the desired location + //XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Assuming worksheet is already defined, and we start inserting from row 16 +processDataAndInsert(worksheet, 16, firstTableData); + // Update lastRowOfFirstTable to account for the new data + lastRowOfFirstTable += firstTableData.length; + // Now insert the text that goes between the two tables + + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + // Start with a base width for each column (optional, but can help with columns that have no data) + // Check if worksheet['!ref'] is defined to prevent errors + const maxCol = worksheet['!ref'] ? worksheet['!ref'].split(':')[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1 : 0; + for (let col = 0; col < maxCol; col++) { + colWidths[col] = 10; // Default base width + } + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + + // Skip the first row in the jsonData + for (let row = 1; row < jsonData.length; row++) { + jsonData[row].forEach((cell, index) => { + // Only process if the cell is not null/undefined + if (cell) { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + } + }); + } + + // Check if worksheet exists before setting '!cols' + if (worksheet) { + worksheet['!cols'] = colWidths.map((width) => ({ wch: width + 2 })); // +2 for a little extra padding + } + + // 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 = `EX01_Financial_Status_Report_${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/ReportSearchBoxe1/index.ts b/src/components/Report/ReportSearchBoxe1/index.ts new file mode 100644 index 0000000..8f4d421 --- /dev/null +++ b/src/components/Report/ReportSearchBoxe1/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBoxe1"; +export type { Criterion } from "./SearchBoxe1";