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