update report add standard hpp, realisasi hpp, dan status

This commit is contained in:
Efril 2026-02-26 14:02:04 +07:00
parent 3c13aa897c
commit a3dd214a99
4 changed files with 377 additions and 197 deletions

42
package-lock.json generated
View File

@ -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"

View File

@ -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' }) => (
<span
className={`inline-block px-3 py-1 rounded-full text-base font-bold ${
status === 'Sehat' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{status}
</span>
)
const DailyPOSReport = () => {
const reportRef = useRef<HTMLDivElement | null>(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 = () => {
</div>
</div>
{/* Category Summary */}
{/* Category Summary — +3 kolom baru */}
<div className='px-8 pb-8'>
<h3 className='text-3xl font-bold mb-8' style={{ color: '#36175e' }}>
Ringkasan Kategori
@ -355,18 +368,31 @@ const DailyPOSReport = () => {
<th className='text-left text-xl p-4 font-semibold'>Nama</th>
<th className='text-center text-xl p-4 font-semibold'>Qty</th>
<th className='text-right text-xl p-4 font-semibold'>Pendapatan</th>
<th className='text-center text-xl p-4 font-semibold'>% Std HPP</th>
<th className='text-center text-xl p-4 font-semibold'>% Real HPP</th>
<th className='text-center text-xl p-4 font-semibold'>Status</th>
</tr>
</thead>
<tbody>
{category?.data?.map((c, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className='p-4 text-xl font-medium text-gray-800'>{c.category_name}</td>
<td className='p-4 text-xl text-center text-gray-700'>{c.total_quantity}</td>
<td className='p-4 text-xl text-right font-semibold' style={{ color: '#36175e' }}>
{formatCurrency(c.total_revenue)}
</td>
</tr>
)) || []}
{category?.data?.map((c, index) => {
const stdHpp = DUMMY_STD_HPP
const realHpp = DUMMY_REAL_HPP
const status = getHppStatus(stdHpp, realHpp)
return (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className='p-4 text-xl font-medium text-gray-800'>{c.category_name}</td>
<td className='p-4 text-xl text-center text-gray-700'>{c.total_quantity}</td>
<td className='p-4 text-xl text-right font-semibold' style={{ color: '#36175e' }}>
{formatCurrency(c.total_revenue)}
</td>
<td className='p-4 text-xl text-center text-gray-700'>{stdHpp}%</td>
<td className='p-4 text-xl text-center text-gray-700'>{realHpp}%</td>
<td className='p-4 text-center'>
<StatusBadge status={status} />
</td>
</tr>
)
}) || []}
</tbody>
<tfoot>
<tr className='text-gray-800 border-t-2 border-gray-300'>
@ -375,13 +401,16 @@ const DailyPOSReport = () => {
<td className='p-4 text-xl text-right font-bold'>
{formatCurrency(categorySummary?.totalRevenue ?? 0)}
</td>
<td className='p-4'></td>
<td className='p-4'></td>
<td className='p-4'></td>
</tr>
</tfoot>
</table>
</div>
</div>
{/* Product Summary - Dipisah per kategori dengan tabel terpisah */}
{/* Product Summary — +3 kolom baru */}
<div className='px-8 pb-8'>
<h3 className='text-3xl font-bold mb-8' style={{ color: '#36175e' }}>
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%' }}
>
<colgroup>
<col style={{ width: '50%' }} />
<col style={{ width: '20%' }} />
<col style={{ width: '30%' }} />
<col style={{ width: '28%' }} />
<col style={{ width: '10%' }} />
<col style={{ width: '22%' }} />
<col style={{ width: '13%' }} />
<col style={{ width: '13%' }} />
<col style={{ width: '14%' }} />
</colgroup>
<thead>
<tr className='text-gray-800 border-b-2 border-gray-300 bg-gray-100'>
<th className='text-left text-xl p-5 font-semibold border-r border-gray-300'>Produk</th>
<th className='text-center text-xl p-5 font-semibold border-r border-gray-300'>Qty</th>
<th className='text-right text-xl p-5 font-semibold'>Pendapatan</th>
<th className='text-right text-xl p-5 font-semibold border-r border-gray-300'>
Pendapatan
</th>
<th className='text-center text-xl p-5 font-semibold border-r border-gray-300'>
% Std HPP
</th>
<th className='text-center text-xl p-5 font-semibold border-r border-gray-300'>
% Real HPP
</th>
<th className='text-center text-xl p-5 font-semibold'>Status</th>
</tr>
</thead>
<tbody>
{categoryProducts.map((item, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className='p-5 text-xl font-medium text-gray-800 border-r border-gray-200'>
{item.product_name}
</td>
<td className='p-5 text-xl text-center text-gray-700 border-r border-gray-200'>
{item.quantity_sold}
</td>
<td className='p-5 text-xl text-right font-semibold text-gray-800'>
{formatCurrency(item.revenue)}
</td>
</tr>
))}
{categoryProducts.map((item, index) => {
const stdHpp = DUMMY_STD_HPP
const realHpp = DUMMY_REAL_HPP
const status = getHppStatus(stdHpp, realHpp)
return (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className='p-5 text-xl font-medium text-gray-800 border-r border-gray-200'>
{item.product_name}
</td>
<td className='p-5 text-xl text-center text-gray-700 border-r border-gray-200'>
{item.quantity_sold}
</td>
<td className='p-5 text-xl text-right font-semibold text-gray-800 border-r border-gray-200'>
{formatCurrency(item.revenue)}
</td>
<td className='p-5 text-xl text-center text-gray-700 border-r border-gray-200'>
{stdHpp}%
</td>
<td className='p-5 text-xl text-center text-gray-700 border-r border-gray-200'>
{realHpp}%
</td>
<td className='p-5 text-center'>
<StatusBadge status={status} />
</td>
</tr>
)
})}
</tbody>
<tfoot>
<tr className='bg-gray-200 border-t-2 border-gray-400'>
@ -455,9 +509,12 @@ const DailyPOSReport = () => {
<td className='p-5 text-xl text-center font-bold text-gray-800 border-r border-gray-400'>
{categoryTotalQty}
</td>
<td className='p-5 text-xl text-right font-bold text-gray-800'>
<td className='p-5 text-xl text-right font-bold text-gray-800 border-r border-gray-400'>
{formatCurrency(categoryTotalRevenue)}
</td>
<td className='p-5 border-r border-gray-400'></td>
<td className='p-5 border-r border-gray-400'></td>
<td className='p-5'></td>
</tr>
</tfoot>
</table>
@ -468,24 +525,35 @@ const DailyPOSReport = () => {
{/* Grand Total */}
<div className='bg-purple-50 rounded-lg border-2 border-purple-300 mt-6'>
<table className='w-full' style={{ borderCollapse: 'collapse' }}>
<table className='w-full' style={{ borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<colgroup>
<col style={{ width: '28%' }} />
<col style={{ width: '10%' }} />
<col style={{ width: '22%' }} />
<col style={{ width: '13%' }} />
<col style={{ width: '13%' }} />
<col style={{ width: '14%' }} />
</colgroup>
<tfoot>
<tr className='text-gray-800'>
<td
className='p-5 text-2xl font-bold border-r-2 border-purple-300'
style={{ width: '50%', color: '#36175e' }}
>
<td className='p-5 text-2xl font-bold border-r-2 border-purple-300' style={{ color: '#36175e' }}>
TOTAL KESELURUHAN
</td>
<td
className='p-5 text-2xl text-center font-bold border-r-2 border-purple-300'
style={{ width: '20%', color: '#36175e' }}
style={{ color: '#36175e' }}
>
{productSummary.totalQuantitySold ?? 0}
</td>
<td className='p-5 text-2xl text-right font-bold' style={{ width: '30%', color: '#36175e' }}>
<td
className='p-5 text-2xl text-right font-bold border-r-2 border-purple-300'
style={{ color: '#36175e' }}
>
{formatCurrency(productSummary.totalRevenue ?? 0)}
</td>
<td className='p-5 border-r-2 border-purple-300'></td>
<td className='p-5 border-r-2 border-purple-300'></td>
<td className='p-5'></td>
</tr>
</tfoot>
</table>

View File

@ -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<ExcelJS.Style> = {
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<ExcelJS.Style> = {
}
}
// Zebra striping untuk data rows
const dataStyleAlt: Partial<ExcelJS.Style> = {
...dataStyle,
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFAFAFA' } } as ExcelJS.FillPattern
@ -92,6 +91,35 @@ const dataStyleAlt: Partial<ExcelJS.Style> = {
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<ExcelJS.Style> => {
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 =

View File

@ -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 }
})