Compare commits

..

23 Commits

Author SHA1 Message Date
Rivaldi A N S 7f961b2f8b Merge branch 'feat/debt-supplier-general-export' into 'development'
[FEAT/FE] Debt Supplier General Export

See merge request mbugroup/lti-web-client!487
2026-05-20 04:14:59 +00:00
ValdiANS a8c02243a4 feat: implement export general and server-side export 2026-05-20 11:13:50 +07:00
Rivaldi A N S 82dca3b57e Merge branch 'feat/debt-supplier-general-export' into 'development'
[FEAT/FE] Debt Supplier General Export

See merge request mbugroup/lti-web-client!486
2026-05-19 10:33:48 +00:00
ValdiANS 6668c7b1f9 feat: update .gitignore 2026-05-19 16:07:08 +07:00
ValdiANS ce4f50c92a feat: create Export to Excel - General button 2026-05-19 16:06:54 +07:00
ValdiANS 146192a5b3 feat: create exportToExcelGeneral method 2026-05-19 16:06:41 +07:00
Rivaldi A N S 27c24e7c82 Merge branch 'fix/daily-checklist' into 'development'
[FIX/FE] Daily Checklist

See merge request mbugroup/lti-web-client!485
2026-05-19 07:47:51 +00:00
ValdiANS a99a399f09 fix: show kandang label even if its not loaded yet in kandang options 2026-05-19 14:44:12 +07:00
Rivaldi A N S 37d0041a4f Merge branch 'fix/daily-checklist' into 'development'
[FIX/FE] Daily Checklist

See merge request mbugroup/lti-web-client!483
2026-05-19 05:12:37 +00:00
ValdiANS 3647f1a1ea fix: make empty kandang end date required 2026-05-19 12:11:24 +07:00
ValdiANS 7b5049165a fix: add hasError props 2026-05-19 12:11:07 +07:00
Rivaldi A N S 3839b46edc Merge branch 'feat/server-side-sorting' into 'development'
[FEAT/FE] Server Side Sorting

See merge request mbugroup/lti-web-client!482
2026-05-19 04:54:47 +00:00
ValdiANS b7f2bca931 feat: add rtk filters.toml 2026-05-19 11:51:59 +07:00
ValdiANS 802bf77bc5 feat: add rtk instructions 2026-05-19 11:51:27 +07:00
ValdiANS fd7b49ab93 feat: implement server-side sorting in report expense 2026-05-19 11:51:17 +07:00
Rivaldi A N S 456070491f Merge branch 'fix/marketing' into 'development'
[FIX/FE] Marketing

See merge request mbugroup/lti-web-client!481
2026-05-18 07:43:05 +00:00
ValdiANS c12beca4d7 fix: recalculate qty if product change 2026-05-18 14:26:52 +07:00
ValdiANS 910981645b fix: remove unnecessary code 2026-05-18 14:25:19 +07:00
ValdiANS 82b5429d02 fix: update DeliveryOrderSchema validation, make all delivery_order should valid instead of some 2026-05-18 14:24:59 +07:00
ValdiANS 6c6f739fc0 fix: remove onAfterSubmit callback in useFormikErrorList 2026-05-18 14:20:30 +07:00
ValdiANS 001dafecb7 fix: adjust copywriting for approve button based on approval step number 2026-05-18 14:18:35 +07:00
Rivaldi A N S 4bb3ada779 Merge branch 'feat/server-side-sorting' into 'development'
[FEAT/FE] Server-Side Sorting

See merge request mbugroup/lti-web-client!480
2026-05-18 04:38:58 +00:00
ValdiANS 0b63dcb532 feat: implement server-side sorting in FinanceTable 2026-05-18 11:37:40 +07:00
16 changed files with 583 additions and 96 deletions
+3
View File
@@ -48,3 +48,6 @@ next-env.d.ts
# rtk
rtk.exe
# local specs
/local-specs
+13
View File
@@ -0,0 +1,13 @@
# Project-local RTK filters — commit this file with your repo.
# Filters here override user-global and built-in filters.
# Docs: https://github.com/rtk-ai/rtk#custom-filters
schema_version = 1
# Example: suppress build noise from a custom tool
# [filters.my-tool]
# description = "Compact my-tool output"
# match_command = "^my-tool\\s+build"
# strip_ansi = true
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
# max_lines = 30
# on_empty = "my-tool: ok"
+152
View File
@@ -260,3 +260,155 @@ const handleExportExcel = useCallback(async () => {
- Do **not** import `xlsx`, `@react-pdf/renderer`, `jspdf`, `exceljs` in page/tab components.
**Reference implementation:** `MarketingReportApiService.exportDailyMarketingToExcel` / `exportDailyMarketingToPDF` in [src/services/api/report/marketing-report.ts](src/services/api/report/marketing-report.ts), consumed by [src/components/pages/report/marketing/tab/DailyMarketingTab.tsx](src/components/pages/report/marketing/tab/DailyMarketingTab.tsx).
<!-- rtk-instructions v2 -->
# RTK (Rust Token Killer) - Token-Optimized Commands
## Golden Rule
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
**Important**: Even in command chains with `&&`, use `rtk`:
```bash
# ❌ Wrong
git add . && git commit -m "msg" && git push
# ✅ Correct
rtk git add . && rtk git commit -m "msg" && rtk git push
```
## RTK Commands by Workflow
### Build & Compile (80-90% savings)
```bash
rtk cargo build # Cargo build output
rtk cargo check # Cargo check output
rtk cargo clippy # Clippy warnings grouped by file (80%)
rtk tsc # TypeScript errors grouped by file/code (83%)
rtk lint # ESLint/Biome violations grouped (84%)
rtk prettier --check # Files needing format only (70%)
rtk next build # Next.js build with route metrics (87%)
```
### Test (60-99% savings)
```bash
rtk cargo test # Cargo test failures only (90%)
rtk go test # Go test failures only (90%)
rtk jest # Jest failures only (99.5%)
rtk vitest # Vitest failures only (99.5%)
rtk playwright test # Playwright failures only (94%)
rtk pytest # Python test failures only (90%)
rtk rake test # Ruby test failures only (90%)
rtk rspec # RSpec test failures only (60%)
rtk test <cmd> # Generic test wrapper - failures only
```
### Git (59-80% savings)
```bash
rtk git status # Compact status
rtk git log # Compact log (works with all git flags)
rtk git diff # Compact diff (80%)
rtk git show # Compact show (80%)
rtk git add # Ultra-compact confirmations (59%)
rtk git commit # Ultra-compact confirmations (59%)
rtk git push # Ultra-compact confirmations
rtk git pull # Ultra-compact confirmations
rtk git branch # Compact branch list
rtk git fetch # Compact fetch
rtk git stash # Compact stash
rtk git worktree # Compact worktree
```
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
### GitHub (26-87% savings)
```bash
rtk gh pr view <num> # Compact PR view (87%)
rtk gh pr checks # Compact PR checks (79%)
rtk gh run list # Compact workflow runs (82%)
rtk gh issue list # Compact issue list (80%)
rtk gh api # Compact API responses (26%)
```
### JavaScript/TypeScript Tooling (70-90% savings)
```bash
rtk pnpm list # Compact dependency tree (70%)
rtk pnpm outdated # Compact outdated packages (80%)
rtk pnpm install # Compact install output (90%)
rtk npm run <script> # Compact npm script output
rtk npx <cmd> # Compact npx command output
rtk prisma # Prisma without ASCII art (88%)
```
### Files & Search (60-75% savings)
```bash
rtk ls <path> # Tree format, compact (65%)
rtk read <file> # Code reading with filtering (60%)
rtk grep <pattern> # Search grouped by file (75%)
rtk find <pattern> # Find grouped by directory (70%)
```
### Analysis & Debug (70-90% savings)
```bash
rtk err <cmd> # Filter errors only from any command
rtk log <file> # Deduplicated logs with counts
rtk json <file> # JSON structure without values
rtk deps # Dependency overview
rtk env # Environment variables compact
rtk summary <cmd> # Smart summary of command output
rtk diff # Ultra-compact diffs
```
### Infrastructure (85% savings)
```bash
rtk docker ps # Compact container list
rtk docker images # Compact image list
rtk docker logs <c> # Deduplicated logs
rtk kubectl get # Compact resource list
rtk kubectl logs # Deduplicated pod logs
```
### Network (65-70% savings)
```bash
rtk curl <url> # Compact HTTP responses (70%)
rtk wget <url> # Compact download output (65%)
```
### Meta Commands
```bash
rtk gain # View token savings statistics
rtk gain --history # View command history with savings
rtk discover # Analyze Claude Code sessions for missed RTK usage
rtk proxy <cmd> # Run command without filtering (for debugging)
rtk init # Add RTK instructions to CLAUDE.md
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
```
## Token Savings Overview
| Category | Commands | Typical Savings |
| ---------------- | ------------------------------ | --------------- |
| Tests | vitest, playwright, cargo test | 90-99% |
| Build | next, tsc, lint, prettier | 70-87% |
| Git | status, log, diff, add, commit | 59-80% |
| GitHub | gh pr, gh run, gh issue | 26-87% |
| Package Managers | pnpm, npm, npx | 70-90% |
| Files | ls, read, grep, find | 60-75% |
| Infrastructure | docker, kubectl | 85% |
| Network | curl, wget | 65-70% |
Overall average: **60-90% token reduction** on common development operations.
<!-- /rtk-instructions -->
+66 -18
View File
@@ -1,7 +1,12 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import { CellContext, ColumnDef } from '@tanstack/react-table';
import {
CellContext,
ColumnDef,
SortingState,
Updater,
} from '@tanstack/react-table';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { useFormik } from 'formik';
@@ -183,7 +188,8 @@ const FinanceTable = () => {
bankIds: '',
customerIds: '',
supplierIds: '',
sortBy: '',
sort_by: '',
orderBy: '',
startDate: '',
endDate: '',
bankNames: '',
@@ -197,7 +203,8 @@ const FinanceTable = () => {
bankIds: 'bank_ids',
customerIds: 'customer_ids',
supplierIds: 'supplier_ids',
sortBy: 'sort_date',
sort_by: 'sort_by',
orderBy: 'sort_order',
startDate: 'start_date',
endDate: 'end_date',
},
@@ -248,7 +255,7 @@ const FinanceTable = () => {
updateFilter('bankIds', values.bank_ids, true);
updateFilter('customerIds', values.customer_ids, true);
updateFilter('supplierIds', values.supplier_ids, true);
updateFilter('sortBy', values.sort_by, true);
updateFilter('sort_by', values.sort_by, true);
updateFilter('startDate', values.start_date, true);
updateFilter('endDate', values.end_date, true);
// Save display names for restoration on modal reopen
@@ -276,7 +283,8 @@ const FinanceTable = () => {
updateFilter('bankIds', '', true);
updateFilter('customerIds', '', true);
updateFilter('supplierIds', '', true);
updateFilter('sortBy', '', true);
updateFilter('sort_by', '', true);
updateFilter('orderBy', '', true);
updateFilter('startDate', '', true);
updateFilter('endDate', '', true);
updateFilter('bankNames', '', true);
@@ -394,6 +402,26 @@ const FinanceTable = () => {
);
};
const sorting: SortingState = tableFilterState.sort_by
? [
{
id: tableFilterState.sort_by,
desc: tableFilterState.orderBy === 'desc',
},
]
: [];
const handleSortingChange = (updater: Updater<SortingState>) => {
const next = typeof updater === 'function' ? updater(sorting) : updater;
if (next.length > 0) {
updateFilter('sort_by', next[0].id, true);
updateFilter('orderBy', next[0].desc ? 'desc' : 'asc', true);
} else {
updateFilter('sort_by', '', true);
updateFilter('orderBy', '', true);
}
};
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const endDate = filterFormik.values.end_date;
@@ -505,7 +533,7 @@ const FinanceTable = () => {
// Restore sort by
const restoredSortBy =
sortByOptions.find(
(opt) => String(opt.value) === tableFilterState.sortBy
(opt) => String(opt.value) === tableFilterState.sort_by
) || null;
setSelectedSortBy(restoredSortBy);
@@ -516,7 +544,7 @@ const FinanceTable = () => {
bank_ids: tableFilterState.bankIds || '',
customer_ids: tableFilterState.customerIds || '',
supplier_ids: tableFilterState.supplierIds || '',
sort_by: tableFilterState.sortBy || '',
sort_by: tableFilterState.sort_by || '',
start_date: tableFilterState.startDate || '',
end_date: tableFilterState.endDate || '',
});
@@ -540,10 +568,12 @@ const FinanceTable = () => {
{
header: 'ID',
accessorKey: 'payment_code',
enableSorting: true,
},
{
header: 'References Number',
accessorKey: 'reference_number',
enableSorting: true,
cell: (props: CellContext<Finance, unknown>) => {
const value = props.row.original.reference_number;
return <span>{value ?? '-'}</span>;
@@ -552,6 +582,7 @@ const FinanceTable = () => {
{
header: 'Jenis Transaksi',
accessorKey: 'transaction_type',
enableSorting: true,
cell: (props: CellContext<Finance, unknown>) => {
const value = props.row.original.transaction_type
.split('_')
@@ -561,7 +592,8 @@ const FinanceTable = () => {
},
{
header: 'Pihak',
accessorFn: (finance: Finance) => finance.party?.name,
accessorKey: 'customer_name',
enableSorting: true,
cell: (props: CellContext<Finance, unknown>) => {
if (props.row.original.party?.id) {
return <span>{props.row.original.party?.name}</span>;
@@ -571,16 +603,22 @@ const FinanceTable = () => {
},
{
header: 'Tanggal Pembayaran',
accessorFn: (finance: Finance) =>
formatDate(finance.payment_date, 'DD MMM YYYY'),
accessorKey: 'payment_date',
enableSorting: true,
cell: (props) =>
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
},
{
header: 'Tanggal Dibuat',
accessorFn: (finance) => formatDate(finance.created_at, 'DD MMM YYYY'),
accessorKey: 'created_at',
enableSorting: true,
cell: (props) =>
formatDate(props.row.original.created_at, 'DD MMM YYYY'),
},
{
header: 'Metode Pembayaran',
accessorKey: 'payment_method',
enableSorting: true,
cell: (props: CellContext<Finance, unknown>) => {
const value = props.row.original.payment_method.split('_').join(' ');
return <span>{formatTitleCase(value)}</span>;
@@ -588,20 +626,26 @@ const FinanceTable = () => {
},
{
header: 'Bank',
accessorFn: (finance: Finance) =>
finance.bank
? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`
accessorKey: 'bank',
enableSorting: true,
cell: (props) =>
props.row.original.bank
? `${props.row.original.bank?.alias} - ${props.row.original.bank?.account_number} - ${props.row.original.bank?.owner}`
: '-',
},
{
header: 'Pengeluaran (Rp)',
accessorFn: (finance: Finance) =>
formatCurrency(Math.abs(finance.expense_amount)),
accessorKey: 'expense_amount',
enableSorting: true,
cell: (props) =>
formatCurrency(Math.abs(props.row.original.expense_amount)),
},
{
header: 'Pemasukan (Rp)',
accessorFn: (finance: Finance) =>
formatCurrency(Math.abs(finance.income_amount)),
accessorKey: 'income_amount',
enableSorting: true,
cell: (props) =>
formatCurrency(Math.abs(props.row.original.income_amount)),
},
{
header: 'Aksi',
@@ -707,6 +751,7 @@ const FinanceTable = () => {
'page',
'pageSize',
'search',
'orderBy',
'bankNames',
'customerNames',
'supplierNames',
@@ -749,6 +794,9 @@ const FinanceTable = () => {
onPageChange={setPage}
onPageSizeChange={setPageSize}
isLoading={isLoading}
sorting={sorting}
setSorting={handleSortingChange}
manualSorting
className={{
containerClassName: cn('p-3 mb-0'),
headerColumnClassName: 'text-nowrap',
@@ -849,7 +849,11 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
disabled={deliveryRejected}
>
Approve
{marketing?.data?.latest_approval?.step_number === 1 &&
'Approve'}
{marketing?.data?.latest_approval?.step_number === 2 &&
'Deliver Item'}
</Button>
</div>
)}
@@ -246,6 +246,7 @@ const SalesOrderFormModal = ({
})
.filter((item) => Boolean(item)),
} as UpdateDeliveryOrderPayload);
switch (modalAction) {
case 'add':
await createMarketingHandler(payload as CreateSalesOrderPayload);
@@ -261,11 +262,7 @@ const SalesOrderFormModal = ({
// ===== Formik Error List =====
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
useFormikErrorList(formik, {
onAfterSubmit: () => {
router.push('/marketing');
},
});
useFormikErrorList(formik);
// ================== FORM REPEATER HANDLER ==================
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
@@ -71,14 +71,14 @@ export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
.required('Pengiriman wajib diisi!')
.test(
'at-least-one-valid-row',
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
'Seluruh data pengiriman harus diisi lengkap!',
function (items) {
if (!items || items.length === 0) return false;
// VALIDASI: minimal 1 item valid full
// VALIDASI: seluruh item harus valid full
const itemSchema = DeliveryOrderProductSchema;
const hasValidItem = items.some((item) => {
const hasValidItem = items.every((item) => {
if (!item) return false;
return itemSchema.isValidSync(item, { abortEarly: true });
});
@@ -146,15 +146,6 @@ const DeliveryOrderProductForm = ({
);
// ============ Fetch Data ============
const { data: productData } = useSWR(
selectedProduct?.value
? ProductApi.basePath + '/' + selectedProduct?.value
: null,
() =>
selectedProduct?.value
? ProductApi.getSingle(Number(selectedProduct?.value))
: undefined
);
// Options Week dari minggu 1 - 22
// const optionsWeek = useMemo(() => {
@@ -440,7 +431,8 @@ const DeliveryOrderProductForm = ({
handleBlurField(currentInput);
formik.setFieldValue(
'uom',
isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
initialValues?.marketing_product?.product_warehouse_data?.product?.uom
?.name ?? ''
);
},
}
@@ -813,9 +805,8 @@ const DeliveryOrderProductForm = ({
endAdornment={
<div className='flex items-center gap-2'>
<span className='text-sm text-gray-500'>
{isResponseSuccess(productData)
? productData?.data?.uom.name
: ''}
{initialValues?.marketing_product?.product_warehouse_data
?.product?.uom?.name ?? ''}
</span>
</div>
}
@@ -826,9 +817,8 @@ const DeliveryOrderProductForm = ({
(item) => item.id === formik.values.marketing_product_id
)?.qty +
' ' +
(isResponseSuccess(productData)
? productData?.data?.uom.name
: '')
(initialValues?.marketing_product?.product_warehouse_data
?.product?.uom?.name ?? '')
: ''
}
/>
@@ -252,6 +252,11 @@ const SalesOrderProductForm = ({
setSelectedProductWarehouse(productWarehouse || null);
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
formik.setFieldValue('qty', productWarehouse?.quantity);
if (productWarehouse?.quantity) {
handleFieldChange('qty', productWarehouse?.quantity);
}
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
if (
productWarehouse?.week !== undefined &&
@@ -39,7 +39,7 @@ import {
} from '@/services/api/master-data';
import { Supplier } from '@/types/api/master-data/supplier';
import { Nonstock } from '@/types/api/master-data/nonstock';
import { ColumnDef } from '@tanstack/react-table';
import { ColumnDef, SortingState, Updater } from '@tanstack/react-table';
import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import ButtonFilter from '@/components/helper/ButtonFilter';
@@ -73,6 +73,25 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// ===== SORTING STATE =====
const [sortBy, setSortBy] = useState('');
const [orderBy, setOrderBy] = useState('');
const sorting: SortingState = sortBy
? [{ id: sortBy, desc: orderBy === 'desc' }]
: [];
const handleSortingChange = (updater: Updater<SortingState>) => {
const next = typeof updater === 'function' ? updater(sorting) : updater;
if (next.length > 0) {
setSortBy(next[0].id);
setOrderBy(next[0].desc ? 'desc' : 'asc');
} else {
setSortBy('');
setOrderBy('');
}
};
const handleFilterModalOpenRef = useRef(() => {});
const filterModal = useModal();
@@ -252,6 +271,8 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
if (filterParams.category) {
params.append('category', filterParams.category);
}
if (sortBy) params.append('sort_by', sortBy);
if (orderBy) params.append('sort_order', orderBy);
Object.entries(extraParams ?? {}).forEach(([key, value]) => {
params.set(key, value);
@@ -259,7 +280,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
return params.toString();
},
[filterParams]
[filterParams, sortBy, orderBy]
);
// ===== DATA FETCHING =====
@@ -443,19 +464,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
return [
{
header: 'No',
enableSorting: false,
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
},
{
header: 'No. PO',
accessorKey: 'po_number',
enableSorting: true,
},
{
header: 'No. Referensi',
accessorKey: 'reference_number',
enableSorting: true,
},
{
header: 'Tanggal Realisasi',
accessorKey: 'realization_date',
enableSorting: true,
cell: ({ row }) => {
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
},
@@ -463,6 +488,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
{
header: 'Tanggal Transaksi',
accessorKey: 'transaction_date',
enableSorting: true,
cell: ({ row }) => {
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
},
@@ -470,21 +496,30 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
{
header: 'Kategori',
accessorKey: 'category',
enableSorting: true,
},
{
header: 'Produk',
accessorKey: 'product',
enableSorting: true,
accessorFn: (row) => row.pengajuan?.nonstock?.name,
},
{
header: 'Supplier',
accessorKey: 'supplier',
enableSorting: true,
accessorFn: (row) => row.supplier?.name,
},
{
header: 'Lokasi',
accessorKey: 'location',
enableSorting: true,
accessorFn: (row) => row.kandang?.location?.name,
},
{
header: 'Kandang',
accessorKey: 'kandang',
enableSorting: true,
accessorFn: (row) => row.kandang?.name,
},
{
@@ -492,23 +527,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
columns: [
{
header: 'Qty',
id: 'qty_pengajuan',
accessorFn: (row) => row.pengajuan?.qty,
accessorKey: 'qty_pengajuan',
cell: ({ row }) =>
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
},
{
header: 'Harga',
id: 'harga_pengajuan',
accessorFn: (row) => row.pengajuan?.price,
accessorKey: 'price_pengajuan',
cell: ({ row }) =>
formatCurrency(row.original.pengajuan?.price || 0),
},
{
header: 'Total',
id: 'total_pengajuan',
accessorFn: (row) =>
(row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0),
accessorKey: 'total_pengajuan',
cell: ({ row }) => {
const total =
(row.original.pengajuan?.qty || 0) *
@@ -523,23 +554,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
columns: [
{
header: 'Qty',
id: 'qty_realisasi',
accessorFn: (row) => row.realisasi?.qty,
accessorKey: 'qty_realisasi',
cell: ({ row }) =>
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
},
{
header: 'Harga',
id: 'harga_realisasi',
accessorFn: (row) => row.realisasi?.price,
accessorKey: 'price_realisasi',
cell: ({ row }) =>
formatCurrency(row.original.realisasi?.price || 0),
},
{
header: 'Total',
id: 'total_realisasi',
accessorFn: (row) =>
(row.realisasi?.qty || 0) * (row.realisasi?.price || 0),
accessorKey: 'total_realisasi',
cell: ({ row }) => {
const total =
(row.original.realisasi?.qty || 0) *
@@ -550,6 +577,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
],
},
{
id: 'realization_status',
header: 'Status Pencairan',
cell: (props) => (
<RealizationStatusBadge
@@ -558,6 +586,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
),
},
{
id: 'bop_status',
header: 'Status BOP',
cell: (props) => (
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
@@ -602,6 +631,9 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
totalItems={meta?.total_results || 0}
onPageChange={setPage}
onPageSizeChange={setPageSize}
sorting={sorting}
setSorting={handleSortingChange}
manualSorting
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName: 'overflow-x-auto',
@@ -31,7 +31,6 @@ import {
CustomerPaymentFilterSchema,
CustomerPaymentFilterType,
} from '@/components/pages/report/finance/filter/CustomerPaymentFilter';
import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX';
import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF';
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
@@ -55,7 +54,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
// ===== STATE MANAGEMENT =====
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
useState(false);
const isAnyExportLoading =
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
// ===== PAGINATION STATE =====
const [currentPage, setCurrentPage] = useState(1);
@@ -294,28 +296,39 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
}, [filterParams]);
// ===== EXPORT HANDLERS =====
const handleExportExcelGeneral = useCallback(async () => {
setIsExcelGeneralExportLoading(true);
try {
await FinanceApi.exportCustomerPaymentToExcelGeneral(
filterParams.customer_ids,
filterParams.filter_by,
filterParams.start_date,
filterParams.end_date
);
toast.success('Excel General berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
} finally {
setIsExcelGeneralExportLoading(false);
}
}, [filterParams]);
const handleExportExcel = useCallback(async () => {
setIsExcelExportLoading(true);
try {
const allDataForExport = await customerPaymentExport();
if (
!allDataForExport ||
!Array.isArray(allDataForExport) ||
allDataForExport.length === 0
) {
toast.error('Tidak ada data untuk diekspor.');
return;
}
await generateCustomerPaymentExcel({ data: allDataForExport });
await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet(
filterParams.customer_ids,
filterParams.filter_by,
filterParams.start_date,
filterParams.end_date
);
toast.success('Excel berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat Excel. Silakan coba lagi.');
} finally {
setIsExcelExportLoading(false);
}
}, [customerPaymentExport]);
}, [filterParams]);
const handleExportPdf = useCallback(async () => {
setIsPdfExportLoading(true);
@@ -422,8 +435,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
Export to Excel - Customer Per Sheet
</Button>
<Button
variant='ghost'
color='none'
onClick={handleExportExcelGeneral}
isLoading={isExcelGeneralExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel - General
</Button>
<Button
variant='ghost'
color='none'
@@ -450,8 +474,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
}, [
tabId,
isAnyExportLoading,
handleExportExcelGeneral,
handleExportExcel,
handleExportPdf,
isExcelGeneralExportLoading,
isExcelExportLoading,
isPdfExportLoading,
filterParams,
@@ -14,7 +14,6 @@ import {
DebtSupplier,
DebtSupplierFilter,
} from '@/types/api/report/debt-supplier';
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
@@ -77,7 +76,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
// ===== STATE MANAGEMENT =====
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
useState(false);
const isAnyExportLoading =
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
// ===== PAGINATION STATE =====
const [currentPage, setCurrentPage] = useState(1);
@@ -249,25 +251,19 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
const handleExportExcel = useCallback(async () => {
setIsExcelExportLoading(true);
try {
const allDataForExport = await debtSupplierExport();
if (
!allDataForExport ||
!Array.isArray(allDataForExport) ||
allDataForExport.length === 0
) {
toast.error('Tidak ada data untuk diekspor.');
return;
}
generateDebtSupplierExcel({ data: allDataForExport });
await DebtSupplierApi.exportToExcelSupplierPerSheet(
filterParams.supplier_ids,
filterParams.filter_by,
filterParams.start_date,
filterParams.end_date
);
toast.success('Excel berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat Excel. Silakan coba lagi.');
} finally {
setIsExcelExportLoading(false);
}
}, [debtSupplierExport]);
}, [filterParams]);
const handleExportPdf = useCallback(async () => {
setIsPdfExportLoading(true);
@@ -308,6 +304,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
formik.values.endDate,
]);
const handleExportExcelGeneral = useCallback(async () => {
setIsExcelGeneralExportLoading(true);
try {
await DebtSupplierApi.exportToExcelGeneral(
filterParams.supplier_ids,
filterParams.filter_by,
filterParams.start_date,
filterParams.end_date
);
toast.success('Excel General berhasil dibuat dan diunduh.');
} catch {
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
} finally {
setIsExcelGeneralExportLoading(false);
}
}, [filterParams]);
// ===== TAB ACTIONS COMPONENT =====
const TabActions = useMemo(() => {
return function TabActionsComponent() {
@@ -370,7 +383,17 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
Export to Excel - Supplier Per Sheet
</Button>
<Button
variant='ghost'
color='none'
onClick={handleExportExcelGeneral}
isLoading={isExcelGeneralExportLoading}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel - General
</Button>
<Button
variant='ghost'
@@ -400,8 +423,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
filterParams,
isAnyExportLoading,
handleExportExcel,
handleExportExcelGeneral,
handleExportPdf,
isExcelExportLoading,
isExcelGeneralExportLoading,
isPdfExportLoading,
]);
@@ -20,6 +20,7 @@ interface DatePickerProps {
disabled?: boolean;
placeholder?: string;
formatDisplay?: (date: string) => string;
hasError?: boolean;
}
export function DatePicker({
@@ -28,6 +29,7 @@ export function DatePicker({
disabled = false,
placeholder = 'Select date',
formatDisplay,
hasError = false,
}: DatePickerProps) {
const [open, setOpen] = useState(false);
const [currentMonth, setCurrentMonth] = useState(() => {
@@ -154,7 +156,7 @@ export function DatePicker({
<Button
variant='outline'
disabled={disabled}
className='w-full justify-start text-left font-normal border-gray-200 hover:bg-gray-50'
className={`w-full justify-start text-left font-normal hover:bg-gray-50 ${hasError ? 'border-red-500 focus:ring-red-500' : 'border-gray-200'}`}
>
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
{date ? (
@@ -181,6 +181,13 @@ export function DailyChecklistContent() {
const [initialLoading, setInitialLoading] = useState(!!checklistIdFromUrl);
const [emptyKandangEndDate, setEmptyKandangEndDate] = useState<string>('');
const [emptyKandangEndDateError, setEmptyKandangEndDateError] =
useState<string>('');
const [preloadedKandang, setPreloadedKandang] = useState<{
id: string;
name: string;
} | null>(null);
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
const [documents, setDocuments] = useState<File[]>([]);
@@ -226,7 +233,11 @@ export function DailyChecklistContent() {
const rawDate = data.date || '';
setDate(rawDate.length > 10 ? rawDate.slice(0, 10) : rawDate);
skipKandangClearRef.current = true;
setKandangId(String(data.kandang?.id || ''));
const loadedKandangId = String(data.kandang?.id || '');
setKandangId(loadedKandangId);
if (data.kandang?.name) {
setPreloadedKandang({ id: loadedKandangId, name: data.kandang.name });
}
const isEmptyKandang =
!!data.empty_kandang || data.category === 'empty_kandang';
@@ -788,6 +799,11 @@ export function DailyChecklistContent() {
return;
}
if (emptyKandang && !emptyKandangEndDate) {
setEmptyKandangEndDateError('Tanggal akhir kandang kosong wajib diisi');
return;
}
setIsLoadingDraft(true);
try {
@@ -865,6 +881,11 @@ export function DailyChecklistContent() {
return;
}
if (emptyKandang && !emptyKandangEndDate) {
setEmptyKandangEndDateError('Tanggal akhir kandang kosong wajib diisi');
return;
}
if (!isKandangEmpty) {
if (selectedEmployees.length === 0) {
toast.error('Pilih minimal 1 ABK');
@@ -1150,9 +1171,17 @@ export function DailyChecklistContent() {
<SelectValue placeholder='Pilih kandang' />
</SelectTrigger>
<SelectContent onScroll={handleKandangScroll}>
{kandangOptions.map((kandang) => (
{preloadedKandang &&
!kandangOptions.some(
(k) => String(k.value) === preloadedKandang.id
) && (
<SelectItem value={preloadedKandang.id}>
{preloadedKandang.name}
</SelectItem>
)}
{kandangOptions.map((kandang, kandangIdx) => (
<SelectItem
key={kandang.value}
key={`${kandang.value}-${kandangIdx}`}
value={String(kandang.value)}
>
{kandang.label}
@@ -1224,11 +1253,20 @@ export function DailyChecklistContent() {
<div className='mt-1.5'>
<DatePicker
date={emptyKandangEndDate}
onDateChange={setEmptyKandangEndDate}
onDateChange={(val) => {
setEmptyKandangEndDate(val);
if (val) setEmptyKandangEndDateError('');
}}
disabled={!isChecklistStatusDraft}
placeholder='Pilih tanggal'
formatDisplay={formatDateForDisplay}
hasError={!!emptyKandangEndDateError}
/>
{emptyKandangEndDateError && (
<p className='text-xs text-red-500 mt-1'>
{emptyKandangEndDateError}
</p>
)}
</div>
</div>
</div>
+78
View File
@@ -1,4 +1,6 @@
import { BaseApiService } from '@/services/api/base';
import { httpClient } from '@/services/http/client';
import { formatDate } from '@/lib/helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { DebtSupplier } from '@/types/api/report/debt-supplier';
@@ -11,6 +13,82 @@ export class DebtSupplierApiService extends BaseApiService<
super(basePath);
}
async exportToExcelGeneral(
supplier_ids?: string,
filter_by?: string,
start_date?: string,
end_date?: string
) {
const params = new URLSearchParams();
if (supplier_ids) params.set('supplier_ids', supplier_ids);
if (filter_by) params.set('filter_by', filter_by);
if (start_date) params.set('start_date', start_date);
if (end_date) params.set('end_date', end_date);
params.set('export', 'excel-all');
params.set('page', '1');
params.set('limit', '99999999999');
const queryString = `?${params.toString()}`;
const res = await httpClient<Blob>(
`${this.basePath.replace(/\/$/, '')}/debt-supplier${queryString}`,
{
method: 'GET',
responseType: 'blob',
}
);
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `laporan-hutang-supplier-general-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
async exportToExcelSupplierPerSheet(
supplier_ids?: string,
filter_by?: string,
start_date?: string,
end_date?: string
) {
const params = new URLSearchParams();
if (supplier_ids) params.set('supplier_ids', supplier_ids);
if (filter_by) params.set('filter_by', filter_by);
if (start_date) params.set('start_date', start_date);
if (end_date) params.set('end_date', end_date);
params.set('export', 'excel');
params.set('page', '1');
params.set('limit', '99999999999');
const queryString = `?${params.toString()}`;
const res = await httpClient<Blob>(
`${this.basePath.replace(/\/$/, '')}/debt-supplier${queryString}`,
{
method: 'GET',
responseType: 'blob',
}
);
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `laporan-hutang-supplier-per-sheet-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
async getDebtSupplierReport(
supplier_ids?: string,
filter_by?: string,
+74
View File
@@ -1,4 +1,6 @@
import { BaseApiService } from '@/services/api/base';
import { httpClient } from '@/services/http/client';
import { formatDate } from '@/lib/helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
@@ -11,6 +13,78 @@ export class FinanceApiService extends BaseApiService<
super(basePath);
}
async exportCustomerPaymentToExcelGeneral(
customer_ids?: string,
filter_by?: string,
start_date?: string,
end_date?: string
) {
const params = new URLSearchParams();
if (customer_ids) params.set('customer_ids', customer_ids);
if (filter_by) params.set('filter_by', filter_by);
if (start_date) params.set('start_date', start_date);
if (end_date) params.set('end_date', end_date);
params.set('export', 'excel-all');
params.set('page', '1');
params.set('limit', '9999999999');
const res = await httpClient<Blob>(
`${this.basePath}/customer-payment?${params.toString()}`,
{
method: 'GET',
responseType: 'blob',
}
);
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `laporan-piutang-customer-general-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
async exportCustomerPaymentToExcelCustomerPerSheet(
customer_ids?: string,
filter_by?: string,
start_date?: string,
end_date?: string
) {
const params = new URLSearchParams();
if (customer_ids) params.set('customer_ids', customer_ids);
if (filter_by) params.set('filter_by', filter_by);
if (start_date) params.set('start_date', start_date);
if (end_date) params.set('end_date', end_date);
params.set('export', 'excel');
params.set('page', '1');
params.set('limit', '9999999999');
const res = await httpClient<Blob>(
`${this.basePath}/customer-payment?${params.toString()}`,
{
method: 'GET',
responseType: 'blob',
}
);
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement('a');
link.href = url;
const fileName = `laporan-piutang-customer-per-sheet-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();
}
async getCustomerPaymentReport(
customer_ids?: string,
// TODO: Uncomment when BE is ready