mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f961b2f8b | |||
| a8c02243a4 | |||
| 82dca3b57e | |||
| 6668c7b1f9 | |||
| ce4f50c92a | |||
| 146192a5b3 | |||
| 27c24e7c82 | |||
| a99a399f09 | |||
| 37d0041a4f | |||
| 3647f1a1ea | |||
| 7b5049165a | |||
| 3839b46edc | |||
| b7f2bca931 | |||
| 802bf77bc5 | |||
| fd7b49ab93 |
@@ -48,3 +48,6 @@ next-env.d.ts
|
||||
|
||||
# rtk
|
||||
rtk.exe
|
||||
|
||||
# local specs
|
||||
/local-specs
|
||||
@@ -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"
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user