From a3dd214a99e6d11a8b8378ab920d596b1c2b15a6 Mon Sep 17 00:00:00 2001 From: Efril Date: Thu, 26 Feb 2026 14:02:04 +0700 Subject: [PATCH] update report add standard hpp, realisasi hpp, dan status --- package-lock.json | 42 +++- .../dashboards/daily-report/page.tsx | 152 ++++++++---- src/utils/excelGenerator.ts | 220 +++++++++--------- src/utils/pdfGenerator.ts | 160 +++++++++---- 4 files changed, 377 insertions(+), 197 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5029587..4f64e70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -308,6 +308,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -331,6 +332,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -489,6 +491,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -532,6 +535,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1211,6 +1215,7 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz", "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==", "license": "MIT", + "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -2043,6 +2048,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.1.tgz", "integrity": "sha512-7VlKGsRKsy1bOSOPaSNgpkzaL+0C7iWAVKd2KYyAvhR9fTLJtiAMpq+KuzgEh1so2mtvQERN0tZVIceWMiIesw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.2.1", @@ -2955,6 +2961,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz", "integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.83.1" }, @@ -3021,6 +3028,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.26.1.tgz", "integrity": "sha512-fymyd/XZvYiHjBoLt1gxs024xP/LY26d43R1vluYq7AHBL/7DE3ywzy+1GEsGyAv5Je2L0KBhNIR/izbq3Kaqg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3348,6 +3356,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.26.1.tgz", "integrity": "sha512-t9Nc/UkrbCfnSHEUi1gvUQ2ZPzvfdYFT5TExoV2DTiUCkhG6+mecT5bTVFGW3QkPmbToL+nFhGn4ZRMDD0SP3Q==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3374,6 +3383,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.26.1.tgz", "integrity": "sha512-8aF+mY/vSHbGFqyG663ds84b+vca5Lge3tHdTMTKazxCnhXR9dn2oQJMnZ78YZvdRbkPkMJJHti9h3K7u2UQvw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -3672,6 +3682,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3683,6 +3694,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3780,6 +3792,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -3989,6 +4002,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4084,6 +4098,7 @@ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.0.tgz", "integrity": "sha512-2T9HnbQFLCuYRPndQLmh+bEQFoz0meUbvASaGgiSKDuYhWcLBodJtIpKql2aOtMx4B/sHrWW0dm90HsW4+h2PQ==", "license": "MIT", + "peer": true, "dependencies": { "@yr/monotone-cubic-spline": "^1.0.3", "svg.draggable.js": "^2.2.2", @@ -4747,6 +4762,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -5007,6 +5023,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -6034,7 +6051,8 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -6362,6 +6380,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8793,6 +8812,7 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.7", "atob": "^2.1.2", @@ -9214,6 +9234,7 @@ "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.9.0.tgz", "integrity": "sha512-QKAxLHcbdoqobXuhu2PP6HJDSy0/GhfZuO5O8BrmwfR0ihZbA5ihYD/u0wGqu2QTDWi/DbgCWJIlV2mXh2Sekg==", "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, "workspaces": [ "src/style-spec", "test/build/typings" @@ -9540,6 +9561,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.1.9.tgz", "integrity": "sha512-OoQpDPV2i3o5Hnn46nz2x6fzdFxFO+JsU4ZES12z65/feMjPHKKHLDVQ2NuEvTaXTRisix/G5+6hyTkwK329kA==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.1.9", "@swc/counter": "0.1.3", @@ -10173,6 +10195,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", @@ -10300,6 +10323,7 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10512,6 +10536,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.2.tgz", "integrity": "sha512-BVypCAJ4SL6jOiTsDffP3Wp6wD69lRhI4zg/iT8JXjp3ccZFiq5WyguxvMKmdKFC3prhaig7wSr8dneDToHE1Q==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -10541,6 +10566,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -10589,6 +10615,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.40.1.tgz", "integrity": "sha512-pbwUjt3G7TlsQQHDiYSupWBhJswpLVB09xXm1YiJPdkjkh9Pe7Y51XdLh5VWIZmROLY8UpUpG03lkdhm9lzIBA==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -10715,6 +10742,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10792,6 +10820,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10828,6 +10857,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.1.tgz", "integrity": "sha512-PUNzFwQeQ5oHiiTUO7GO/EJXGEtuun2Y1A59rLnZBBj+vNEOWt/3ERTiG1/zt7dVeJEM+4vDX/7XQ/qanuvPMg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -11184,7 +11214,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -12610,7 +12641,8 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/stylis-plugin-rtl": { "version": "2.1.1", @@ -13438,7 +13470,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.19.2", @@ -13570,6 +13603,7 @@ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx index 935f441..2921923 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx @@ -15,6 +15,23 @@ import ReportGeneratorComponent from '@/views/dashboards/daily-report/report-gen import ReportHeader from '@/views/dashboards/daily-report/report-header' import React, { useEffect, useRef, useState } from 'react' +// Dummy HPP values — ganti dengan data real nantinya +const DUMMY_STD_HPP = 30 // 30% +const DUMMY_REAL_HPP = 28 // 28% + +const getHppStatus = (stdHpp: number, realHpp: number): 'Sehat' | 'Tidak Sehat' => + realHpp <= stdHpp ? 'Sehat' : 'Tidak Sehat' + +const StatusBadge = ({ status }: { status: 'Sehat' | 'Tidak Sehat' }) => ( + + {status} + +) + const DailyPOSReport = () => { const reportRef = useRef(null) @@ -108,11 +125,7 @@ const DailyPOSReport = () => { } const getReportTitle = () => { - if (filterType === 'single') { - return 'Laporan Transaksi' - } else { - return `Laporan Transaksi` - } + return 'Laporan Transaksi' } const handleGeneratePDF = async () => { @@ -342,7 +355,7 @@ const DailyPOSReport = () => { - {/* Category Summary */} + {/* Category Summary — +3 kolom baru */}

Ringkasan Kategori @@ -355,18 +368,31 @@ const DailyPOSReport = () => { Nama Qty Pendapatan + % Std HPP + % Real HPP + Status - {category?.data?.map((c, index) => ( - - {c.category_name} - {c.total_quantity} - - {formatCurrency(c.total_revenue)} - - - )) || []} + {category?.data?.map((c, index) => { + const stdHpp = DUMMY_STD_HPP + const realHpp = DUMMY_REAL_HPP + const status = getHppStatus(stdHpp, realHpp) + return ( + + {c.category_name} + {c.total_quantity} + + {formatCurrency(c.total_revenue)} + + {stdHpp}% + {realHpp}% + + + + + ) + }) || []} @@ -375,13 +401,16 @@ const DailyPOSReport = () => { {formatCurrency(categorySummary?.totalRevenue ?? 0)} + + +

- {/* Product Summary - Dipisah per kategori dengan tabel terpisah */} + {/* Product Summary — +3 kolom baru */}

Ringkasan Item Per Kategori @@ -398,7 +427,6 @@ const DailyPOSReport = () => { }) .map((categoryName, catIndex) => { const categoryProducts = groupedProducts[categoryName].sort((a, b) => { - // Sort by product_sku ASC const skuA = a.product_sku || '' const skuB = b.product_sku || '' return skuA.localeCompare(skuB) @@ -421,31 +449,57 @@ const DailyPOSReport = () => { style={{ borderCollapse: 'collapse', tableLayout: 'fixed', width: '100%' }} > - - - + + + + + + Produk Qty - Pendapatan + + Pendapatan + + + % Std HPP + + + % Real HPP + + Status - {categoryProducts.map((item, index) => ( - - - {item.product_name} - - - {item.quantity_sold} - - - {formatCurrency(item.revenue)} - - - ))} + {categoryProducts.map((item, index) => { + const stdHpp = DUMMY_STD_HPP + const realHpp = DUMMY_REAL_HPP + const status = getHppStatus(stdHpp, realHpp) + return ( + + + {item.product_name} + + + {item.quantity_sold} + + + {formatCurrency(item.revenue)} + + + {stdHpp}% + + + {realHpp}% + + + + + + ) + })} @@ -455,9 +509,12 @@ const DailyPOSReport = () => { {categoryTotalQty} - + {formatCurrency(categoryTotalRevenue)} + + + @@ -468,24 +525,35 @@ const DailyPOSReport = () => { {/* Grand Total */}
- +
+ + + + + + + + - - + + +
+ TOTAL KESELURUHAN {productSummary.totalQuantitySold ?? 0} + {formatCurrency(productSummary.totalRevenue ?? 0)}
diff --git a/src/utils/excelGenerator.ts b/src/utils/excelGenerator.ts index ce9042b..04b0a9b 100644 --- a/src/utils/excelGenerator.ts +++ b/src/utils/excelGenerator.ts @@ -35,7 +35,7 @@ const getReportPeriodText = (params: ExcelGeneratorParams) => { return `${formatDateDDMMYYYY(params.dateRange.startDate)} - ${formatDateDDMMYYYY(params.dateRange.endDate)}` } -// ========== EXCEL STYLES (IMPROVED) ========== +// ========== EXCEL STYLES ========== const headerStyle: Partial = { font: { bold: true, size: 12, color: { argb: 'FFFFFFFF' } }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern, @@ -83,7 +83,6 @@ const dataStyle: Partial = { } } -// Zebra striping untuk data rows const dataStyleAlt: Partial = { ...dataStyle, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFAFAFA' } } as ExcelJS.FillPattern @@ -92,6 +91,35 @@ const dataStyleAlt: Partial = { const currencyFormat = '#,##0' const percentageFormat = '0.0"%"' +// ========== HELPER: STATUS STYLE ========== +// Sehat → teks hijau gelap, background hijau muda +// Tidak Sehat → teks merah gelap, background merah muda +const getStatusStyle = (status: 'Sehat' | 'Tidak Sehat', isAlt: boolean): Partial => { + const baseStyle = isAlt ? dataStyleAlt : dataStyle + if (status === 'Sehat') { + return { + ...baseStyle, + font: { bold: true, size: 11, color: { argb: 'FF166534' } }, // green-800 + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFDCFCE7' } } as ExcelJS.FillPattern, // green-100 + alignment: { horizontal: 'center', vertical: 'middle' }, + border: dataStyle.border + } + } + return { + ...baseStyle, + font: { bold: true, size: 11, color: { argb: 'FF991B1B' } }, // red-800 + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEE2E2' } } as ExcelJS.FillPattern, // red-100 + alignment: { horizontal: 'center', vertical: 'middle' }, + border: dataStyle.border + } +} + +// Dummy HPP values — ganti dengan data real nantinya +const DUMMY_STD_HPP = 0.3 // 30% +const DUMMY_REAL_HPP = 0.28 // 28% +const getDummyStatus = (stdHpp: number, realHpp: number): 'Sehat' | 'Tidak Sehat' => + realHpp <= stdHpp ? 'Sehat' : 'Tidak Sehat' + export const generateExcel = async (params: ExcelGeneratorParams) => { const { outlet, @@ -109,7 +137,6 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { const ExcelJS = await import('exceljs') const workbook = new ExcelJS.Workbook() - // Metadata workbook.creator = outlet?.name || 'POS System' workbook.created = new Date() @@ -118,7 +145,6 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { views: [{ showGridLines: false }] }) - // Title ws1.mergeCells('A1:B1') const titleCell = ws1.getCell('A1') titleCell.value = 'LAPORAN TRANSAKSI' @@ -128,7 +154,6 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { } ws1.getRow(1).height = 30 - // Outlet Info ws1.getCell('A2').value = outlet?.name || '' ws1.getCell('A2').style = { ...subtitleStyle, font: { size: 12, color: { argb: 'FF4B5563' } } } ws1.getCell('A3').value = outlet?.address || '' @@ -141,21 +166,18 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { ws1.getCell('A5').style = labelStyle ws1.getCell('B5').style = { font: { size: 11 } } - // Section: Ringkasan ws1.getCell('A7').value = 'RINGKASAN' ws1.getCell('A7').style = { font: { bold: true, size: 14, color: { argb: 'FF36175E' } }, alignment: { vertical: 'middle', horizontal: 'left' } } - // Header row ws1.getCell('A8').value = 'Keterangan' ws1.getCell('B8').value = 'Jumlah (IDR)' ws1.getCell('A8').style = headerStyle ws1.getCell('B8').style = headerStyle ws1.getRow(8).height = 25 - // Data rows const summaryData = [ ['Total Penjualan', profitLoss?.summary.total_revenue || 0], ['Total Diskon', profitLoss?.summary.total_discount || 0], @@ -168,33 +190,20 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { const rowNum = idx + 9 ws1.getCell(`A${rowNum}`).value = row[0] ws1.getCell(`B${rowNum}`).value = row[1] - const isAlt = idx % 2 === 1 const baseStyle = isAlt ? dataStyleAlt : dataStyle - ws1.getCell(`A${rowNum}`).style = { ...baseStyle, alignment: { horizontal: 'left' } } - ws1.getCell(`B${rowNum}`).style = { - ...baseStyle, - alignment: { horizontal: 'right' }, - numFmt: currencyFormat - } - + ws1.getCell(`B${rowNum}`).style = { ...baseStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat } ws1.getRow(rowNum).height = 22 }) - // Total row const totalRow = 9 + summaryData.length ws1.getCell(`A${totalRow}`).value = 'Total' ws1.getCell(`B${totalRow}`).value = profitLoss?.summary.total_revenue || 0 ws1.getCell(`A${totalRow}`).style = { ...totalRowStyle, alignment: { horizontal: 'left' } } - ws1.getCell(`B${totalRow}`).style = { - ...totalRowStyle, - alignment: { horizontal: 'right' }, - numFmt: currencyFormat - } + ws1.getCell(`B${totalRow}`).style = { ...totalRowStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat } ws1.getRow(totalRow).height = 25 - // Invoice section (pisah dari tabel ringkasan) const invoiceRow = totalRow + 2 ws1.getCell(`A${invoiceRow}`).value = 'INVOICE' ws1.getCell(`A${invoiceRow}`).style = { @@ -207,7 +216,6 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { ws1.getCell(`A${invoiceRow + 1}`).style = { ...dataStyle, font: { bold: true } } ws1.getCell(`B${invoiceRow + 1}`).style = { ...dataStyle, alignment: { horizontal: 'right' }, font: { bold: true } } - // Column widths ws1.getColumn(1).width = 28 ws1.getColumn(2).width = 22 @@ -216,14 +224,12 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { views: [{ showGridLines: false }] }) - // Title ws2.mergeCells('A1:D1') const paymentTitle = ws2.getCell('A1') paymentTitle.value = 'RINGKASAN METODE PEMBAYARAN' paymentTitle.style = titleStyle ws2.getRow(1).height = 30 - // Headers const paymentHeaders = ['Metode', 'Jumlah Order', 'Total Amount (IDR)', 'Persentase'] paymentHeaders.forEach((header, idx) => { const cell = ws2.getCell(3, idx + 1) @@ -232,7 +238,6 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { }) ws2.getRow(3).height = 25 - // Data let paymentRow = 4 paymentAnalytics?.data?.forEach((payment: any, idx: number) => { const isAlt = idx % 2 === 1 @@ -245,57 +250,41 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { ws2.getCell(paymentRow, 1).style = { ...baseStyle, alignment: { horizontal: 'left' } } ws2.getCell(paymentRow, 2).style = { ...baseStyle, alignment: { horizontal: 'center' } } - ws2.getCell(paymentRow, 3).style = { - ...baseStyle, - alignment: { horizontal: 'right' }, - numFmt: currencyFormat - } - ws2.getCell(paymentRow, 4).style = { - ...baseStyle, - alignment: { horizontal: 'center' }, - numFmt: percentageFormat - } + ws2.getCell(paymentRow, 3).style = { ...baseStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat } + ws2.getCell(paymentRow, 4).style = { ...baseStyle, alignment: { horizontal: 'center' }, numFmt: percentageFormat } ws2.getRow(paymentRow).height = 22 paymentRow++ }) - // Total row ws2.getCell(paymentRow, 1).value = 'TOTAL' ws2.getCell(paymentRow, 2).value = paymentAnalytics?.summary.total_orders || 0 ws2.getCell(paymentRow, 3).value = paymentAnalytics?.summary.total_amount || 0 ws2.getCell(paymentRow, 4).value = '' - ws2.getCell(paymentRow, 1).style = { ...totalRowStyle, alignment: { horizontal: 'left' } } ws2.getCell(paymentRow, 2).style = { ...totalRowStyle, alignment: { horizontal: 'center' } } - ws2.getCell(paymentRow, 3).style = { - ...totalRowStyle, - alignment: { horizontal: 'right' }, - numFmt: currencyFormat - } + ws2.getCell(paymentRow, 3).style = { ...totalRowStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat } ws2.getCell(paymentRow, 4).style = totalRowStyle ws2.getRow(paymentRow).height = 25 - // Column widths ws2.getColumn(1).width = 28 ws2.getColumn(2).width = 16 ws2.getColumn(3).width = 22 ws2.getColumn(4).width = 16 // ============ SHEET 3: KATEGORI ============ + // + 3 kolom baru: % Standard HPP | % Realisasi HPP | Status const ws3 = workbook.addWorksheet('Kategori', { views: [{ showGridLines: false }] }) - // Title - ws3.mergeCells('A1:C1') + ws3.mergeCells('A1:F1') const categoryTitle = ws3.getCell('A1') categoryTitle.value = 'RINGKASAN KATEGORI' categoryTitle.style = titleStyle ws3.getRow(1).height = 30 - // Headers - const categoryHeaders = ['Nama', 'Qty', 'Pendapatan (IDR)'] + const categoryHeaders = ['Nama', 'Qty', 'Pendapatan (IDR)', '% Standard HPP', '% Realisasi HPP', 'Status'] categoryHeaders.forEach((header, idx) => { const cell = ws3.getCell(3, idx + 1) cell.value = header @@ -303,67 +292,72 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { }) ws3.getRow(3).height = 25 - // Data let categoryRow = 4 category?.data?.forEach((cat: any, idx: number) => { const isAlt = idx % 2 === 1 const baseStyle = isAlt ? dataStyleAlt : dataStyle + const stdHpp = DUMMY_STD_HPP + const realHpp = DUMMY_REAL_HPP + const status = getDummyStatus(stdHpp, realHpp) ws3.getCell(categoryRow, 1).value = cat.category_name ws3.getCell(categoryRow, 2).value = cat.total_quantity ws3.getCell(categoryRow, 3).value = cat.total_revenue + ws3.getCell(categoryRow, 4).value = stdHpp // akan diformat sebagai persentase + ws3.getCell(categoryRow, 5).value = realHpp + ws3.getCell(categoryRow, 6).value = status ws3.getCell(categoryRow, 1).style = { ...baseStyle, alignment: { horizontal: 'left' } } ws3.getCell(categoryRow, 2).style = { ...baseStyle, alignment: { horizontal: 'center' } } - ws3.getCell(categoryRow, 3).style = { - ...baseStyle, - alignment: { horizontal: 'right' }, - numFmt: currencyFormat - } + ws3.getCell(categoryRow, 3).style = { ...baseStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat } + ws3.getCell(categoryRow, 4).style = { ...baseStyle, alignment: { horizontal: 'center' }, numFmt: '0.0"%"' } + ws3.getCell(categoryRow, 5).style = { ...baseStyle, alignment: { horizontal: 'center' }, numFmt: '0.0"%"' } + ws3.getCell(categoryRow, 6).style = getStatusStyle(status, isAlt) ws3.getRow(categoryRow).height = 22 categoryRow++ }) - // Total row + // Total row (3 kolom baru dikosongkan) ws3.getCell(categoryRow, 1).value = 'TOTAL' ws3.getCell(categoryRow, 2).value = categorySummary.totalQuantity ws3.getCell(categoryRow, 3).value = categorySummary.totalRevenue + ws3.getCell(categoryRow, 4).value = '' + ws3.getCell(categoryRow, 5).value = '' + ws3.getCell(categoryRow, 6).value = '' ws3.getCell(categoryRow, 1).style = { ...totalRowStyle, alignment: { horizontal: 'left' } } ws3.getCell(categoryRow, 2).style = { ...totalRowStyle, alignment: { horizontal: 'center' } } - ws3.getCell(categoryRow, 3).style = { - ...totalRowStyle, - alignment: { horizontal: 'right' }, - numFmt: currencyFormat - } + ws3.getCell(categoryRow, 3).style = { ...totalRowStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat } + ws3.getCell(categoryRow, 4).style = totalRowStyle + ws3.getCell(categoryRow, 5).style = totalRowStyle + ws3.getCell(categoryRow, 6).style = totalRowStyle ws3.getRow(categoryRow).height = 25 - // Column widths ws3.getColumn(1).width = 35 ws3.getColumn(2).width = 12 ws3.getColumn(3).width = 22 + ws3.getColumn(4).width = 18 // % Standard HPP + ws3.getColumn(5).width = 18 // % Realisasi HPP + ws3.getColumn(6).width = 16 // Status // ============ SHEET 4: DETAIL PRODUK ============ + // + 3 kolom baru: % Standard HPP | % Realisasi HPP | Status const ws4 = workbook.addWorksheet('Detail Produk', { views: [{ showGridLines: false }] }) - // Title - ws4.mergeCells('A1:C1') + ws4.mergeCells('A1:F1') const productTitle = ws4.getCell('A1') productTitle.value = 'RINGKASAN ITEM PER KATEGORI' productTitle.style = titleStyle ws4.getRow(1).height = 30 - // Group products by category const groupedProducts = products?.data?.reduce( (acc: any, item: any) => { const categoryName = item.category_name || 'Tidak Berkategori' - if (!acc[categoryName]) { - acc[categoryName] = [] - } + if (!acc[categoryName]) acc[categoryName] = [] acc[categoryName].push(item) return acc }, @@ -372,18 +366,15 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { let currentRow = 3 - // Loop through each category Object.keys(groupedProducts) .sort((a, b) => { - const productsA = groupedProducts[a] - const productsB = groupedProducts[b] - const orderA = productsA[0]?.category_order ?? 999 - const orderB = productsB[0]?.category_order ?? 999 + const orderA = groupedProducts[a][0]?.category_order ?? 999 + const orderB = groupedProducts[b][0]?.category_order ?? 999 return orderA - orderB }) .forEach((categoryName, index) => { - // Category header - ws4.mergeCells(`A${currentRow}:C${currentRow}`) + // Category header — span 6 kolom + ws4.mergeCells(`A${currentRow}:F${currentRow}`) const catHeader = ws4.getCell(`A${currentRow}`) catHeader.value = `${index + 1}. ${categoryName.toUpperCase()}` catHeader.style = { @@ -400,8 +391,8 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { ws4.getRow(currentRow).height = 28 currentRow++ - // Column headers - const prodHeaders = ['Produk', 'Qty', 'Pendapatan (IDR)'] + // Column headers — 6 kolom + const prodHeaders = ['Produk', 'Qty', 'Pendapatan (IDR)', '% Standard HPP', '% Realisasi HPP', 'Status'] prodHeaders.forEach((header, idx) => { const cell = ws4.getCell(currentRow, idx + 1) cell.value = header @@ -420,93 +411,104 @@ export const generateExcel = async (params: ExcelGeneratorParams) => { ws4.getRow(currentRow).height = 24 currentRow++ - // Sort products - const categoryProducts = groupedProducts[categoryName].sort((a: any, b: any) => { - const skuA = a.product_sku || '' - const skuB = b.product_sku || '' - return skuA.localeCompare(skuB) - }) + const categoryProducts = groupedProducts[categoryName].sort((a: any, b: any) => + (a.product_sku || '').localeCompare(b.product_sku || '') + ) - // Add products with zebra striping categoryProducts.forEach((product: any, idx: number) => { const isAlt = idx % 2 === 1 const baseStyle = isAlt ? dataStyleAlt : dataStyle + const stdHpp = DUMMY_STD_HPP + const realHpp = DUMMY_REAL_HPP + const status = getDummyStatus(stdHpp, realHpp) ws4.getCell(currentRow, 1).value = product.product_name ws4.getCell(currentRow, 2).value = product.quantity_sold ws4.getCell(currentRow, 3).value = product.revenue + ws4.getCell(currentRow, 4).value = stdHpp + ws4.getCell(currentRow, 5).value = realHpp + ws4.getCell(currentRow, 6).value = status ws4.getCell(currentRow, 1).style = { ...baseStyle, alignment: { horizontal: 'left' } } ws4.getCell(currentRow, 2).style = { ...baseStyle, alignment: { horizontal: 'center' } } - ws4.getCell(currentRow, 3).style = { - ...baseStyle, - alignment: { horizontal: 'right' }, - numFmt: currencyFormat - } + ws4.getCell(currentRow, 3).style = { ...baseStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat } + ws4.getCell(currentRow, 4).style = { ...baseStyle, alignment: { horizontal: 'center' }, numFmt: '0.0"%"' } + ws4.getCell(currentRow, 5).style = { ...baseStyle, alignment: { horizontal: 'center' }, numFmt: '0.0"%"' } + ws4.getCell(currentRow, 6).style = getStatusStyle(status, isAlt) ws4.getRow(currentRow).height = 20 currentRow++ }) - // Subtotal + // Subtotal — 3 kolom baru dikosongkan const subQty = categoryProducts.reduce((sum: number, p: any) => sum + p.quantity_sold, 0) const subRevenue = categoryProducts.reduce((sum: number, p: any) => sum + p.revenue, 0) + const subtotalFill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern ws4.getCell(currentRow, 1).value = `Subtotal ${categoryName}` ws4.getCell(currentRow, 2).value = subQty ws4.getCell(currentRow, 3).value = subRevenue + ws4.getCell(currentRow, 4).value = '' + ws4.getCell(currentRow, 5).value = '' + ws4.getCell(currentRow, 6).value = '' - ws4.getCell(currentRow, 1).style = { - ...totalRowStyle, - alignment: { horizontal: 'left' }, - fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern - } - ws4.getCell(currentRow, 2).style = { - ...totalRowStyle, - alignment: { horizontal: 'center' }, - fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern - } + ws4.getCell(currentRow, 1).style = { ...totalRowStyle, alignment: { horizontal: 'left' }, fill: subtotalFill } + ws4.getCell(currentRow, 2).style = { ...totalRowStyle, alignment: { horizontal: 'center' }, fill: subtotalFill } ws4.getCell(currentRow, 3).style = { ...totalRowStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat, - fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern + fill: subtotalFill } + ws4.getCell(currentRow, 4).style = { ...totalRowStyle, fill: subtotalFill } + ws4.getCell(currentRow, 5).style = { ...totalRowStyle, fill: subtotalFill } + ws4.getCell(currentRow, 6).style = { ...totalRowStyle, fill: subtotalFill } ws4.getRow(currentRow).height = 24 - currentRow += 3 // Spacing lebih lega + currentRow += 3 }) // Grand Total + const grandFill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern + const grandFont = { bold: true, size: 13, color: { argb: 'FFFFFFFF' } } + ws4.getCell(currentRow, 1).value = 'TOTAL KESELURUHAN' ws4.getCell(currentRow, 2).value = productSummary.totalQuantitySold ws4.getCell(currentRow, 3).value = productSummary.totalRevenue + ws4.getCell(currentRow, 4).value = '' + ws4.getCell(currentRow, 5).value = '' + ws4.getCell(currentRow, 6).value = '' ws4.getCell(currentRow, 1).style = { ...totalRowStyle, alignment: { horizontal: 'left' }, - font: { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }, - fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern + font: grandFont, + fill: grandFill } ws4.getCell(currentRow, 2).style = { ...totalRowStyle, alignment: { horizontal: 'center' }, - font: { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }, - fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern + font: grandFont, + fill: grandFill } ws4.getCell(currentRow, 3).style = { ...totalRowStyle, alignment: { horizontal: 'right' }, numFmt: currencyFormat, - font: { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }, - fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern + font: grandFont, + fill: grandFill } + ws4.getCell(currentRow, 4).style = { ...totalRowStyle, font: grandFont, fill: grandFill } + ws4.getCell(currentRow, 5).style = { ...totalRowStyle, font: grandFont, fill: grandFill } + ws4.getCell(currentRow, 6).style = { ...totalRowStyle, font: grandFont, fill: grandFill } ws4.getRow(currentRow).height = 28 - // Column widths ws4.getColumn(1).width = 45 ws4.getColumn(2).width = 12 ws4.getColumn(3).width = 22 + ws4.getColumn(4).width = 18 // % Standard HPP + ws4.getColumn(5).width = 18 // % Realisasi HPP + ws4.getColumn(6).width = 16 // Status // ============ GENERATE & DOWNLOAD FILE ============ const fileName = diff --git a/src/utils/pdfGenerator.ts b/src/utils/pdfGenerator.ts index c435ca0..bb50e68 100644 --- a/src/utils/pdfGenerator.ts +++ b/src/utils/pdfGenerator.ts @@ -47,6 +47,11 @@ const formatDateForInput = (date: Date) => { return date.toISOString().split('T')[0] } +// Helper: tentukan warna status +const getStatusColor = (status: string): [number, number, number] => { + return status === 'Sehat' ? [22, 163, 74] : [220, 38, 38] // green-600 / red-600 +} + export const generatePDF = async (params: PDFGeneratorParams) => { const { reportRef, @@ -227,23 +232,47 @@ export const generatePDF = async (params: PDFGeneratorParams) => { currentY = (pdf as any).lastAutoTable.finalY + 20 // ========== CATEGORY SECTION ========== + // Kolom baru: % Standard HPP, % Realisasi HPP, Status — diisi dummy visual untuk saat ini pdf.setFontSize(PDF_FONT_SIZES.heading) pdf.text('Ringkasan Kategori', 14, currentY) currentY += 15 + const DUMMY_STD_HPP = 30 // % (nilai dummy — ganti dengan data real nantinya) + const categoryBody = - category?.data?.map((c: any) => [c.category_name, String(c.total_quantity), formatCurrency(c.total_revenue)]) || [] + category?.data?.map((c: any) => { + const stdHpp = DUMMY_STD_HPP + const realisasiHpp = 28 // dummy — ganti dengan kalkulasi real + const status = realisasiHpp <= stdHpp ? 'Sehat' : 'Tidak Sehat' + return [ + c.category_name, + String(c.total_quantity), + formatCurrency(c.total_revenue), + `${stdHpp}%`, + `${realisasiHpp}%`, + status + ] + }) || [] autoTable(pdf, { startY: currentY, - head: [['Nama', 'Qty', 'Pendapatan']], + head: [['Nama', 'Qty', 'Pendapatan', '% Std HPP', '% Real HPP', 'Status']], body: categoryBody, - foot: [['TOTAL', String(categorySummary?.totalQuantity ?? 0), formatCurrency(categorySummary?.totalRevenue ?? 0)]], + foot: [ + [ + 'TOTAL', + String(categorySummary?.totalQuantity ?? 0), + formatCurrency(categorySummary?.totalRevenue ?? 0), + '', + '', + '' + ] + ], theme: 'grid', showFoot: 'lastPage', tableWidth: 'auto', styles: { - fontSize: PDF_FONT_SIZES.tableContent, + fontSize: 9, cellPadding: PDF_SPACING.cellPadding, lineColor: [0, 0, 0], lineWidth: 0.1 @@ -252,7 +281,7 @@ export const generatePDF = async (params: PDFGeneratorParams) => { fillColor: [54, 23, 94], textColor: 255, fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableHeader, + fontSize: 9, lineColor: [0, 0, 0], lineWidth: 0.1 }, @@ -260,24 +289,37 @@ export const generatePDF = async (params: PDFGeneratorParams) => { fillColor: [220, 220, 220], textColor: [60, 60, 60], fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableFooter, + fontSize: 9, lineColor: [0, 0, 0], - lineWidth: 0.1, - halign: 'center' + lineWidth: 0.1 }, columnStyles: { 1: { halign: 'center' }, - 2: { halign: 'right' } + 2: { halign: 'right' }, + 3: { halign: 'center' }, + 4: { halign: 'center' }, + 5: { halign: 'center' } }, didParseCell: (data: any) => { + // Footer alignment if (data.section === 'foot') { - if (data.column.index === 0) { - data.cell.styles.halign = 'left' - } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' - } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' - } + if (data.column.index === 0) data.cell.styles.halign = 'left' + else if (data.column.index === 1) data.cell.styles.halign = 'center' + else if (data.column.index === 2) data.cell.styles.halign = 'right' + } + }, + didDrawCell: (data: any) => { + // Warna teks Status (kolom index 5) + if (data.section === 'body' && data.column.index === 5) { + const status = data.cell.text[0] + const color = getStatusColor(status) + pdf.setTextColor(...color) + pdf.setFontSize(9) + pdf.setFont('helvetica', 'bold') + pdf.text(status, data.cell.x + data.cell.width / 2, data.cell.y + data.cell.height / 2 + 1, { align: 'center' }) + // Reset warna + pdf.setTextColor(0, 0, 0) + pdf.setFont('helvetica', 'normal') } }, margin: { left: 14, right: 14 } @@ -324,11 +366,20 @@ export const generatePDF = async (params: PDFGeneratorParams) => { const categoryTotalQty = categoryProducts.reduce((sum: number, item: any) => sum + (item.quantity_sold || 0), 0) const categoryTotalRevenue = categoryProducts.reduce((sum: number, item: any) => sum + (item.revenue || 0), 0) - const productBody = categoryProducts.map((item: any) => [ - item.product_name, - String(item.quantity_sold), - formatCurrency(item.revenue) - ]) + // Kolom baru: % Standard HPP, % Realisasi HPP, Status — dummy visual + const productBody = categoryProducts.map((item: any) => { + const stdHpp = 30 // dummy — ganti dengan data real + const realisasiHpp = 28 // dummy — ganti dengan kalkulasi real + const status = realisasiHpp <= stdHpp ? 'Sehat' : 'Tidak Sehat' + return [ + item.product_name, + String(item.quantity_sold), + formatCurrency(item.revenue), + `${stdHpp}%`, + `${realisasiHpp}%`, + status + ] + }) const estimatedHeight = (productBody.length + 3) * 12 if (currentY + estimatedHeight > 270) { @@ -344,13 +395,15 @@ export const generatePDF = async (params: PDFGeneratorParams) => { autoTable(pdf, { startY: currentY, - head: [['Produk', 'Qty', 'Pendapatan']], + head: [['Produk', 'Qty', 'Pendapatan', '% Std HPP', '% Real HPP', 'Status']], body: productBody, - foot: [[`Subtotal ${categoryName}`, String(categoryTotalQty), formatCurrency(categoryTotalRevenue)]], + foot: [ + [`Subtotal ${categoryName}`, String(categoryTotalQty), formatCurrency(categoryTotalRevenue), '', '', ''] + ], showFoot: 'lastPage', theme: 'grid', styles: { - fontSize: PDF_FONT_SIZES.tableContent, + fontSize: 9, cellPadding: PDF_SPACING.cellPadding, lineColor: [0, 0, 0], lineWidth: 0.1 @@ -359,7 +412,7 @@ export const generatePDF = async (params: PDFGeneratorParams) => { fillColor: [54, 23, 94], textColor: 255, fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableHeader, + fontSize: 9, lineColor: [0, 0, 0], lineWidth: 0.1 }, @@ -367,25 +420,38 @@ export const generatePDF = async (params: PDFGeneratorParams) => { fillColor: [200, 200, 200], textColor: [60, 60, 60], fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableFooter, + fontSize: 9, lineColor: [0, 0, 0], - lineWidth: 0.1, - halign: 'center' + lineWidth: 0.1 }, columnStyles: { - 0: { cellWidth: 90 }, - 1: { halign: 'center', cellWidth: 40 }, - 2: { halign: 'right', cellWidth: 52 } + 0: { cellWidth: 55 }, + 1: { halign: 'center', cellWidth: 20 }, + 2: { halign: 'right', cellWidth: 35 }, + 3: { halign: 'center', cellWidth: 22 }, + 4: { halign: 'center', cellWidth: 22 }, + 5: { halign: 'center', cellWidth: 22 } }, didParseCell: (data: any) => { if (data.section === 'foot') { - if (data.column.index === 0) { - data.cell.styles.halign = 'left' - } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' - } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' - } + if (data.column.index === 0) data.cell.styles.halign = 'left' + else if (data.column.index === 1) data.cell.styles.halign = 'center' + else if (data.column.index === 2) data.cell.styles.halign = 'right' + } + }, + didDrawCell: (data: any) => { + // Warna teks Status (kolom index 5) + if (data.section === 'body' && data.column.index === 5) { + const status = data.cell.text[0] + const color = getStatusColor(status) + pdf.setTextColor(...color) + pdf.setFontSize(9) + pdf.setFont('helvetica', 'bold') + pdf.text(status, data.cell.x + data.cell.width / 2, data.cell.y + data.cell.height / 2 + 1, { + align: 'center' + }) + pdf.setTextColor(0, 0, 0) + pdf.setFont('helvetica', 'normal') } }, margin: { left: 14, right: 14 } @@ -404,7 +470,14 @@ export const generatePDF = async (params: PDFGeneratorParams) => { startY: currentY, head: [], body: [ - ['TOTAL KESELURUHAN', String(productSummary.totalQuantitySold), formatCurrency(productSummary.totalRevenue)] + [ + 'TOTAL KESELURUHAN', + String(productSummary.totalQuantitySold), + formatCurrency(productSummary.totalRevenue), + '', + '', + '' + ] ], theme: 'grid', styles: { @@ -416,9 +489,12 @@ export const generatePDF = async (params: PDFGeneratorParams) => { lineWidth: 0.2 }, columnStyles: { - 0: { cellWidth: 90 }, - 1: { halign: 'center', cellWidth: 40 }, - 2: { halign: 'right', cellWidth: 52 } + 0: { cellWidth: 55 }, + 1: { halign: 'center', cellWidth: 20 }, + 2: { halign: 'right', cellWidth: 35 }, + 3: { cellWidth: 22 }, + 4: { cellWidth: 22 }, + 5: { cellWidth: 22 } }, margin: { left: 14, right: 14 } })