From 8fbe6aa148d0268fb7f53337e2e98a1694e533dd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:26:33 +0700 Subject: [PATCH 01/35] chore(FE-Storyless): Add .claude to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d86875dd..e47b8ec3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts # idea .idea + +# claude +.claude From 50559caf52a0af86bbb9fd3545b4282b5f7e71a5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:28:18 +0700 Subject: [PATCH 02/35] feat(FE-326): Support custom header rows and cell render hook --- src/components/Table.tsx | 155 +++++++++++++++++++++++++++------------ 1 file changed, 110 insertions(+), 45 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b02dd3b5..eafd3e7a 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,8 @@ import { SortingState, OnChangeFn, Row, + HeaderGroup, + Column, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -32,6 +34,20 @@ interface TableClassNames { bodyRowClassName?: string; bodyColumnClassName?: string; paginationClassName?: string; + customHeaderRowClassName?: string; + customHeaderCellClassName?: string; +} + +export interface CustomHeaderRow { + id: string; + cells: Array<{ + id: string; + content: ReactNode; + colSpan?: number; + rowSpan?: number; + className?: string; + }>; + className?: string; } export interface TableProps { @@ -52,6 +68,13 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + customHeaderRows?: CustomHeaderRow[]; + renderCustomHeaders?: boolean; + onCustomHeaderCellRender?: ( + cell: ReactNode, + column: Column, + headerGroup: HeaderGroup + ) => ReactNode; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -85,6 +108,8 @@ const Table = ({ bodyRowClassName: '', bodyColumnClassName: '', paginationClassName: '', + customHeaderRowClassName: '', + customHeaderCellClassName: '', }, emptyContent = emptyContentDefaultValue, sorting, @@ -93,6 +118,9 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + customHeaderRows = [], + renderCustomHeaders = false, + onCustomHeaderCellRender, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -195,55 +223,92 @@ const Table = ({
+ {renderCustomHeaders && + customHeaderRows.length > 0 && + customHeaderRows.map((headerRow) => ( + + {headerRow.cells.map((cell) => ( + + ))} + + ))} + {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => ( - - ))} + > +
+ {cellContent} + + {header.column.getCanSort() && ( +
+ + +
+ )} +
+ + ); + })} ))} From 3a87b039bfecfa51b3f8a892900fe2df236752bf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:31:10 +0700 Subject: [PATCH 03/35] feat(FE-326): Add SalesReportTable component --- .../pages/closing/sale/SalesReportTable.tsx | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 src/components/pages/closing/sale/SalesReportTable.tsx diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx new file mode 100644 index 00000000..525d1dcf --- /dev/null +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -0,0 +1,554 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import React, { useState, useMemo } from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import Table, { CustomHeaderRow } from '@/components/Table'; +import Card from '@/components/Card'; +import Badge from '@/components/Badge'; +import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; + +type BaseClosingSales = { + id: number; + realization_date: string; + week_age: number; + age_label: string; + delivery_order_number: string; + product: string; + product_type: string; + customer: string; + quantity: number; + weight: number; + average: number; + price: number; + total: number; + kandang: string; + kandang_id: number; + payment_status: string; +}; + +const generateCustomHeaders = (template: { + groups: Array<{ + label: string; + field?: string; + rowSpan?: number; + colSpan?: number; + subLabels?: string[]; + }>; +}): CustomHeaderRow[] => { + const mainRow: Array<{ + id: string; + content: React.ReactNode; + colSpan?: number; + rowSpan?: number; + className: string; + }> = []; + const subRow: Array<{ + id: string; + content: React.ReactNode; + colSpan?: number; + rowSpan?: number; + className: string; + }> = []; + let subColumnIndex = 0; + + template.groups.forEach((group) => { + if (group.subLabels) { + mainRow.push({ + id: `${group.field || 'group'}-${subColumnIndex}`, + content: group.label, + colSpan: group.colSpan, + className: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-300', + }); + + group.subLabels.forEach((subLabel) => { + subRow.push({ + id: `sub-${subColumnIndex}`, + content: subLabel, + className: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300 border-t-0', + }); + subColumnIndex++; + }); + } else { + mainRow.push({ + id: `${group.field}-header`, + content: group.label, + rowSpan: group.rowSpan, + className: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300', + }); + } + }); + + const rows: CustomHeaderRow[] = [ + { + id: 'main-header', + cells: mainRow, + className: 'bg-gray-50', + }, + ]; + + if (subRow.length > 0) { + rows.push({ + id: 'sub-header', + cells: subRow, + className: 'bg-gray-50', + }); + } + + return rows; +}; + +const SalesReportTable = () => { + const [activeTabId, setActiveTabId] = useState('penjualan'); + + const salesBroilerData: BaseClosingSales[] = useMemo( + () => [ + { + id: 1, + realization_date: '2025-10-02', + week_age: 3, + age_label: '3 Weeks', + delivery_order_number: 'DO.MBU.699', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'TIAN YUSTIAN', + quantity: 1045, + weight: 1000, + average: 0.96, + price: 25300, + total: 25300000, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 2, + realization_date: '2025-10-07', + week_age: 4, + age_label: '4 Weeks', + delivery_order_number: 'DO.MBU.1037', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'ZAENAL MUTAQIN', + quantity: 850, + weight: 1211.4, + average: 1.43, + price: 23700, + total: 28710180, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 3, + realization_date: '2025-10-09', + week_age: 4, + age_label: '4 Weeks', + delivery_order_number: 'DO.MBU.1107', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'CORNELIUS TONY KUSTANTO', + quantity: 560, + weight: 990, + average: 1.77, + price: 23100, + total: 22869000, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 4, + realization_date: '2025-10-09', + week_age: 0, + age_label: '', + delivery_order_number: 'DO.MBU.1108', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'CV. KOPO AB', + quantity: 1088, + weight: 1934.3, + average: 1.78, + price: 23100, + total: 44682330, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 5, + realization_date: '2025-10-09', + week_age: 0, + age_label: '', + delivery_order_number: 'DO.MBU.1110', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'H. MAMAN ROMANSAH', + quantity: 624, + weight: 1121.4, + average: 1.8, + price: 22960, + total: 25747344, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + { + id: 6, + realization_date: '2025-10-09', + week_age: 0, + age_label: '', + delivery_order_number: 'DO.MBU.1133', + product: 'MBU BERLIAN CHICK A', + product_type: 'CHICKEN', + customer: 'PT. SAMUDERA MULIA LESTARI', + quantity: 624, + weight: 1102.3, + average: 1.77, + price: 23100, + total: 25463130, + kandang: 'ACE AWANG', + kandang_id: 1, + payment_status: 'Lunas', + }, + ], + [] + ); + + const totals = useMemo(() => { + const totalQuantity = salesBroilerData.reduce( + (sum, item) => sum + (item.quantity || 0), + 0 + ); + const totalWeight = salesBroilerData.reduce( + (sum, item) => sum + (item.weight || 0), + 0 + ); + const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; + + const validPriceItems = salesBroilerData.filter( + (item) => item.price != null + ); + const avgPricePartner = + validPriceItems.length > 0 + ? validPriceItems.reduce((sum, item) => sum + item.price, 0) / + validPriceItems.length + : 0; + + const totalPartner = salesBroilerData.reduce( + (sum, item) => sum + (item.total || 0), + 0 + ); + + const avgPriceAct = avgPricePartner; + const totalAct = totalPartner; + + return { + totalQuantity, + totalWeight, + avgWeight, + avgPricePartner, + totalPartner, + avgPriceAct, + totalAct, + }; + }, [salesBroilerData]); + + const salesColumns: ColumnDef[] = useMemo( + () => [ + { + id: 'realization_date', + accessorKey: 'realization_date', + header: 'Tanggal Realisasi', + cell: (props) => { + const date = props.row.original.realization_date; + return date ? formatDate(date, 'DD MMM YYYY') : '-'; + }, + }, + { + id: 'age_label', + accessorKey: 'age_label', + header: 'Umur', + cell: (props) => props.getValue() || '-', + }, + { + id: 'delivery_order_number', + accessorKey: 'delivery_order_number', + header: 'No. DO', + cell: (props) => props.getValue() || '-', + }, + { + id: 'product', + accessorKey: 'product', + header: 'Produk', + cell: (props) => props.getValue() || '-', + }, + { + id: 'customer', + accessorKey: 'customer', + header: 'Customer', + cell: (props) => props.getValue() || '-', + }, + { + id: 'quantity', + accessorKey: 'quantity', + header: 'Ekor', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + id: 'weight', + accessorKey: 'weight', + header: 'Kg', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + id: 'average', + accessorKey: 'average', + header: 'AVG (Kg)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + id: 'price_partner', + accessorKey: 'price', + header: 'Harga Mitra (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'total_mitra', + accessorKey: 'total', + header: 'Total Mitra (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'price_act', + accessorKey: 'price', + header: 'Harga Act (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'total_act', + accessorKey: 'total', + header: 'Total Act (Rp)', + cell: (props) => { + const value = props.getValue() as number; + const isSummary = props.row.id === 'summary'; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'kandang', + accessorKey: 'kandang', + header: 'Kandang', + cell: (props) => props.getValue() || '-', + }, + { + id: 'payment_status', + accessorKey: 'payment_status', + header: 'Status Pembayaran', + cell: (props) => { + const status = props.getValue() as string; + const getStatusColor = (status: string) => { + if (!status) return 'neutral'; + switch (status.toLowerCase()) { + case 'lunas': + return 'success'; + case 'pending': + return 'warning'; + case 'belum lunas': + return 'error'; + default: + return 'neutral'; + } + }; + + return ( + + {status || '-'} + + ); + }, + }, + ], + [] + ); + + const headerTemplate = { + groups: [ + { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, + { label: 'Umur', field: 'age_label', rowSpan: 2 }, + { label: 'No. DO', field: 'delivery_order_number', rowSpan: 2 }, + { label: 'Produk', field: 'product', rowSpan: 2 }, + { label: 'Customer', field: 'customer', rowSpan: 2 }, + { + label: 'Jumlah', + colSpan: 2, + subLabels: ['Ekor', 'Kg'], + }, + { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, + { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, + { label: 'Total Mitra (Rp)', field: 'total_partner', rowSpan: 2 }, + { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, + { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, + { label: 'Kandang', field: 'kandang', rowSpan: 2 }, + { label: 'Status Pembayaran', field: 'payment_status', rowSpan: 2 }, + ], + }; + + const salesCustomHeaderRows = useMemo( + () => generateCustomHeaders(headerTemplate), + [] + ); + + return ( + <> +
+ +

+ Penjualan Ayam Besar +

+ +
+ {cell.content} +
-
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} + {headerGroup.headers.map((header) => { + let cellContent = flexRender( + header.column.columnDef.header, + header.getContext() + ); - {header.column.getCanSort() && ( -
- - -
+ if (onCustomHeaderCellRender) { + cellContent = onCustomHeaderCellRender( + cellContent, + header.column, + headerGroup + ); + } + + return ( +
-
+ + {/* Summary Row */} +
+ + + + + + + + + + + + + + + + + + +
+ Total Penjualan + + - + + - + + - + + - + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} + + - + + - +
+ +
+ ), + }, + ]} + variant='lifted' + /> + + + ); +}; + +export default SalesReportTable; From 411c2586f5c1e2bae689e84edeaab9a0150a6eb5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:32:11 +0700 Subject: [PATCH 04/35] chore(format): prettier format --- .gitlab-ci.yml | 2 -- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6cf254e7..4a413bc4 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -370,7 +370,7 @@ const RecordingTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); - const [approvalNotes, setApprovalNotes] = useState(''); + const [, setApprovalNotes] = useState(''); const singleDeleteModal = useModal(); const approveModal = useModal(); From 991a594ee18a8d171aaa38c78b2de4acbf1fd97b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 11:51:11 +0700 Subject: [PATCH 05/35] refactor(FE-326): Add ClosingApiService and types for closing sales data --- src/services/api/closing.ts | 163 +++++++++++++++++++++++++++++ src/types/api/closing/closing.d.ts | 26 +++++ 2 files changed, 189 insertions(+) create mode 100644 src/services/api/closing.ts create mode 100644 src/types/api/closing/closing.d.ts diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts new file mode 100644 index 00000000..6dab6f8d --- /dev/null +++ b/src/services/api/closing.ts @@ -0,0 +1,163 @@ +import { BaseApiService } from './base'; +import { BaseApiResponse, CreatedUser } from '@/types/api/api-general'; +import { ClosingSales } from '@/types/api/closing/closing'; +import { sleep } from '@/lib/helper'; + +const adminUser: CreatedUser = { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', +}; + +const DUMMY_SALES: ClosingSales[] = [ + { + id: 1, + realization_date: '2025-01-15', + week_age: 4, + age_label: 'Week 4', + delivery_order_number: 'DO-2025-001', + product: { + id: 1, + name: 'Ayam Broiler', + brand: 'Brand A', + sku: 'AYM-001', + product_price: 20000, + selling_price: 25000, + tax: 10, + expiry_period: 30, + uom: { + id: 1, + name: 'Kg', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'LC001', + name: 'Live Chicken', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + suppliers: [], + flags: [], + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'LC001', + name: 'Live Chicken', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + customer: { + id: 1, + name: 'PT. Bumi Mandiri', + pic_id: 1, + pic: adminUser, + type: 'COMPANY', + address: 'Jl. Industri No. 123', + phone: '+62-21-1234567', + email: 'info@bumimandiri.com', + account_number: '1234567890', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + quantity: 1000, + weight: 1850, + average: 1.85, + price: 25000, + total: 25000000, + kandang: { + id: 1, + name: 'Singaparna 1', + status: 'ACTIVE', + location: { + id: 1, + name: 'Singaparna', + address: 'Jl. Singaparna No. 1', + area: { + id: 1, + name: 'Tasikmalaya', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + capacity: 1000, + pic: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + kandang_id: 1, + payment_status: 'PAID', + created_user: adminUser, + created_at: '2025-01-15T10:00:00Z', + updated_at: '2025-01-15T10:00:00Z', + }, +]; + +export class ClosingApiService extends BaseApiService< + ClosingSales, + unknown, + unknown +> { + constructor(basePath: string = '') { + super(basePath); + } + + async getPenjualan( + id: number + ): Promise | undefined> { + try { + // TODO: Remove dummy data when real API is ready + await sleep(750); + + const saleData = DUMMY_SALES.find((sale) => sale.id === id); + + if (!saleData) { + return { + code: 404, + status: 'error', + message: 'Sales data not found!', + }; + } + + return { + code: 200, + status: 'success', + message: 'Successfully get sales data!', + meta: { + page: 1, + limit: 10, + total_pages: 1, + total_results: 1, + }, + data: saleData, + }; + + // const getPenjualanPath = `${this.basePath}/${id}/penjualan`; + // return await this.customRequest>(getPenjualanPath); + } catch (error) { + console.error('Error fetching penjualan:', error); + return undefined; + } + } +} + +export const ClosingApi = new ClosingApiService('/closing'); diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts new file mode 100644 index 00000000..d4b94770 --- /dev/null +++ b/src/types/api/closing/closing.d.ts @@ -0,0 +1,26 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Kandang } from '@type/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { ProductCategory } from '@type/api/master-data/product-category'; +import { Customer } from '@type/api/master-data/customer'; + +export type BaseClosingSales = { + id: number; + realization_date: string; + week_age: number; + age_label: string; + delivery_order_number: string; + product: Product; + product_category: ProductCategory; + customer: Customer; + quantity: number; + weight: number; + average: number; + price: number; + total: number; + kandang: Kandang; + kandang_id: number; + payment_status: string; +}; + +export type ClosingSales = BaseMetadata & BaseClosingSales; From 647b002065d77bbe201975331978eb5089aa681f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 11:55:57 +0700 Subject: [PATCH 06/35] refactor(FE-326): change SalesReportTable to use BaseClosingSales type and support initial values --- .../pages/closing/sale/SalesReportTable.tsx | 147 ++---------------- 1 file changed, 15 insertions(+), 132 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 525d1dcf..85fe50a5 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,25 +7,12 @@ import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; +import { BaseClosingSales } from '@/types/api/closing/closing'; -type BaseClosingSales = { - id: number; - realization_date: string; - week_age: number; - age_label: string; - delivery_order_number: string; - product: string; - product_type: string; - customer: string; - quantity: number; - weight: number; - average: number; - price: number; - total: number; - kandang: string; - kandang_id: number; - payment_status: string; -}; +interface SalesReportTableProps { + type?: 'detail'; + initialValues?: BaseClosingSales; +} const generateCustomHeaders = (template: { groups: Array<{ @@ -101,122 +88,18 @@ const generateCustomHeaders = (template: { return rows; }; -const SalesReportTable = () => { +const SalesReportTable = ({ + type = 'detail', + initialValues, +}: SalesReportTableProps) => { const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseClosingSales[] = useMemo( - () => [ - { - id: 1, - realization_date: '2025-10-02', - week_age: 3, - age_label: '3 Weeks', - delivery_order_number: 'DO.MBU.699', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'TIAN YUSTIAN', - quantity: 1045, - weight: 1000, - average: 0.96, - price: 25300, - total: 25300000, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 2, - realization_date: '2025-10-07', - week_age: 4, - age_label: '4 Weeks', - delivery_order_number: 'DO.MBU.1037', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'ZAENAL MUTAQIN', - quantity: 850, - weight: 1211.4, - average: 1.43, - price: 23700, - total: 28710180, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 3, - realization_date: '2025-10-09', - week_age: 4, - age_label: '4 Weeks', - delivery_order_number: 'DO.MBU.1107', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'CORNELIUS TONY KUSTANTO', - quantity: 560, - weight: 990, - average: 1.77, - price: 23100, - total: 22869000, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 4, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1108', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'CV. KOPO AB', - quantity: 1088, - weight: 1934.3, - average: 1.78, - price: 23100, - total: 44682330, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 5, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1110', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'H. MAMAN ROMANSAH', - quantity: 624, - weight: 1121.4, - average: 1.8, - price: 22960, - total: 25747344, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 6, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1133', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'PT. SAMUDERA MULIA LESTARI', - quantity: 624, - weight: 1102.3, - average: 1.77, - price: 23100, - total: 25463130, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - ], - [] - ); + const salesBroilerData: BaseClosingSales[] = useMemo(() => { + if (activeTabId === 'penjualan' && initialValues) { + return [initialValues]; + } + return []; + }, [initialValues, activeTabId]); const totals = useMemo(() => { const totalQuantity = salesBroilerData.reduce( From ba40bbb1d37c647f91cbfb567e86c9f28d388a26 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:07:10 +0700 Subject: [PATCH 07/35] refactor(FE-327): remove dummy sales data and update ClosingApiService to fetch real sales data from API --- src/services/api/closing.ts | 149 ++---------------------------------- 1 file changed, 7 insertions(+), 142 deletions(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 6dab6f8d..1378ab58 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -1,123 +1,13 @@ import { BaseApiService } from './base'; -import { BaseApiResponse, CreatedUser } from '@/types/api/api-general'; +import { BaseApiResponse } from '@/types/api/api-general'; import { ClosingSales } from '@/types/api/closing/closing'; -import { sleep } from '@/lib/helper'; - -const adminUser: CreatedUser = { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin User', -}; - -const DUMMY_SALES: ClosingSales[] = [ - { - id: 1, - realization_date: '2025-01-15', - week_age: 4, - age_label: 'Week 4', - delivery_order_number: 'DO-2025-001', - product: { - id: 1, - name: 'Ayam Broiler', - brand: 'Brand A', - sku: 'AYM-001', - product_price: 20000, - selling_price: 25000, - tax: 10, - expiry_period: 30, - uom: { - id: 1, - name: 'Kg', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - product_category: { - id: 1, - code: 'LC001', - name: 'Live Chicken', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - suppliers: [], - flags: [], - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - product_category: { - id: 1, - code: 'LC001', - name: 'Live Chicken', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - customer: { - id: 1, - name: 'PT. Bumi Mandiri', - pic_id: 1, - pic: adminUser, - type: 'COMPANY', - address: 'Jl. Industri No. 123', - phone: '+62-21-1234567', - email: 'info@bumimandiri.com', - account_number: '1234567890', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - quantity: 1000, - weight: 1850, - average: 1.85, - price: 25000, - total: 25000000, - kandang: { - id: 1, - name: 'Singaparna 1', - status: 'ACTIVE', - location: { - id: 1, - name: 'Singaparna', - address: 'Jl. Singaparna No. 1', - area: { - id: 1, - name: 'Tasikmalaya', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - capacity: 1000, - pic: { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin User', - }, - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - kandang_id: 1, - payment_status: 'PAID', - created_user: adminUser, - created_at: '2025-01-15T10:00:00Z', - updated_at: '2025-01-15T10:00:00Z', - }, -]; export class ClosingApiService extends BaseApiService< ClosingSales, unknown, unknown > { - constructor(basePath: string = '') { + constructor(basePath: string) { super(basePath); } @@ -125,36 +15,11 @@ export class ClosingApiService extends BaseApiService< id: number ): Promise | undefined> { try { - // TODO: Remove dummy data when real API is ready - await sleep(750); - - const saleData = DUMMY_SALES.find((sale) => sale.id === id); - - if (!saleData) { - return { - code: 404, - status: 'error', - message: 'Sales data not found!', - }; - } - - return { - code: 200, - status: 'success', - message: 'Successfully get sales data!', - meta: { - page: 1, - limit: 10, - total_pages: 1, - total_results: 1, - }, - data: saleData, - }; - - // const getPenjualanPath = `${this.basePath}/${id}/penjualan`; - // return await this.customRequest>(getPenjualanPath); - } catch (error) { - console.error('Error fetching penjualan:', error); + const getPenjualanPath = `http://localhost:4010/api/closing/${id}/penjualan`; + return await this.customRequest>( + getPenjualanPath + ); + } catch { return undefined; } } From 1a4a05308f0ee94f926af968954b702f077969f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:08:44 +0700 Subject: [PATCH 08/35] refactor(FE-326,327): enhance SalesReportTable to handle empty sales data and conditionally render summary row --- .../pages/closing/sale/SalesReportTable.tsx | 112 ++++++++++-------- 1 file changed, 63 insertions(+), 49 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 85fe50a5..18af45a5 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -102,6 +102,18 @@ const SalesReportTable = ({ }, [initialValues, activeTabId]); const totals = useMemo(() => { + if (salesBroilerData.length === 0) { + return { + totalQuantity: 0, + totalWeight: 0, + avgWeight: 0, + avgPricePartner: 0, + totalPartner: 0, + avgPriceAct: 0, + totalAct: 0, + }; + } + const totalQuantity = salesBroilerData.reduce( (sum, item) => sum + (item.quantity || 0), 0 @@ -113,7 +125,7 @@ const SalesReportTable = ({ const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; const validPriceItems = salesBroilerData.filter( - (item) => item.price != null + (item) => item.price != null && item.price > 0 ); const avgPricePartner = validPriceItems.length > 0 @@ -374,54 +386,56 @@ const SalesReportTable = ({ /> {/* Summary Row */} - - - - - - - - - - - - - - - - - - - -
- Total Penjualan - - - - - - - - - - - - - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPriceAct)} - - {formatCurrency(totals.totalAct)} - - - - - - -
+ {salesBroilerData.length > 0 && ( + + + + + + + + + + + + + + + + + + + +
+ Total Penjualan + + - + + - + + - + + - + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} + + - + + - +
+ )} ), From 8ea29579ecb5d712856f065dc615ca3e2e51b84d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:10:57 +0700 Subject: [PATCH 09/35] feat(FE-326,327): add layout and closing detail page with sales report integration --- src/app/closing/detail/layout.tsx | 11 +++++++ src/app/closing/detail/page.tsx | 55 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/app/closing/detail/layout.tsx create mode 100644 src/app/closing/detail/page.tsx diff --git a/src/app/closing/detail/layout.tsx b/src/app/closing/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/closing/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx new file mode 100644 index 00000000..43a8469a --- /dev/null +++ b/src/app/closing/detail/page.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; + +const ClosingDetailPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const closingId = searchParams.get('closingId'); + + const { data: closing, isLoading: isLoadingClosing } = useSWR( + closingId, + (id: string) => { + const numericId = parseInt(id, 10); + if (isNaN(numericId) || numericId <= 0) { + throw new Error('Invalid closing ID'); + } + return ClosingApi.getPenjualan(numericId); + } + ); + + if (!closingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingClosing && (!closing || isResponseError(closing))) { + // router.replace('/404'); + return; + } + + return ( +
+ {isLoadingClosing && ( +
+ +
+ )} + {!isLoadingClosing && isResponseSuccess(closing) && ( + + )} +
+ ); +}; + +export default ClosingDetailPage; From 492efb18e24506d56ff7c948eedc48e62ed9b544 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:15:58 +0700 Subject: [PATCH 10/35] chore: update next.js to version 15.5.7 in package.json and package-lock.json --- package-lock.json | 92 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec1316ae..535bb986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1098,9 +1098,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1114,9 +1114,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1146,9 +1146,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1162,9 +1162,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1178,9 +1178,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1194,9 +1194,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5672,14 +5678,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7396d49d..85485ee3 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From 15ced14e20b9c9bca49ae2d291f736a173b4e1b6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:25:11 +0700 Subject: [PATCH 11/35] refactor(FE-Storyless): add footer rendering support to Table component --- src/components/Table.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index eafd3e7a..96f23e8e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -75,6 +75,8 @@ export interface TableProps { column: Column, headerGroup: HeaderGroup ) => ReactNode; + renderFooter?: boolean; + footerContent?: ReactNode; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -121,6 +123,8 @@ const Table = ({ customHeaderRows = [], renderCustomHeaders = false, onCustomHeaderCellRender, + renderFooter = false, + footerContent, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -327,6 +331,8 @@ const Table = ({ ))} + + {renderFooter && footerContent} From 949761d59d4b98faa6331321fbe07cee9a6c6744 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:25:27 +0700 Subject: [PATCH 12/35] feat(FE-326,327): add footer rendering to SalesReportTable for total sales display --- .../pages/closing/sale/SalesReportTable.tsx | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 18af45a5..552985a9 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -375,20 +375,9 @@ const SalesReportTable = ({ columns={salesColumns} renderCustomHeaders={true} customHeaderRows={salesCustomHeaderRows} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', - bodyRowClassName: 'hover:bg-gray-50 transition-colors', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', - }} - /> - - {/* Summary Row */} - {salesBroilerData.length > 0 && ( - - + renderFooter={salesBroilerData.length > 0} + footerContent={ + - -
Total Penjualan @@ -433,9 +422,17 @@ const SalesReportTable = ({ -
- )} + + } + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'hidden', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', + }} + /> ), From b095208fae29059050f25be05131dd05040142ad Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 17:41:22 +0700 Subject: [PATCH 13/35] refactor(FE-327): Temporarily map Indonesian sales fields to English --- .../pages/closing/sale/SalesReportTable.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 552985a9..b702752a 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -88,6 +88,34 @@ const generateCustomHeaders = (template: { return rows; }; +// TODO: TEMPORARY - Remove this when backend API returns English field names +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { + if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { + return []; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return data.penjualan.map((item: any) => ({ + id: item.id, + realization_date: item.tanggal_realisasi, + age_label: item.umur_label, + umur_minggu: item.umur_minggu, + delivery_order_number: item.no_do, + product: item.produk, + jenis_produk: item.jenis_produk, + customer: item.customer, + quantity: item.qty, + weight: item.kg, + average: item.avg, + price: item.harga, + total: item.total, + kandang: item.kandang, + payment_status: item.status_pembayaran, + })); +}; +// END TODO + const SalesReportTable = ({ type = 'detail', initialValues, @@ -96,6 +124,13 @@ const SalesReportTable = ({ const salesBroilerData: BaseClosingSales[] = useMemo(() => { if (activeTabId === 'penjualan' && initialValues) { + // TODO: TEMPORARY - Remove this when backend API returns English field names + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { + return mapIndonesianDataToEnglish(initialValues); + } + // END TODO return [initialValues]; } return []; From 7d9a88cf3b8dc293ba71c671036b83b9aedc5086 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 20:14:29 +0700 Subject: [PATCH 14/35] feat(FE-326,327): Add sortable table headers and styling --- src/components/Table.tsx | 76 +++++++++++++--- .../pages/closing/sale/SalesReportTable.tsx | 88 +++++++++++-------- 2 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 96f23e8e..69406220 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -46,6 +46,7 @@ export interface CustomHeaderRow { colSpan?: number; rowSpan?: number; className?: string; + field?: string; }>; className?: string; } @@ -236,18 +237,69 @@ const Table = ({ headerRow.className || className.customHeaderRowClassName } > - {headerRow.cells.map((cell) => ( - - {cell.content} - - ))} + {headerRow.cells.map((cell) => { + const column = table + .getAllColumns() + .find((col) => col.id === cell.field); + + const canSort = column?.getCanSort(); + const sortingState = column?.getIsSorted(); + + return ( + +
+ {cell.content} + + {canSort && ( +
+ + +
+ )} +
+ + ); + })} ))} diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index b702752a..b9f73635 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -14,6 +14,15 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } +interface HeaderCell { + id: string; + content: React.ReactNode; + colSpan?: number; + rowSpan?: number; + className: string; + field?: string; +} + const generateCustomHeaders = (template: { groups: Array<{ label: string; @@ -41,31 +50,43 @@ const generateCustomHeaders = (template: { template.groups.forEach((group) => { if (group.subLabels) { - mainRow.push({ + const mainCell: HeaderCell = { id: `${group.field || 'group'}-${subColumnIndex}`, content: group.label, colSpan: group.colSpan, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-300', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200', + }; + + mainRow.push(mainCell); group.subLabels.forEach((subLabel) => { - subRow.push({ + const subCell: HeaderCell = { id: `sub-${subColumnIndex}`, content: subLabel, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300 border-t-0', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', + }; + + if (group.label === 'Jumlah') { + subCell.field = subLabel === 'Ekor' ? 'quantity' : 'weight'; + } + + subRow.push(subCell); subColumnIndex++; }); } else { - mainRow.push({ + const mainCell: HeaderCell = { id: `${group.field}-header`, content: group.label, rowSpan: group.rowSpan, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', + }; + + mainCell.field = group.field; + + mainRow.push(mainCell); } }); @@ -371,7 +392,7 @@ const SalesReportTable = ({ }, { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, - { label: 'Total Mitra (Rp)', field: 'total_partner', rowSpan: 2 }, + { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, { label: 'Kandang', field: 'kandang', rowSpan: 2 }, @@ -413,49 +434,37 @@ const SalesReportTable = ({ renderFooter={salesBroilerData.length > 0} footerContent={ - - + + Total Penjualan - - - - - - - - - - - - - - - - - + + + + + {formatNumber(totals.totalQuantity)} - + {formatNumber(totals.totalWeight)} - + {formatNumber(totals.avgWeight)} - + {formatCurrency(totals.avgPricePartner)} - + {formatCurrency(totals.totalPartner)} - + {formatCurrency(totals.avgPriceAct)} - + {formatCurrency(totals.totalAct)} - - - - - - - - + + } @@ -463,9 +472,10 @@ const SalesReportTable = ({ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'hidden', - bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> From 075d945a59a193d33f56b8e3d213592fcd7e0dac Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 21:29:45 +0700 Subject: [PATCH 15/35] refactor(FE-326): Use placeholder for sales type in header --- src/components/pages/closing/sale/SalesReportTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index b9f73635..ca616107 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -419,7 +419,7 @@ const SalesReportTable = ({ content: (

- Penjualan Ayam Besar + Penjualan {'(diisi dengan jenis penjualan)'}

Date: Fri, 5 Dec 2025 11:14:52 +0700 Subject: [PATCH 16/35] refactor(FE-327): Split BaseClosingSales into BaseSales and wrapper --- src/types/api/closing/closing.d.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index d4b94770..6b17d8e1 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -4,7 +4,7 @@ import { Product } from '@type/api/master-data/product'; import { ProductCategory } from '@type/api/master-data/product-category'; import { Customer } from '@type/api/master-data/customer'; -export type BaseClosingSales = { +export type BaseSales = { id: number; realization_date: string; week_age: number; @@ -23,4 +23,9 @@ export type BaseClosingSales = { payment_status: string; }; +export type BaseClosingSales = { + project_type: string; + penjualan: BaseSales[]; +}; + export type ClosingSales = BaseMetadata & BaseClosingSales; From 46e072bbcfaf091a9c0fafe30c3504dd95524c7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 11:15:41 +0700 Subject: [PATCH 17/35] refactor(FE-327): Map Indonesian sales fields and add API sample --- .../pages/closing/sale/SalesReportTable.tsx | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index ca616107..d93dbf2e 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,7 +7,7 @@ import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { BaseClosingSales } from '@/types/api/closing/closing'; +import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; interface SalesReportTableProps { type?: 'detail'; @@ -110,8 +110,7 @@ const generateCustomHeaders = (template: { }; // TODO: TEMPORARY - Remove this when backend API returns English field names -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { +const mapIndonesianDataToEnglish = (data: BaseClosingSales): BaseSales[] => { if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { return []; } @@ -120,11 +119,11 @@ const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { return data.penjualan.map((item: any) => ({ id: item.id, realization_date: item.tanggal_realisasi, + week_age: item.umur_minggu, age_label: item.umur_label, - umur_minggu: item.umur_minggu, delivery_order_number: item.no_do, product: item.produk, - jenis_produk: item.jenis_produk, + product_category: item.jenis_produk, customer: item.customer, quantity: item.qty, weight: item.kg, @@ -132,6 +131,7 @@ const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { price: item.harga, total: item.total, kandang: item.kandang, + kandang_id: item.kandang_id, payment_status: item.status_pembayaran, })); }; @@ -143,16 +143,14 @@ const SalesReportTable = ({ }: SalesReportTableProps) => { const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseClosingSales[] = useMemo(() => { + const salesBroilerData: BaseSales[] = useMemo(() => { if (activeTabId === 'penjualan' && initialValues) { // TODO: TEMPORARY - Remove this when backend API returns English field names - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { return mapIndonesianDataToEnglish(initialValues); } // END TODO - return [initialValues]; + return []; } return []; }, [initialValues, activeTabId]); @@ -208,7 +206,7 @@ const SalesReportTable = ({ }; }, [salesBroilerData]); - const salesColumns: ColumnDef[] = useMemo( + const salesColumns: ColumnDef[] = useMemo( () => [ { id: 'realization_date', @@ -418,9 +416,7 @@ const SalesReportTable = ({ label: 'Penjualan', content: (
-

- Penjualan {'(diisi dengan jenis penjualan)'} -

+

Penjualan

+
+ +
+            {JSON.stringify(
+              {
+                code: 200,
+                status: 'success',
+                message: 'Retrieved sales report successfully',
+                data: {
+                  project_type: 'GROWING',
+                  flock_id: '1',
+                  period: 10,
+                  sales: [
+                    {
+                      id: 1,
+                      realization_date: '2025-12-05T02:22:17.443165Z',
+                      age: 20,
+                      do_number: 'SO-DO-10001',
+                      product: {
+                        id: 1,
+                        name: 'Laptop Gaming X500',
+                        product_price: 15000000,
+                        selling_price: 16500000.5,
+                        uom: {
+                          id: 1,
+                          name: 'KG',
+                        },
+                        flags: ['Best Seller', 'New Arrival'],
+                        product_category: {
+                          id: 5,
+                          name: 'Elektronik',
+                          code: 'DOC',
+                        },
+                      },
+                      customer: {
+                        id: 12345,
+                        name: 'PT. Solusi Teknologi Nusantara',
+                        type: 'Perusahaan',
+                        account_number: 'ACC1234567890',
+                        balance: 5000000.75,
+                        pic: {
+                          id: 101,
+                          name: 'Budi Santoso',
+                          email: 'budi.santoso@example.com',
+                          role: 'Manajer Akun',
+                        },
+                      },
+                      qty: 6348,
+                      weight: 19142,
+                      avg_weight: 3.02,
+                      price: 26419,
+                      total_price: 505712498,
+                      kandang: {
+                        id: 1,
+                        name: 'cibeber 1',
+                      },
+                      payment_status: 'Paid',
+                    },
+                  ],
+                },
+              },
+              null,
+              2
+            )}
+          
+
+
); }; From f205c6650985f7d3af07f5a127e18d2564815c9c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 17:49:59 +0700 Subject: [PATCH 18/35] refactor(FE-327): Rename Ekor label to Kuantitas --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index d93dbf2e..6796465e 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -69,7 +69,7 @@ const generateCustomHeaders = (template: { }; if (group.label === 'Jumlah') { - subCell.field = subLabel === 'Ekor' ? 'quantity' : 'weight'; + subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight'; } subRow.push(subCell); @@ -244,7 +244,7 @@ const SalesReportTable = ({ { id: 'quantity', accessorKey: 'quantity', - header: 'Ekor', + header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; const isSummary = props.row.id === 'summary'; @@ -386,7 +386,7 @@ const SalesReportTable = ({ { label: 'Jumlah', colSpan: 2, - subLabels: ['Ekor', 'Kg'], + subLabels: ['Kuantitas', 'Kg'], }, { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, From 5869e0434b175f859a9b3e4fd753c4ac55f98bda Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 18:26:58 +0700 Subject: [PATCH 19/35] refactor(FE-327): change closing API paths and sales types --- src/services/api/closing.ts | 4 ++-- src/types/api/closing/closing.d.ts | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 1378ab58..66f88c76 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -15,7 +15,7 @@ export class ClosingApiService extends BaseApiService< id: number ): Promise | undefined> { try { - const getPenjualanPath = `http://localhost:4010/api/closing/${id}/penjualan`; + const getPenjualanPath = `${id}/penjualan`; return await this.customRequest>( getPenjualanPath ); @@ -25,4 +25,4 @@ export class ClosingApiService extends BaseApiService< } } -export const ClosingApi = new ClosingApiService('/closing'); +export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index 6b17d8e1..03217438 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -1,31 +1,27 @@ import { BaseMetadata } from '@/types/api/api-general'; -import { Kandang } from '@type/api/master-data/kandang'; import { Product } from '@type/api/master-data/product'; -import { ProductCategory } from '@type/api/master-data/product-category'; import { Customer } from '@type/api/master-data/customer'; export type BaseSales = { id: number; realization_date: string; - week_age: number; - age_label: string; - delivery_order_number: string; + age: number; + do_number: string; product: Product; - product_category: ProductCategory; customer: Customer; - quantity: number; + qty: number; weight: number; - average: number; + avg_weight: number; price: number; - total: number; - kandang: Kandang; - kandang_id: number; + total_price: number; payment_status: string; }; export type BaseClosingSales = { project_type: string; - penjualan: BaseSales[]; + flock_id: number; + period: number; + sales: BaseSales[]; }; export type ClosingSales = BaseMetadata & BaseClosingSales; From 30db7ee95d806ed47785c1987b0ca43841b5ea51 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 18:27:45 +0700 Subject: [PATCH 20/35] refactor(FE-327): change SalesReportTable to use new API fields --- .../pages/closing/sale/SalesReportTable.tsx | 181 ++++-------------- 1 file changed, 32 insertions(+), 149 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 6796465e..64247890 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -8,6 +8,8 @@ import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; interface SalesReportTableProps { type?: 'detail'; @@ -109,34 +111,6 @@ const generateCustomHeaders = (template: { return rows; }; -// TODO: TEMPORARY - Remove this when backend API returns English field names -const mapIndonesianDataToEnglish = (data: BaseClosingSales): BaseSales[] => { - if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { - return []; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return data.penjualan.map((item: any) => ({ - id: item.id, - realization_date: item.tanggal_realisasi, - week_age: item.umur_minggu, - age_label: item.umur_label, - delivery_order_number: item.no_do, - product: item.produk, - product_category: item.jenis_produk, - customer: item.customer, - quantity: item.qty, - weight: item.kg, - average: item.avg, - price: item.harga, - total: item.total, - kandang: item.kandang, - kandang_id: item.kandang_id, - payment_status: item.status_pembayaran, - })); -}; -// END TODO - const SalesReportTable = ({ type = 'detail', initialValues, @@ -144,13 +118,8 @@ const SalesReportTable = ({ const [activeTabId, setActiveTabId] = useState('penjualan'); const salesBroilerData: BaseSales[] = useMemo(() => { - if (activeTabId === 'penjualan' && initialValues) { - // TODO: TEMPORARY - Remove this when backend API returns English field names - if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { - return mapIndonesianDataToEnglish(initialValues); - } - // END TODO - return []; + if (activeTabId === 'penjualan' && initialValues && initialValues.sales) { + return initialValues.sales; } return []; }, [initialValues, activeTabId]); @@ -169,7 +138,7 @@ const SalesReportTable = ({ } const totalQuantity = salesBroilerData.reduce( - (sum, item) => sum + (item.quantity || 0), + (sum, item) => sum + (item.qty || 0), 0 ); const totalWeight = salesBroilerData.reduce( @@ -188,7 +157,7 @@ const SalesReportTable = ({ : 0; const totalPartner = salesBroilerData.reduce( - (sum, item) => sum + (item.total || 0), + (sum, item) => sum + (item.total_price || 0), 0 ); @@ -218,14 +187,14 @@ const SalesReportTable = ({ }, }, { - id: 'age_label', - accessorKey: 'age_label', + id: 'age', + accessorKey: 'age', header: 'Umur', cell: (props) => props.getValue() || '-', }, { - id: 'delivery_order_number', - accessorKey: 'delivery_order_number', + id: 'do_number', + accessorKey: 'do_number', header: 'No. DO', cell: (props) => props.getValue() || '-', }, @@ -233,17 +202,23 @@ const SalesReportTable = ({ id: 'product', accessorKey: 'product', header: 'Produk', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const product = props.getValue() as Product; + return product?.name || '-'; + }, }, { id: 'customer', accessorKey: 'customer', header: 'Customer', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const customer = props.getValue() as Customer; + return customer?.name || '-'; + }, }, { - id: 'quantity', - accessorKey: 'quantity', + id: 'qty', + accessorKey: 'qty', header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; @@ -270,8 +245,8 @@ const SalesReportTable = ({ }, }, { - id: 'average', - accessorKey: 'average', + id: 'avg_weight', + accessorKey: 'avg_weight', header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; @@ -289,26 +264,16 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { id: 'total_mitra', - accessorKey: 'total', + accessorKey: 'total_price', header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -317,26 +282,16 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { id: 'total_act', - accessorKey: 'total', + accessorKey: 'total_price', header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -379,16 +334,16 @@ const SalesReportTable = ({ const headerTemplate = { groups: [ { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, - { label: 'Umur', field: 'age_label', rowSpan: 2 }, - { label: 'No. DO', field: 'delivery_order_number', rowSpan: 2 }, + { label: 'Umur', field: 'age', rowSpan: 2 }, + { label: 'No. DO', field: 'do_number', rowSpan: 2 }, { label: 'Produk', field: 'product', rowSpan: 2 }, { label: 'Customer', field: 'customer', rowSpan: 2 }, { label: 'Jumlah', colSpan: 2, - subLabels: ['Kuantitas', 'Kg'], + subLabels: ['Qty', 'Kg'], }, - { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, + { label: 'AVG (Kg)', field: 'avg_weight', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, @@ -482,78 +437,6 @@ const SalesReportTable = ({ variant='lifted' /> -
- -
-            {JSON.stringify(
-              {
-                code: 200,
-                status: 'success',
-                message: 'Retrieved sales report successfully',
-                data: {
-                  project_type: 'GROWING',
-                  flock_id: '1',
-                  period: 10,
-                  sales: [
-                    {
-                      id: 1,
-                      realization_date: '2025-12-05T02:22:17.443165Z',
-                      age: 20,
-                      do_number: 'SO-DO-10001',
-                      product: {
-                        id: 1,
-                        name: 'Laptop Gaming X500',
-                        product_price: 15000000,
-                        selling_price: 16500000.5,
-                        uom: {
-                          id: 1,
-                          name: 'KG',
-                        },
-                        flags: ['Best Seller', 'New Arrival'],
-                        product_category: {
-                          id: 5,
-                          name: 'Elektronik',
-                          code: 'DOC',
-                        },
-                      },
-                      customer: {
-                        id: 12345,
-                        name: 'PT. Solusi Teknologi Nusantara',
-                        type: 'Perusahaan',
-                        account_number: 'ACC1234567890',
-                        balance: 5000000.75,
-                        pic: {
-                          id: 101,
-                          name: 'Budi Santoso',
-                          email: 'budi.santoso@example.com',
-                          role: 'Manajer Akun',
-                        },
-                      },
-                      qty: 6348,
-                      weight: 19142,
-                      avg_weight: 3.02,
-                      price: 26419,
-                      total_price: 505712498,
-                      kandang: {
-                        id: 1,
-                        name: 'cibeber 1',
-                      },
-                      payment_status: 'Paid',
-                    },
-                  ],
-                },
-              },
-              null,
-              2
-            )}
-          
-
-
); }; From eaf118845c4af32c0f3962ee60de20c7e5276f7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 19:15:38 +0700 Subject: [PATCH 21/35] feat(FE-327): Include Kandang in sales data and display name --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++++- src/types/api/closing/closing.d.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 64247890..e2956bb6 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -10,6 +10,7 @@ import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; +import { Kandang } from '@type/api/master-data/kandang'; interface SalesReportTableProps { type?: 'detail'; @@ -298,7 +299,10 @@ const SalesReportTable = ({ id: 'kandang', accessorKey: 'kandang', header: 'Kandang', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const kandang = props.getValue() as Kandang; + return kandang?.name || '-'; + }, }, { id: 'payment_status', diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index 03217438..64d0d465 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -1,6 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; +import { Kandang } from '@type/api/master-data/kandang'; export type BaseSales = { id: number; @@ -14,6 +15,7 @@ export type BaseSales = { avg_weight: number; price: number; total_price: number; + kandang: Kandang; payment_status: string; }; From 4fe53f364a1756a1b91e3d89b3b4955e52907476 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 08:54:12 +0700 Subject: [PATCH 22/35] refactor(FE-326): Remove Tabs wrapper from SalesReportTable --- .../pages/closing/sale/SalesReportTable.tsx | 138 ++++++++---------- 1 file changed, 62 insertions(+), 76 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index e2956bb6..7213a621 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -1,6 +1,5 @@ 'use client'; -import Tabs from '@/components/Tabs'; import React, { useState, useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; import Table, { CustomHeaderRow } from '@/components/Table'; @@ -365,81 +364,68 @@ const SalesReportTable = ({ return ( <>
- -

Penjualan

- - 0} - footerContent={ - - - - - - - - - - - - - - - - - - - } - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - - - ), - }, - ]} - variant='lifted' - /> +
+

Penjualan

+ +
- Total Penjualan - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPriceAct)} - - {formatCurrency(totals.totalAct)} -
0} + footerContent={ + + + + + + + + + + + + + + + + + + + } + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'hidden', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + + ); From 4ff16499918b0483ebd0c48d19da14a88392fa96 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 08:56:14 +0700 Subject: [PATCH 23/35] chore(FE-327): Remove unused state from SalesReportTable --- .../pages/closing/sale/SalesReportTable.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 7213a621..a62c20a6 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; @@ -115,14 +115,9 @@ const SalesReportTable = ({ type = 'detail', initialValues, }: SalesReportTableProps) => { - const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseSales[] = useMemo(() => { - if (activeTabId === 'penjualan' && initialValues && initialValues.sales) { - return initialValues.sales; - } - return []; - }, [initialValues, activeTabId]); + return initialValues?.sales || []; + }, [initialValues]); const totals = useMemo(() => { if (salesBroilerData.length === 0) { From ff1493b520b00b4e4e35320b9add9154bf4c5816 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:09:41 +0700 Subject: [PATCH 24/35] refactor(FE-326): Remove avgPriceAct/totalAct and use partner totals, fix badge case --- .../pages/closing/sale/SalesReportTable.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index a62c20a6..dedcb498 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -127,8 +127,6 @@ const SalesReportTable = ({ avgWeight: 0, avgPricePartner: 0, totalPartner: 0, - avgPriceAct: 0, - totalAct: 0, }; } @@ -156,17 +154,12 @@ const SalesReportTable = ({ 0 ); - const avgPriceAct = avgPricePartner; - const totalAct = totalPartner; - return { totalQuantity, totalWeight, avgWeight, avgPricePartner, totalPartner, - avgPriceAct, - totalAct, }; }, [salesBroilerData]); @@ -307,12 +300,10 @@ const SalesReportTable = ({ const getStatusColor = (status: string) => { if (!status) return 'neutral'; switch (status.toLowerCase()) { - case 'lunas': + case 'paid': return 'success'; - case 'pending': + case 'tempo': return 'warning'; - case 'belum lunas': - return 'error'; default: return 'neutral'; } @@ -399,10 +390,10 @@ const SalesReportTable = ({ {formatCurrency(totals.totalPartner)} From aad24c3c58e97527b95e83bb212befe74de3ba54 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:12:02 +0700 Subject: [PATCH 25/35] refactor(FE-327): Rename salesBroilerData to salesData --- .../pages/closing/sale/SalesReportTable.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index dedcb498..a473dd14 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -115,12 +115,12 @@ const SalesReportTable = ({ type = 'detail', initialValues, }: SalesReportTableProps) => { - const salesBroilerData: BaseSales[] = useMemo(() => { + const salesData: BaseSales[] = useMemo(() => { return initialValues?.sales || []; }, [initialValues]); const totals = useMemo(() => { - if (salesBroilerData.length === 0) { + if (salesData.length === 0) { return { totalQuantity: 0, totalWeight: 0, @@ -130,17 +130,17 @@ const SalesReportTable = ({ }; } - const totalQuantity = salesBroilerData.reduce( + const totalQuantity = salesData.reduce( (sum, item) => sum + (item.qty || 0), 0 ); - const totalWeight = salesBroilerData.reduce( + const totalWeight = salesData.reduce( (sum, item) => sum + (item.weight || 0), 0 ); const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; - const validPriceItems = salesBroilerData.filter( + const validPriceItems = salesData.filter( (item) => item.price != null && item.price > 0 ); const avgPricePartner = @@ -149,7 +149,7 @@ const SalesReportTable = ({ validPriceItems.length : 0; - const totalPartner = salesBroilerData.reduce( + const totalPartner = salesData.reduce( (sum, item) => sum + (item.total_price || 0), 0 ); @@ -161,7 +161,7 @@ const SalesReportTable = ({ avgPricePartner, totalPartner, }; - }, [salesBroilerData]); + }, [salesData]); const salesColumns: ColumnDef[] = useMemo( () => [ @@ -359,11 +359,11 @@ const SalesReportTable = ({ }} >
+ Total Penjualan + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} +
- {formatCurrency(totals.avgPriceAct)} + {formatCurrency(totals.avgPricePartner)} - {formatCurrency(totals.totalAct)} + {formatCurrency(totals.totalPartner)}
0} + renderFooter={salesData.length > 0} footerContent={ From c9552dec2db111b47ed73f66fa46987d19f87ea7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:47:38 +0700 Subject: [PATCH 26/35] refactor(FE-326): Remove custom header rows and simplify Table --- src/components/Table.tsx | 208 ++++-------------- .../pages/closing/sale/SalesReportTable.tsx | 138 +----------- 2 files changed, 53 insertions(+), 293 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 69406220..b5148fea 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,8 +14,6 @@ import { SortingState, OnChangeFn, Row, - HeaderGroup, - Column, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -34,21 +32,6 @@ interface TableClassNames { bodyRowClassName?: string; bodyColumnClassName?: string; paginationClassName?: string; - customHeaderRowClassName?: string; - customHeaderCellClassName?: string; -} - -export interface CustomHeaderRow { - id: string; - cells: Array<{ - id: string; - content: ReactNode; - colSpan?: number; - rowSpan?: number; - className?: string; - field?: string; - }>; - className?: string; } export interface TableProps { @@ -69,13 +52,6 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); - customHeaderRows?: CustomHeaderRow[]; - renderCustomHeaders?: boolean; - onCustomHeaderCellRender?: ( - cell: ReactNode, - column: Column, - headerGroup: HeaderGroup - ) => ReactNode; renderFooter?: boolean; footerContent?: ReactNode; } @@ -111,8 +87,6 @@ const Table = ({ bodyRowClassName: '', bodyColumnClassName: '', paginationClassName: '', - customHeaderRowClassName: '', - customHeaderCellClassName: '', }, emptyContent = emptyContentDefaultValue, sorting, @@ -121,9 +95,6 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, - customHeaderRows = [], - renderCustomHeaders = false, - onCustomHeaderCellRender, renderFooter = false, footerContent, }: TableProps) => { @@ -228,143 +199,55 @@ const Table = ({
- {renderCustomHeaders && - customHeaderRows.length > 0 && - customHeaderRows.map((headerRow) => ( - - {headerRow.cells.map((cell) => { - const column = table - .getAllColumns() - .find((col) => col.id === cell.field); - - const canSort = column?.getCanSort(); - const sortingState = column?.getIsSorted(); - - return ( - - ); - })} - - ))} - {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { - let cellContent = flexRender( - header.column.columnDef.header, - header.getContext() - ); - - if (onCustomHeaderCellRender) { - cellContent = onCustomHeaderCellRender( - cellContent, - header.column, - headerGroup - ); - } - - return ( - - ); - })} + {header.column.getCanSort() && ( +
+ + +
+ )} + + + ))} ))} @@ -383,7 +266,6 @@ const Table = ({ ))} - {renderFooter && footerContent}
-
- {cell.content} - - {canSort && ( -
- - -
- )} -
-
( + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() )} - > -
- {cellContent} - {header.column.getCanSort() && ( -
- - -
- )} -
-
diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index a473dd14..3218d1d8 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; -import Table, { CustomHeaderRow } from '@/components/Table'; +import Table from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; @@ -16,101 +16,6 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } -interface HeaderCell { - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - field?: string; -} - -const generateCustomHeaders = (template: { - groups: Array<{ - label: string; - field?: string; - rowSpan?: number; - colSpan?: number; - subLabels?: string[]; - }>; -}): CustomHeaderRow[] => { - const mainRow: Array<{ - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - }> = []; - const subRow: Array<{ - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - }> = []; - let subColumnIndex = 0; - - template.groups.forEach((group) => { - if (group.subLabels) { - const mainCell: HeaderCell = { - id: `${group.field || 'group'}-${subColumnIndex}`, - content: group.label, - colSpan: group.colSpan, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200', - }; - - mainRow.push(mainCell); - - group.subLabels.forEach((subLabel) => { - const subCell: HeaderCell = { - id: `sub-${subColumnIndex}`, - content: subLabel, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', - }; - - if (group.label === 'Jumlah') { - subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight'; - } - - subRow.push(subCell); - subColumnIndex++; - }); - } else { - const mainCell: HeaderCell = { - id: `${group.field}-header`, - content: group.label, - rowSpan: group.rowSpan, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', - }; - - mainCell.field = group.field; - - mainRow.push(mainCell); - } - }); - - const rows: CustomHeaderRow[] = [ - { - id: 'main-header', - cells: mainRow, - className: 'bg-gray-50', - }, - ]; - - if (subRow.length > 0) { - rows.push({ - id: 'sub-header', - cells: subRow, - className: 'bg-gray-50', - }); - } - - return rows; -}; - const SalesReportTable = ({ type = 'detail', initialValues, @@ -310,7 +215,7 @@ const SalesReportTable = ({ }; return ( - + {status || '-'} ); @@ -320,33 +225,6 @@ const SalesReportTable = ({ [] ); - const headerTemplate = { - groups: [ - { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, - { label: 'Umur', field: 'age', rowSpan: 2 }, - { label: 'No. DO', field: 'do_number', rowSpan: 2 }, - { label: 'Produk', field: 'product', rowSpan: 2 }, - { label: 'Customer', field: 'customer', rowSpan: 2 }, - { - label: 'Jumlah', - colSpan: 2, - subLabels: ['Qty', 'Kg'], - }, - { label: 'AVG (Kg)', field: 'avg_weight', rowSpan: 2 }, - { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, - { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, - { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, - { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, - { label: 'Kandang', field: 'kandang', rowSpan: 2 }, - { label: 'Status Pembayaran', field: 'payment_status', rowSpan: 2 }, - ], - }; - - const salesCustomHeaderRows = useMemo( - () => generateCustomHeaders(headerTemplate), - [] - ); - return ( <>
@@ -361,8 +239,6 @@ const SalesReportTable = ({ 0} footerContent={ @@ -374,13 +250,13 @@ const SalesReportTable = ({ - - - ))} - {renderFooter && footerContent} + + {renderFooter && + (footerData && footerData.length > 0 + ? footerTable.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + : footerContent)} +
+ {formatNumber(totals.totalQuantity)} + {formatNumber(totals.totalWeight)} + {formatNumber(totals.avgWeight)} @@ -403,7 +279,9 @@ const SalesReportTable = ({ className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', bodyRowClassName: 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: From 27c867036f1d0ce3f1e9be7b0aeb935b2d973b99 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:51:40 +0700 Subject: [PATCH 27/35] refactor(FE-327): Update import paths for consistency in SalesReportTable --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 3218d1d8..1330b7f0 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,9 +7,9 @@ import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; -import { Product } from '@type/api/master-data/product'; -import { Customer } from '@type/api/master-data/customer'; -import { Kandang } from '@type/api/master-data/kandang'; +import { Product } from '@/types/api/master-data/product'; +import { Customer } from '@/types/api/master-data/customer'; +import { Kandang } from '@/types/api/master-data/kandang'; interface SalesReportTableProps { type?: 'detail'; From 99b9df27a78fa4f029615d5ff487b9cc1e72a22e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:58:38 +0700 Subject: [PATCH 28/35] refactor(FE-326): Comment _closing for copy-paste function --- src/app/{closing => _closing}/detail/layout.tsx | 0 src/app/{closing => _closing}/detail/page.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/{closing => _closing}/detail/layout.tsx (100%) rename src/app/{closing => _closing}/detail/page.tsx (97%) diff --git a/src/app/closing/detail/layout.tsx b/src/app/_closing/detail/layout.tsx similarity index 100% rename from src/app/closing/detail/layout.tsx rename to src/app/_closing/detail/layout.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/_closing/detail/page.tsx similarity index 97% rename from src/app/closing/detail/page.tsx rename to src/app/_closing/detail/page.tsx index 43a8469a..038e5072 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/_closing/detail/page.tsx @@ -34,7 +34,7 @@ const ClosingDetailPage = () => { } if (!isLoadingClosing && (!closing || isResponseError(closing))) { - // router.replace('/404'); + router.replace('/404'); return; } From e407410c4ab25c8e26f5eb4d0f055ba2331ea3a8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 10:25:40 +0700 Subject: [PATCH 29/35] feat(FE-Storyless): Add footer support to Table component --- src/components/Table.tsx | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b5148fea..5c76f44e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -31,6 +31,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -54,6 +57,7 @@ export interface TableProps { enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; footerContent?: ReactNode; + footerData?: TData[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -86,6 +90,9 @@ const Table = ({ tableBodyClassName: '', bodyRowClassName: '', bodyColumnClassName: '', + tableFooterClassName: '', + footerRowClassName: '', + footerColumnClassName: '', paginationClassName: '', }, emptyContent = emptyContentDefaultValue, @@ -97,6 +104,7 @@ const Table = ({ enableRowSelection, renderFooter = false, footerContent, + footerData = [], }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -164,6 +172,14 @@ const Table = ({ const table = useReactTable(tableOptions); const { setPageSize } = table; + const footerTableOptions: TableOptions = { + columns, + data: footerData, + getCoreRowModel: getCoreRowModel(), + }; + + const footerTable = useReactTable(footerTableOptions); + const prevPageClickHandler = () => { table.previousPage(); @@ -266,7 +282,26 @@ const Table = ({
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
From b3f7b8a3c530c0d782576e2d85f64d21e002fdf3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 10:26:26 +0700 Subject: [PATCH 30/35] feat(FE-326): Add totals footer row to sales report table --- .../pages/closing/sale/SalesReportTable.tsx | 172 +++++++++++++----- 1 file changed, 124 insertions(+), 48 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 1330b7f0..f0810f15 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -16,6 +16,10 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } +interface FooterSalesRow extends BaseSales { + _isFooter: true; +} + const SalesReportTable = ({ type = 'detail', initialValues, @@ -68,6 +72,29 @@ const SalesReportTable = ({ }; }, [salesData]); + const footerData = useMemo((): FooterSalesRow[] => { + if (salesData.length === 0) return []; + + const footerRow: FooterSalesRow = { + id: -999, + realization_date: 'Total Penjualan', + age: 0, + do_number: '', + product: {} as Product, + customer: {} as Customer, + qty: totals.totalQuantity, + weight: totals.totalWeight, + avg_weight: totals.avgWeight, + price: totals.avgPricePartner, + total_price: totals.totalPartner, + kandang: {} as Kandang, + payment_status: '', + _isFooter: true, + }; + + return [footerRow]; + }, [salesData, totals]); + const salesColumns: ColumnDef[] = useMemo( () => [ { @@ -75,6 +102,14 @@ const SalesReportTable = ({ accessorKey: 'realization_date', header: 'Tanggal Realisasi', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) { + return ( +
+ {props.row.original.realization_date} +
+ ); + } const date = props.row.original.realization_date; return date ? formatDate(date, 'DD MMM YYYY') : '-'; }, @@ -83,19 +118,27 @@ const SalesReportTable = ({ id: 'age', accessorKey: 'age', header: 'Umur', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + return isFooter ? null : props.getValue() || '-'; + }, }, { id: 'do_number', accessorKey: 'do_number', header: 'No. DO', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + return isFooter ? null : props.getValue() || '-'; + }, }, { id: 'product', accessorKey: 'product', header: 'Produk', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const product = props.getValue() as Product; return product?.name || '-'; }, @@ -105,6 +148,8 @@ const SalesReportTable = ({ accessorKey: 'customer', header: 'Customer', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const customer = props.getValue() as Customer; return customer?.name || '-'; }, @@ -115,9 +160,13 @@ const SalesReportTable = ({ header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -129,9 +178,13 @@ const SalesReportTable = ({ header: 'Kg', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -143,9 +196,13 @@ const SalesReportTable = ({ header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -157,7 +214,18 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -166,7 +234,18 @@ const SalesReportTable = ({ header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -175,7 +254,18 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -184,7 +274,18 @@ const SalesReportTable = ({ header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -192,6 +293,8 @@ const SalesReportTable = ({ accessorKey: 'kandang', header: 'Kandang', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const kandang = props.getValue() as Kandang; return kandang?.name || '-'; }, @@ -201,6 +304,9 @@ const SalesReportTable = ({ accessorKey: 'payment_status', header: 'Status Pembayaran', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const status = props.getValue() as string; const getStatusColor = (status: string) => { if (!status) return 'neutral'; @@ -239,43 +345,8 @@ const SalesReportTable = ({ 0} - footerContent={ - - - - - - - - - - - - - - - - - - - } className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', @@ -286,6 +357,11 @@ const SalesReportTable = ({ 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> From e90c7d993c4ad3b379a1904172b9352ec1eb6ead Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 11:44:46 +0700 Subject: [PATCH 31/35] =?UTF-8?q?Merge=20branch=20=E2=80=98development?= =?UTF-8?q?=E2=80=99=20of=20gitlab.com:mbugroup/lti-web-client=20into=20fe?= =?UTF-8?q?at/FE/US-285/marketing-closing-report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Table.tsx | 162 +++++++++----- .../pages/closing/sale/SalesReportTable.tsx | 209 +++++------------- src/types/api/closing.d.ts | 30 ++- 3 files changed, 197 insertions(+), 204 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 970c5bc1..9feb33e2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,7 @@ import { SortingState, OnChangeFn, Row, + HeaderContext, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -31,6 +32,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -53,6 +57,7 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + renderFooter?: boolean; withCheckbox?: boolean; rowOptions?: number[]; } @@ -67,18 +72,22 @@ const emptyContentDefaultValue = ( ); -const TABLE_DEFAULT_STYLING = { +export const TABLE_DEFAULT_STYLING = { containerClassName: 'w-full mb-20', tableWrapperClassName: 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableHeaderClassName: '', headerRowClassName: '', - headerColumnClassName: 'px-4 py-3 text-base-content/50', + headerColumnClassName: + 'px-4 py-3 border-base-content/10 text-base-content/50', tableBodyClassName: '', - bodyRowClassName: 'border-t border-t-base-content/10', + bodyRowClassName: 'border-t border-base-content/10', bodyColumnClassName: 'px-4 py-3 text-base-content', paginationClassName: '', + tableFooterClassName: 'font-semibold border-base-content/10', + footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', + footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', }; const Table = ({ @@ -100,6 +109,7 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + renderFooter = false, withCheckbox = false, rowOptions = [10, 20, 50, 100], }: TableProps) => { @@ -214,58 +224,82 @@ const Table = ({ key={headerGroup.id} className={tableClassNames.headerRowClassName} > - {headerGroup.headers.map((header) => ( - - ))} + {header.column.getCanSort() && ( +
+ + +
+ )} + + + ); + })} ))} @@ -290,6 +324,28 @@ const Table = ({ ))} + + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + ))} + + )} +
- Total Penjualan - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} -
-
- {flexRender( - header.column.columnDef.header, - header.getContext() + {headerGroup.headers.map((header) => { + const columnRelativeDepth = + header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = leafs[leafs.length - 1].depth - header.depth; + } + return ( +
1, + }, + tableClassNames.headerColumnClassName )} + > +
1, + })} + > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} - {header.column.getCanSort() && ( -
- - -
- )} -
-
+ {column.columnDef.footer && + flexRender(column.columnDef.footer, { + column, + header: column.columnDef, + table, + } as HeaderContext)} +
diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index f0810f15..e509eb7d 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -6,7 +6,7 @@ import Table from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; +import { BaseClosingSales, BaseSales } from '@/types/api/closing'; import { Product } from '@/types/api/master-data/product'; import { Customer } from '@/types/api/master-data/customer'; import { Kandang } from '@/types/api/master-data/kandang'; @@ -16,10 +16,6 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } -interface FooterSalesRow extends BaseSales { - _isFooter: true; -} - const SalesReportTable = ({ type = 'detail', initialValues, @@ -72,29 +68,6 @@ const SalesReportTable = ({ }; }, [salesData]); - const footerData = useMemo((): FooterSalesRow[] => { - if (salesData.length === 0) return []; - - const footerRow: FooterSalesRow = { - id: -999, - realization_date: 'Total Penjualan', - age: 0, - do_number: '', - product: {} as Product, - customer: {} as Customer, - qty: totals.totalQuantity, - weight: totals.totalWeight, - avg_weight: totals.avgWeight, - price: totals.avgPricePartner, - total_price: totals.totalPartner, - kandang: {} as Kandang, - payment_status: '', - _isFooter: true, - }; - - return [footerRow]; - }, [salesData, totals]); - const salesColumns: ColumnDef[] = useMemo( () => [ { @@ -102,43 +75,30 @@ const SalesReportTable = ({ accessorKey: 'realization_date', header: 'Tanggal Realisasi', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
- {props.row.original.realization_date} -
- ); - } const date = props.row.original.realization_date; return date ? formatDate(date, 'DD MMM YYYY') : '-'; }, + footer: () => ( +
Total Penjualan
+ ), }, { id: 'age', accessorKey: 'age', header: 'Umur', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, + cell: (props) => props.getValue() || '-', }, { id: 'do_number', accessorKey: 'do_number', header: 'No. DO', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, + cell: (props) => props.getValue() || '-', }, { id: 'product', accessorKey: 'product', header: 'Produk', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const product = props.getValue() as Product; return product?.name || '-'; }, @@ -148,47 +108,43 @@ const SalesReportTable = ({ accessorKey: 'customer', header: 'Customer', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const customer = props.getValue() as Customer; return customer?.name || '-'; }, }, { - id: 'qty', - accessorKey: 'qty', - header: 'Kuantitas', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, - }, - { - id: 'weight', - accessorKey: 'weight', - header: 'Kg', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, + id: 'jumlah', + header: 'Jumlah', + columns: [ + { + id: 'qty', + accessorKey: 'qty', + header: 'Kuantitas', + cell: (props) => { + const value = props.getValue() as number; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(totals.totalQuantity)} +
+ ), + }, + { + id: 'weight', + accessorKey: 'weight', + header: 'Kg', + cell: (props) => { + const value = props.getValue() as number; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(totals.totalWeight)} +
+ ), + }, + ], }, { id: 'avg_weight', @@ -196,17 +152,13 @@ const SalesReportTable = ({ header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); + return
{formatNumber(value)}
; }, + footer: () => ( +
+ {formatNumber(totals.avgWeight)} +
+ ), }, { id: 'price_partner', @@ -214,19 +166,13 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(totals.avgPricePartner)} +
+ ), }, { id: 'total_mitra', @@ -234,19 +180,13 @@ const SalesReportTable = ({ header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(totals.totalPartner)} +
+ ), }, { id: 'price_act', @@ -254,18 +194,7 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -274,18 +203,7 @@ const SalesReportTable = ({ header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -293,8 +211,6 @@ const SalesReportTable = ({ accessorKey: 'kandang', header: 'Kandang', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const kandang = props.getValue() as Kandang; return kandang?.name || '-'; }, @@ -304,9 +220,6 @@ const SalesReportTable = ({ accessorKey: 'payment_status', header: 'Status Pembayaran', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const status = props.getValue() as string; const getStatusColor = (status: string) => { if (!status) return 'neutral'; @@ -345,16 +258,14 @@ const SalesReportTable = ({ 0} className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', bodyColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', tableFooterClassName: diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 3f7ba816..95b2f57f 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -1,9 +1,34 @@ import { Area } from '@/types/api/master-data/area'; import { Fcr } from '@/types/api/master-data/fcr'; import { Flock } from '@/types/api/master-data/flock'; -import { Kandang } from '@/types/api/master-data/kandang'; import { Location } from '@/types/api/master-data/location'; -import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseSales = { + id: number; + realization_date: string; + age: number; + do_number: string; + product: Product; + customer: Customer; + qty: number; + weight: number; + avg_weight: number; + price: number; + total_price: number; + kandang: Kandang; + payment_status: string; +}; + +export type BaseClosingSales = { + project_type: string; + flock_id: number; + period: number; + sales: BaseSales[]; +}; export type BaseClosing = { id: number; @@ -53,3 +78,4 @@ export type ClosingIncomingSapronak = { }; export type ClosingOutgoingSapronak = ClosingIncomingSapronak; +export type ClosingSales = BaseMetadata & BaseClosingSales; From 7e999b2e347cea4382eb360270f6b456b7ee37b4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 11:53:47 +0700 Subject: [PATCH 32/35] feat(FE): Show sales report on closing detail page --- src/app/closing/detail/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 6225b8dd..487533be 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import ClosingDetail from '@/components/pages/closing/ClosingDetail'; +import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; import { ClosingApi } from '@/services/api/closing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; @@ -19,6 +20,11 @@ const ClosingDetailPage = () => { (id: number) => ClosingApi.getGeneralInfo(id) ); + const { data: salesReport, isLoading: isLoadingSalesReport } = useSWR( + closingId, + (id: number) => ClosingApi.getPenjualan(id) + ); + if (!closingId) { router.back(); @@ -43,6 +49,9 @@ const ClosingDetailPage = () => { {!isLoadingClosing && isResponseSuccess(closing) && ( )} + {!isLoadingSalesReport && isResponseSuccess(salesReport) && ( + + )} ); }; From eed142a85ffc38e29e06e49b75c13d51842dff2d Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 13:25:07 +0700 Subject: [PATCH 33/35] hotfix(FE): fixing dropdown logout and floating button max size --- src/app/production/project-flock/layout.tsx | 1 + src/components/FloatingActionsButton.tsx | 2 +- src/components/Navbar.tsx | 25 +-- src/components/dropdown/Dropdown.tsx | 116 ++++++++++++ src/components/dropdown/README.md | 83 ++++++++ src/components/helper/RequireAuth.tsx | 199 ++++++++++++++++---- src/services/api/closing.ts | 2 +- 7 files changed, 381 insertions(+), 47 deletions(-) create mode 100644 src/components/dropdown/Dropdown.tsx create mode 100644 src/components/dropdown/README.md diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx index 698064cf..b74ef612 100644 --- a/src/app/production/project-flock/layout.tsx +++ b/src/app/production/project-flock/layout.tsx @@ -52,6 +52,7 @@ export default function ProjectFlockLayout({ closeOnBackdropClick={isDetail ? true : false} onBackdropClick={handleBackdropClick} variant='right' + zIndex='99999' sidebarContent={isOpen &&
{children}
} /> diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx index c0033d72..c9ca3454 100644 --- a/src/components/FloatingActionsButton.tsx +++ b/src/components/FloatingActionsButton.tsx @@ -54,7 +54,7 @@ const FloatingActionsButton = ({
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 973bf031..bee92a57 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,6 +7,7 @@ import { Icon } from '@iconify/react'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; @@ -52,21 +53,21 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
-
-
-
- + +
+ +
-
- - + } + contentClassName='w-52 mt-3' + > + -
+
); diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx new file mode 100644 index 00000000..4489231d --- /dev/null +++ b/src/components/dropdown/Dropdown.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { ReactNode, useRef, useEffect, useState } from 'react'; +import { cn } from '@/lib/helper'; + +interface DropdownProps { + trigger: ReactNode; + children: ReactNode; + position?: + | 'top' + | 'bottom' + | 'left' + | 'right' + | 'top-start' + | 'top-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end' + | 'right-start' + | 'right-end'; + align?: 'start' | 'center' | 'end'; + hover?: boolean; + className?: string; + contentClassName?: string; +} + +const Dropdown = ({ + trigger, + children, + position = 'bottom', + align = 'start', + hover = false, + className, + contentClassName, +}: DropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + // Build position classes + const getPositionClasses = () => { + const classes: string[] = []; + + // Handle combined positions like 'top-start' + if (position.includes('-')) { + const [pos, al] = position.split('-'); + classes.push(`dropdown-${pos}`); + classes.push(`dropdown-${al}`); + } else { + classes.push(`dropdown-${position}`); + if (align !== 'start') { + classes.push(`dropdown-${align}`); + } + } + + return classes.join(' '); + }; + + const handleToggle = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + // alert('clicked'); + setIsOpen(!isOpen); + }; + + return ( +
+ {/* Trigger Button */} +
+ {trigger} +
+ + {/* Dropdown Content - Only render when open */} + {isOpen && ( +
setIsOpen(false)} // Close on item click + > + {children} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md new file mode 100644 index 00000000..e682682a --- /dev/null +++ b/src/components/dropdown/README.md @@ -0,0 +1,83 @@ +# Dropdown Component + +Komponen Dropdown reusable berdasarkan DaisyUI yang mengatasi issue children component yang ter-render sebelum dropdown dibuka. + +## Features + +- ✅ **Conditional Rendering**: Children hanya di-render ketika dropdown aktif/terbuka +- ✅ **Click Outside to Close**: Otomatis menutup dropdown ketika klik di luar area dropdown +- ✅ **Multiple Positions**: Support berbagai posisi (top, bottom, left, right) dengan alignment (start, center, end) +- ✅ **Hover Support**: Optional hover mode untuk membuka dropdown +- ✅ **Customizable**: Mendukung custom className untuk container dan content + +## Usage + +### Basic Example + +```tsx +import Dropdown from '@/components/dropdown/Dropdown'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; + +Click Me + } +> + + console.log('Item 1')} /> + console.log('Item 2')} /> + + +``` + +### With Position + +```tsx +Dropdown} + contentClassName="w-52 mt-3" +> + {/* Your content */} + +``` + +### Hover Mode + +```tsx +Hover Me} +> + {/* Your content */} + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `trigger` | `ReactNode` | - | **Required**. Element yang akan men-trigger dropdown | +| `children` | `ReactNode` | - | **Required**. Content dropdown yang akan ditampilkan | +| `position` | `'top' \| 'bottom' \| 'left' \| 'right' \| 'top-start' \| 'top-end' \| 'bottom-start' \| 'bottom-end' \| 'left-start' \| 'left-end' \| 'right-start' \| 'right-end'` | `'bottom'` | Posisi dropdown relatif terhadap trigger | +| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment dropdown (digunakan jika position tidak mengandung alignment) | +| `hover` | `boolean` | `false` | Aktifkan mode hover untuk membuka dropdown | +| `className` | `string` | - | Custom className untuk container dropdown | +| `contentClassName` | `string` | - | Custom className untuk content dropdown | + +## Position Examples + +- `bottom` - Dropdown muncul di bawah, align ke start +- `bottom-end` - Dropdown muncul di bawah, align ke end +- `bottom-center` - Dropdown muncul di bawah, align ke center +- `top-start` - Dropdown muncul di atas, align ke start +- `left-end` - Dropdown muncul di kiri, align ke end +- Dan seterusnya... + +## Key Benefits + +1. **Performance**: Children tidak di-render sampai dropdown dibuka, menghemat resources +2. **Clean State**: Setiap kali dropdown dibuka, children di-render fresh +3. **DaisyUI Compatible**: Menggunakan class DaisyUI yang sudah ada +4. **Accessible**: Menggunakan proper ARIA attributes dan keyboard navigation diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 119d74cb..dbd4b6bc 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; -import { AxiosError } from 'axios'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { GetMeResponse } from '@/types/api/api-general'; + +// TODO: delete this later, DONT HARDCODE USER DATA +const DUMMY_USER = { + id: 1, + email: 'admin@mbugroup.id', + npk: '0001', + name: 'Super Admin', + image: null, + created_at: '2025-09-30T03:24:20.899229Z', + updated_at: '2025-09-30T03:24:20.899229Z', + roles: [ + { + id: 1, + key: 'mbu.super_admin', + name: 'MBU Administrator', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + permissions: [ + { + id: 1, + name: 'mbu:purchase:read', + action: 'read', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 2, + name: 'mbu:purchase:create', + action: 'create', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 3, + name: 'mbu:purchase:approve', + action: 'approve', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + ], + }, + { + id: 2, + key: 'lti.super_admin', + name: 'LTI Administrator', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + permissions: [ + { + id: 4, + name: 'lti:purchase:read', + action: 'read', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 5, + name: 'lti:purchase:create', + action: 'create', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 6, + name: 'lti:purchase:approve', + action: 'approve', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + ], + }, + { + id: 3, + key: 'manbu.super_admin', + name: 'MANBU Administrator', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + permissions: [ + { + id: 7, + name: 'manbu:purchase:read', + action: 'read', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 8, + name: 'manbu:purchase:create', + action: 'create', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 9, + name: 'manbu:purchase:approve', + action: 'approve', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + ], + }, + ], +}; interface RequireAuthProps { children?: ReactNode; @@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { - data: userResponse, - isLoading: isLoadingUserResponse, - error: userErrorResponse, - } = useSWRImmutable< - GetMeResponse & { ok?: boolean }, - AxiosError, - SWRHttpKey - >('/sso/userinfo', httpClientFetcher, { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - }); + const { data: userResponse, isLoading: isLoadingUserResponse } = + useSWRImmutable( + '/auth/sso/userinfo', + httpClientFetcher, + { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + } + ); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else if ( - isResponseError(userErrorResponse?.response?.data) && - typeof window !== 'undefined' - ) { - router.replace( - `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` - ); + } else { + // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // TODO: remove this later, DONT HARDCODE USER DATA + setUser(DUMMY_USER); } - }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); + }, [userResponse, setIsLoadingUser, setUser]); - if (isLoadingUserResponse && !userResponse && !userErrorResponse) { - return ( -
- -
- ); - } + // TODO: uncomment this later + // if (isLoadingUserResponse && !userResponse) { + // return ( + //
+ // + //
+ // ); + // } - return <>{isResponseSuccess(userResponse) && children}; + return <>{children}; }; export default RequireAuth; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 041108d0..dc0d804a 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -51,4 +51,4 @@ export class ClosingApiService extends BaseApiService { } } -export const ClosingApi = new ClosingApiService('/closing'); +export const ClosingApi = new ClosingApiService('/closings'); From a116f7ca6672a400ee22a7e7a1a43e164467fa9e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 13:32:29 +0700 Subject: [PATCH 34/35] fix(FE): Remove closing detail page and layout --- src/app/_closing/detail/layout.tsx | 11 ------ src/app/_closing/detail/page.tsx | 55 ------------------------------ 2 files changed, 66 deletions(-) delete mode 100644 src/app/_closing/detail/layout.tsx delete mode 100644 src/app/_closing/detail/page.tsx diff --git a/src/app/_closing/detail/layout.tsx b/src/app/_closing/detail/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/_closing/detail/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/_closing/detail/page.tsx b/src/app/_closing/detail/page.tsx deleted file mode 100644 index 038e5072..00000000 --- a/src/app/_closing/detail/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; -import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; - -const ClosingDetailPage = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const closingId = searchParams.get('closingId'); - - const { data: closing, isLoading: isLoadingClosing } = useSWR( - closingId, - (id: string) => { - const numericId = parseInt(id, 10); - if (isNaN(numericId) || numericId <= 0) { - throw new Error('Invalid closing ID'); - } - return ClosingApi.getPenjualan(numericId); - } - ); - - if (!closingId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingClosing && (!closing || isResponseError(closing))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingClosing && ( -
- -
- )} - {!isLoadingClosing && isResponseSuccess(closing) && ( - - )} -
- ); -}; - -export default ClosingDetailPage; From f48cfca65058ab626ace3421acf5f575df90a99b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 13:35:42 +0700 Subject: [PATCH 35/35] fix(FE): revert require auth component --- src/components/dropdown/README.md | 83 ----------- src/components/helper/RequireAuth.tsx | 199 +++++--------------------- 2 files changed, 33 insertions(+), 249 deletions(-) delete mode 100644 src/components/dropdown/README.md diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md deleted file mode 100644 index e682682a..00000000 --- a/src/components/dropdown/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Dropdown Component - -Komponen Dropdown reusable berdasarkan DaisyUI yang mengatasi issue children component yang ter-render sebelum dropdown dibuka. - -## Features - -- ✅ **Conditional Rendering**: Children hanya di-render ketika dropdown aktif/terbuka -- ✅ **Click Outside to Close**: Otomatis menutup dropdown ketika klik di luar area dropdown -- ✅ **Multiple Positions**: Support berbagai posisi (top, bottom, left, right) dengan alignment (start, center, end) -- ✅ **Hover Support**: Optional hover mode untuk membuka dropdown -- ✅ **Customizable**: Mendukung custom className untuk container dan content - -## Usage - -### Basic Example - -```tsx -import Dropdown from '@/components/dropdown/Dropdown'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; - -Click Me - } -> - - console.log('Item 1')} /> - console.log('Item 2')} /> - - -``` - -### With Position - -```tsx -Dropdown} - contentClassName="w-52 mt-3" -> - {/* Your content */} - -``` - -### Hover Mode - -```tsx -Hover Me} -> - {/* Your content */} - -``` - -## Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `trigger` | `ReactNode` | - | **Required**. Element yang akan men-trigger dropdown | -| `children` | `ReactNode` | - | **Required**. Content dropdown yang akan ditampilkan | -| `position` | `'top' \| 'bottom' \| 'left' \| 'right' \| 'top-start' \| 'top-end' \| 'bottom-start' \| 'bottom-end' \| 'left-start' \| 'left-end' \| 'right-start' \| 'right-end'` | `'bottom'` | Posisi dropdown relatif terhadap trigger | -| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment dropdown (digunakan jika position tidak mengandung alignment) | -| `hover` | `boolean` | `false` | Aktifkan mode hover untuk membuka dropdown | -| `className` | `string` | - | Custom className untuk container dropdown | -| `contentClassName` | `string` | - | Custom className untuk content dropdown | - -## Position Examples - -- `bottom` - Dropdown muncul di bawah, align ke start -- `bottom-end` - Dropdown muncul di bawah, align ke end -- `bottom-center` - Dropdown muncul di bawah, align ke center -- `top-start` - Dropdown muncul di atas, align ke start -- `left-end` - Dropdown muncul di kiri, align ke end -- Dan seterusnya... - -## Key Benefits - -1. **Performance**: Children tidak di-render sampai dropdown dibuka, menghemat resources -2. **Clean State**: Setiap kali dropdown dibuka, children di-render fresh -3. **DaisyUI Compatible**: Menggunakan class DaisyUI yang sudah ada -4. **Accessible**: Menggunakan proper ARIA attributes dan keyboard navigation diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..119d74cb 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,147 +6,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { GetMeResponse } from '@/types/api/api-general'; - -// TODO: delete this later, DONT HARDCODE USER DATA -const DUMMY_USER = { - id: 1, - email: 'admin@mbugroup.id', - npk: '0001', - name: 'Super Admin', - image: null, - created_at: '2025-09-30T03:24:20.899229Z', - updated_at: '2025-09-30T03:24:20.899229Z', - roles: [ - { - id: 1, - key: 'mbu.super_admin', - name: 'MBU Administrator', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - permissions: [ - { - id: 1, - name: 'mbu:purchase:read', - action: 'read', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 2, - name: 'mbu:purchase:create', - action: 'create', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 3, - name: 'mbu:purchase:approve', - action: 'approve', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - ], - }, - { - id: 2, - key: 'lti.super_admin', - name: 'LTI Administrator', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - permissions: [ - { - id: 4, - name: 'lti:purchase:read', - action: 'read', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 5, - name: 'lti:purchase:create', - action: 'create', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 6, - name: 'lti:purchase:approve', - action: 'approve', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - ], - }, - { - id: 3, - key: 'manbu.super_admin', - name: 'MANBU Administrator', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - permissions: [ - { - id: 7, - name: 'manbu:purchase:read', - action: 'read', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 8, - name: 'manbu:purchase:create', - action: 'create', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 9, - name: 'manbu:purchase:approve', - action: 'approve', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - ], - }, - ], -}; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; interface RequireAuthProps { children?: ReactNode; @@ -156,17 +18,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWRImmutable< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + }); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -175,23 +40,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else { - // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); - // TODO: remove this later, DONT HARDCODE USER DATA - setUser(DUMMY_USER); + } else if ( + isResponseError(userErrorResponse?.response?.data) && + typeof window !== 'undefined' + ) { + router.replace( + `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` + ); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
- // - //
- // ); - // } + if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + return ( +
+ +
+ ); + } - return <>{children}; + return <>{isResponseSuccess(userResponse) && children}; }; export default RequireAuth;