mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
Compare commits
34 Commits
2bf5f36a77
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| a314a62f1f | |||
| 37d0041a4f | |||
| 3647f1a1ea | |||
| 7b5049165a | |||
| 3839b46edc | |||
| b7f2bca931 | |||
| 802bf77bc5 | |||
| fd7b49ab93 | |||
| 456070491f | |||
| c12beca4d7 | |||
| 910981645b | |||
| 82b5429d02 | |||
| 6c6f739fc0 | |||
| 001dafecb7 | |||
| 4bb3ada779 | |||
| 0b63dcb532 | |||
| 23dd220b2f | |||
| 770c293257 | |||
| 3374ab4779 | |||
| 7a668c0cf9 | |||
| 14151f6f5a | |||
| 0275e66eda | |||
| 9bc5842493 | |||
| 4cad8aba64 | |||
| 7b5af69dd1 | |||
| 2e179b74ba | |||
| fe2a2dfb43 | |||
| 910a36857e | |||
| 58ddd9b991 | |||
| bb9c6ab969 | |||
| ddffdd1b27 | |||
| f097620c4b | |||
| 280d790f0c | |||
| 3a2e74b559 |
@@ -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.
|
- 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).
|
**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 -->
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ const Table = <TData extends object>({
|
|||||||
const tableOptions: TableOptions<TData> = {
|
const tableOptions: TableOptions<TData> = {
|
||||||
columns,
|
columns,
|
||||||
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
||||||
|
defaultColumn: { sortDescFirst: false },
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
secondaryButton={
|
secondaryButton={
|
||||||
secondaryButton
|
secondaryButton
|
||||||
? {
|
? {
|
||||||
|
...secondaryButton,
|
||||||
text: secondaryButton?.text ?? 'Tidak',
|
text: secondaryButton?.text ?? 'Tidak',
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
if (secondaryButton && secondaryButton?.onClick) {
|
if (secondaryButton && secondaryButton?.onClick) {
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
CellContext,
|
CellContext,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
Row,
|
Row,
|
||||||
SortingState,
|
SortingState,
|
||||||
|
Updater,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -47,7 +42,8 @@ import { BaseApiResponse } from '@/types/api/api-general';
|
|||||||
|
|
||||||
type ExpenseTableFilters = {
|
type ExpenseTableFilters = {
|
||||||
search: string;
|
search: string;
|
||||||
nameSort: string;
|
sort_by: string;
|
||||||
|
order_by: string;
|
||||||
transactionDate: string;
|
transactionDate: string;
|
||||||
realizationDate: string;
|
realizationDate: string;
|
||||||
locationId: string;
|
locationId: string;
|
||||||
@@ -242,7 +238,8 @@ const ExpensesTable = () => {
|
|||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
search: '',
|
search: '',
|
||||||
nameSort: '',
|
sort_by: '',
|
||||||
|
order_by: '',
|
||||||
transactionDate: '',
|
transactionDate: '',
|
||||||
realizationDate: '',
|
realizationDate: '',
|
||||||
locationId: '',
|
locationId: '',
|
||||||
@@ -261,7 +258,8 @@ const ExpensesTable = () => {
|
|||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
nameSort: 'sort_name',
|
sort_by: 'sort_by',
|
||||||
|
order_by: 'sort_order',
|
||||||
transactionDate: 'transaction_date',
|
transactionDate: 'transaction_date',
|
||||||
realizationDate: 'realization_date',
|
realizationDate: 'realization_date',
|
||||||
locationId: 'location_id',
|
locationId: 'location_id',
|
||||||
@@ -319,7 +317,26 @@ const ExpensesTable = () => {
|
|||||||
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
||||||
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const sorting: SortingState = tableFilterState.sort_by
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: tableFilterState.sort_by,
|
||||||
|
desc: tableFilterState.order_by === '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('order_by', next[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('order_by', '', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
parseInt(item)
|
parseInt(item)
|
||||||
@@ -437,10 +454,12 @@ const ExpensesTable = () => {
|
|||||||
cell: (props) => props.row.original.location?.name ?? '-',
|
cell: (props) => props.row.original.location?.name ?? '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'created_user',
|
||||||
accessorFn: (row) => row.created_user.name ?? '-',
|
accessorFn: (row) => row.created_user.name ?? '-',
|
||||||
header: 'Nama Pengaju',
|
header: 'Nama Pengaju',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'supplier',
|
||||||
accessorFn: (row) => row.supplier.name ?? '-',
|
accessorFn: (row) => row.supplier.name ?? '-',
|
||||||
header: 'Uraian',
|
header: 'Uraian',
|
||||||
},
|
},
|
||||||
@@ -454,17 +473,20 @@ const ExpensesTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status Pencairan',
|
header: 'Status Pencairan',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RealizationStatusBadge approval={props.row.original.latest_approval} />
|
<RealizationStatusBadge approval={props.row.original.latest_approval} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status BOP',
|
header: 'Status BOP',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
|
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'is_paid',
|
||||||
header: 'Status Lunas',
|
header: 'Status Lunas',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
return (
|
return (
|
||||||
@@ -478,6 +500,14 @@ const ExpensesTable = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.created_at
|
||||||
|
? formatDate(props.row.original.created_at, 'DD MMM YYYY')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -882,17 +912,6 @@ const ExpensesTable = () => {
|
|||||||
}
|
}
|
||||||
}, [getTableFilterQueryString]);
|
}, [getTableFilterQueryString]);
|
||||||
|
|
||||||
// track sorting
|
|
||||||
useEffect(() => {
|
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
|
||||||
|
|
||||||
if (!isNameSorted) {
|
|
||||||
updateFilter('nameSort', '', false);
|
|
||||||
} else {
|
|
||||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
|
||||||
}
|
|
||||||
}, [sorting, updateFilter]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -1051,7 +1070,8 @@ const ExpensesTable = () => {
|
|||||||
'page',
|
'page',
|
||||||
'pageSize',
|
'pageSize',
|
||||||
'search',
|
'search',
|
||||||
'nameSort',
|
'sort_by',
|
||||||
|
'order_by',
|
||||||
'userId',
|
'userId',
|
||||||
'locationName',
|
'locationName',
|
||||||
'vendorName',
|
'vendorName',
|
||||||
@@ -1152,7 +1172,8 @@ const ExpensesTable = () => {
|
|||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={setSorting}
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
enableRowSelection={tableEnableRowSelectionHandler}
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
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 useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -183,7 +188,8 @@ const FinanceTable = () => {
|
|||||||
bankIds: '',
|
bankIds: '',
|
||||||
customerIds: '',
|
customerIds: '',
|
||||||
supplierIds: '',
|
supplierIds: '',
|
||||||
sortBy: '',
|
sort_by: '',
|
||||||
|
orderBy: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
bankNames: '',
|
bankNames: '',
|
||||||
@@ -197,7 +203,8 @@ const FinanceTable = () => {
|
|||||||
bankIds: 'bank_ids',
|
bankIds: 'bank_ids',
|
||||||
customerIds: 'customer_ids',
|
customerIds: 'customer_ids',
|
||||||
supplierIds: 'supplier_ids',
|
supplierIds: 'supplier_ids',
|
||||||
sortBy: 'sort_date',
|
sort_by: 'sort_by',
|
||||||
|
orderBy: 'sort_order',
|
||||||
startDate: 'start_date',
|
startDate: 'start_date',
|
||||||
endDate: 'end_date',
|
endDate: 'end_date',
|
||||||
},
|
},
|
||||||
@@ -248,7 +255,7 @@ const FinanceTable = () => {
|
|||||||
updateFilter('bankIds', values.bank_ids, true);
|
updateFilter('bankIds', values.bank_ids, true);
|
||||||
updateFilter('customerIds', values.customer_ids, true);
|
updateFilter('customerIds', values.customer_ids, true);
|
||||||
updateFilter('supplierIds', values.supplier_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('startDate', values.start_date, true);
|
||||||
updateFilter('endDate', values.end_date, true);
|
updateFilter('endDate', values.end_date, true);
|
||||||
// Save display names for restoration on modal reopen
|
// Save display names for restoration on modal reopen
|
||||||
@@ -276,7 +283,8 @@ const FinanceTable = () => {
|
|||||||
updateFilter('bankIds', '', true);
|
updateFilter('bankIds', '', true);
|
||||||
updateFilter('customerIds', '', true);
|
updateFilter('customerIds', '', true);
|
||||||
updateFilter('supplierIds', '', true);
|
updateFilter('supplierIds', '', true);
|
||||||
updateFilter('sortBy', '', true);
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('orderBy', '', true);
|
||||||
updateFilter('startDate', '', true);
|
updateFilter('startDate', '', true);
|
||||||
updateFilter('endDate', '', true);
|
updateFilter('endDate', '', true);
|
||||||
updateFilter('bankNames', '', 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 startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const endDate = filterFormik.values.end_date;
|
const endDate = filterFormik.values.end_date;
|
||||||
@@ -505,7 +533,7 @@ const FinanceTable = () => {
|
|||||||
// Restore sort by
|
// Restore sort by
|
||||||
const restoredSortBy =
|
const restoredSortBy =
|
||||||
sortByOptions.find(
|
sortByOptions.find(
|
||||||
(opt) => String(opt.value) === tableFilterState.sortBy
|
(opt) => String(opt.value) === tableFilterState.sort_by
|
||||||
) || null;
|
) || null;
|
||||||
setSelectedSortBy(restoredSortBy);
|
setSelectedSortBy(restoredSortBy);
|
||||||
|
|
||||||
@@ -516,7 +544,7 @@ const FinanceTable = () => {
|
|||||||
bank_ids: tableFilterState.bankIds || '',
|
bank_ids: tableFilterState.bankIds || '',
|
||||||
customer_ids: tableFilterState.customerIds || '',
|
customer_ids: tableFilterState.customerIds || '',
|
||||||
supplier_ids: tableFilterState.supplierIds || '',
|
supplier_ids: tableFilterState.supplierIds || '',
|
||||||
sort_by: tableFilterState.sortBy || '',
|
sort_by: tableFilterState.sort_by || '',
|
||||||
start_date: tableFilterState.startDate || '',
|
start_date: tableFilterState.startDate || '',
|
||||||
end_date: tableFilterState.endDate || '',
|
end_date: tableFilterState.endDate || '',
|
||||||
});
|
});
|
||||||
@@ -540,10 +568,12 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'ID',
|
header: 'ID',
|
||||||
accessorKey: 'payment_code',
|
accessorKey: 'payment_code',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'References Number',
|
header: 'References Number',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.reference_number;
|
const value = props.row.original.reference_number;
|
||||||
return <span>{value ?? '-'}</span>;
|
return <span>{value ?? '-'}</span>;
|
||||||
@@ -552,6 +582,7 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Jenis Transaksi',
|
header: 'Jenis Transaksi',
|
||||||
accessorKey: 'transaction_type',
|
accessorKey: 'transaction_type',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.transaction_type
|
const value = props.row.original.transaction_type
|
||||||
.split('_')
|
.split('_')
|
||||||
@@ -561,7 +592,8 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorFn: (finance: Finance) => finance.party?.name,
|
accessorKey: 'customer_name',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
if (props.row.original.party?.id) {
|
if (props.row.original.party?.id) {
|
||||||
return <span>{props.row.original.party?.name}</span>;
|
return <span>{props.row.original.party?.name}</span>;
|
||||||
@@ -571,16 +603,22 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Pembayaran',
|
header: 'Tanggal Pembayaran',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'payment_date',
|
||||||
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Dibuat',
|
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',
|
header: 'Metode Pembayaran',
|
||||||
accessorKey: 'payment_method',
|
accessorKey: 'payment_method',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.payment_method.split('_').join(' ');
|
const value = props.row.original.payment_method.split('_').join(' ');
|
||||||
return <span>{formatTitleCase(value)}</span>;
|
return <span>{formatTitleCase(value)}</span>;
|
||||||
@@ -588,20 +626,26 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'bank',
|
||||||
finance.bank
|
enableSorting: true,
|
||||||
? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`
|
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)',
|
header: 'Pengeluaran (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'expense_amount',
|
||||||
formatCurrency(Math.abs(finance.expense_amount)),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatCurrency(Math.abs(props.row.original.expense_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'income_amount',
|
||||||
formatCurrency(Math.abs(finance.income_amount)),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatCurrency(Math.abs(props.row.original.income_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
@@ -707,6 +751,7 @@ const FinanceTable = () => {
|
|||||||
'page',
|
'page',
|
||||||
'pageSize',
|
'pageSize',
|
||||||
'search',
|
'search',
|
||||||
|
'orderBy',
|
||||||
'bankNames',
|
'bankNames',
|
||||||
'customerNames',
|
'customerNames',
|
||||||
'supplierNames',
|
'supplierNames',
|
||||||
@@ -749,6 +794,9 @@ const FinanceTable = () => {
|
|||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
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'
|
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||||
disabled={deliveryRejected}
|
disabled={deliveryRejected}
|
||||||
>
|
>
|
||||||
Approve
|
{marketing?.data?.latest_approval?.step_number === 1 &&
|
||||||
|
'Approve'}
|
||||||
|
|
||||||
|
{marketing?.data?.latest_approval?.step_number === 2 &&
|
||||||
|
'Deliver Item'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -297,6 +297,8 @@ const MarketingTable = () => {
|
|||||||
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isDeliveryLoading, setIsDeliveryLoading] = useState(false);
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
updateFilter('product_ids', '', true);
|
updateFilter('product_ids', '', true);
|
||||||
@@ -452,23 +454,33 @@ const MarketingTable = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const approveMarketingRes: BaseApiResponse<unknown> | undefined =
|
setIsApproveLoading(true);
|
||||||
approveAction === 'APPROVED'
|
|
||||||
? await MarketingApi.bulkApprovals(
|
|
||||||
idsToProcess,
|
|
||||||
nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER',
|
|
||||||
'',
|
|
||||||
notes || `APPROVED marketing ${idsToProcess.join(', ')}`
|
|
||||||
)
|
|
||||||
: await SalesOrderApi.bulkApprovals(idsToProcess, approveAction, notes);
|
|
||||||
|
|
||||||
if (isResponseSuccess(approveMarketingRes)) {
|
try {
|
||||||
confirmationModal.closeModal();
|
const approveMarketingRes: BaseApiResponse<unknown> | undefined =
|
||||||
toast.success(approveMarketingRes?.message as string);
|
approveAction === 'APPROVED'
|
||||||
setRowSelection({});
|
? await MarketingApi.bulkApprovals(
|
||||||
|
idsToProcess,
|
||||||
|
nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER',
|
||||||
|
'',
|
||||||
|
notes || `APPROVED marketing ${idsToProcess.join(', ')}`
|
||||||
|
)
|
||||||
|
: await SalesOrderApi.bulkApprovals(
|
||||||
|
idsToProcess,
|
||||||
|
approveAction,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(approveMarketingRes)) {
|
||||||
|
confirmationModal.closeModal();
|
||||||
|
toast.success(approveMarketingRes?.message as string);
|
||||||
|
setRowSelection({});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMarketing();
|
||||||
|
} finally {
|
||||||
|
setIsApproveLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMarketing();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const bulkDeliveryDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
const bulkDeliveryDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||||
@@ -530,13 +542,21 @@ const MarketingTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||||
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
setIsDeliveryLoading(true);
|
||||||
deliveryModal.closeModal();
|
try {
|
||||||
toast.success(res?.message as string);
|
const res = await SalesOrderApi.delivery(
|
||||||
refreshMarketing?.();
|
selectedItem?.id as number,
|
||||||
router.push(
|
notes
|
||||||
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
);
|
||||||
);
|
deliveryModal.closeModal();
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
refreshMarketing?.();
|
||||||
|
router.push(
|
||||||
|
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsDeliveryLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowCanSelect = useCallback(
|
const getRowCanSelect = useCallback(
|
||||||
@@ -772,6 +792,14 @@ const MarketingTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.created_at
|
||||||
|
? formatDate(props.row.original.created_at, 'DD MMM yyyy')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
maxSize: 80,
|
maxSize: 80,
|
||||||
@@ -1012,11 +1040,13 @@ const MarketingTable = () => {
|
|||||||
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan tahap ${selectedApprovalStep ?? '-'} (${idsToProcess.length} data)?`}
|
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan tahap ${selectedApprovalStep ?? '-'} (${idsToProcess.length} data)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
isLoading: isApproveLoading,
|
||||||
onClick: confirmationModal.closeModal,
|
onClick: confirmationModal.closeModal,
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
||||||
|
isLoading: isApproveLoading,
|
||||||
onClick: approveMarketingHandler,
|
onClick: approveMarketingHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1040,10 +1070,12 @@ const MarketingTable = () => {
|
|||||||
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
|
isLoading: isDeliveryLoading,
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
|
isLoading: isDeliveryLoading,
|
||||||
onClick: confirmationModalDeliveryClickHandler,
|
onClick: confirmationModalDeliveryClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1103,6 +1135,7 @@ const MarketingTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
|
disabled={isSubmittingBulkDelivery}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
bulkDeliveryModal.closeModal();
|
bulkDeliveryModal.closeModal();
|
||||||
setBulkDeliveryDate('');
|
setBulkDeliveryDate('');
|
||||||
@@ -1115,6 +1148,7 @@ const MarketingTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
color='success'
|
color='success'
|
||||||
isLoading={isSubmittingBulkDelivery}
|
isLoading={isSubmittingBulkDelivery}
|
||||||
|
disabled={isSubmittingBulkDelivery}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
submitBulkDeliveryApprovalHandler(
|
submitBulkDeliveryApprovalHandler(
|
||||||
idsToProcess,
|
idsToProcess,
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ const SalesOrderFormModal = ({
|
|||||||
})
|
})
|
||||||
.filter((item) => Boolean(item)),
|
.filter((item) => Boolean(item)),
|
||||||
} as UpdateDeliveryOrderPayload);
|
} as UpdateDeliveryOrderPayload);
|
||||||
|
|
||||||
switch (modalAction) {
|
switch (modalAction) {
|
||||||
case 'add':
|
case 'add':
|
||||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||||
@@ -261,11 +262,7 @@ const SalesOrderFormModal = ({
|
|||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
||||||
useFormikErrorList(formik, {
|
useFormikErrorList(formik);
|
||||||
onAfterSubmit: () => {
|
|
||||||
router.push('/marketing');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ================== FORM REPEATER HANDLER ==================
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
|||||||
.required('Pengiriman wajib diisi!')
|
.required('Pengiriman wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
'at-least-one-valid-row',
|
'at-least-one-valid-row',
|
||||||
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
|
'Seluruh data pengiriman harus diisi lengkap!',
|
||||||
function (items) {
|
function (items) {
|
||||||
if (!items || items.length === 0) return false;
|
if (!items || items.length === 0) return false;
|
||||||
|
|
||||||
// VALIDASI: minimal 1 item valid full
|
// VALIDASI: seluruh item harus valid full
|
||||||
const itemSchema = DeliveryOrderProductSchema;
|
const itemSchema = DeliveryOrderProductSchema;
|
||||||
|
|
||||||
const hasValidItem = items.some((item) => {
|
const hasValidItem = items.every((item) => {
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
return itemSchema.isValidSync(item, { abortEarly: true });
|
return itemSchema.isValidSync(item, { abortEarly: true });
|
||||||
});
|
});
|
||||||
@@ -123,8 +123,17 @@ export const SalesProductToFieldValues = (
|
|||||||
total_price: product.total_price,
|
total_price: product.total_price,
|
||||||
marketing_type: product.marketing_type
|
marketing_type: product.marketing_type
|
||||||
? {
|
? {
|
||||||
value: product.marketing_type,
|
value:
|
||||||
label: formatTitleCase(product.marketing_type),
|
product.marketing_type === 'AYAM' ||
|
||||||
|
product.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM,AYAM_PULLET'
|
||||||
|
: product.marketing_type,
|
||||||
|
label: formatTitleCase(
|
||||||
|
product.marketing_type === 'AYAM' ||
|
||||||
|
product.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM'
|
||||||
|
: product.marketing_type
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: product.convertion_unit
|
convertion_unit: product.convertion_unit
|
||||||
@@ -185,8 +194,17 @@ export const DeliveryProductToFieldValues = (
|
|||||||
marketing_product_id: item.marketing_product_id ?? salesOrder?.id,
|
marketing_product_id: item.marketing_product_id ?? salesOrder?.id,
|
||||||
marketing_type: salesOrder?.marketing_type
|
marketing_type: salesOrder?.marketing_type
|
||||||
? {
|
? {
|
||||||
value: salesOrder?.marketing_type,
|
value:
|
||||||
label: formatTitleCase(salesOrder?.marketing_type),
|
salesOrder?.marketing_type === 'AYAM' ||
|
||||||
|
salesOrder?.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM,AYAM_PULLET'
|
||||||
|
: salesOrder?.marketing_type,
|
||||||
|
label: formatTitleCase(
|
||||||
|
salesOrder?.marketing_type === 'AYAM' ||
|
||||||
|
salesOrder?.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM'
|
||||||
|
: salesOrder?.marketing_type
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: salesOrder?.convertion_unit
|
convertion_unit: salesOrder?.convertion_unit
|
||||||
|
|||||||
+6
-16
@@ -146,15 +146,6 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ============ Fetch Data ============
|
// ============ 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
|
// Options Week dari minggu 1 - 22
|
||||||
// const optionsWeek = useMemo(() => {
|
// const optionsWeek = useMemo(() => {
|
||||||
@@ -440,7 +431,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
handleBlurField(currentInput);
|
handleBlurField(currentInput);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'uom',
|
'uom',
|
||||||
isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
|
initialValues?.marketing_product?.product_warehouse_data?.product?.uom
|
||||||
|
?.name ?? ''
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -813,9 +805,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-gray-500'>
|
<span className='text-sm text-gray-500'>
|
||||||
{isResponseSuccess(productData)
|
{initialValues?.marketing_product?.product_warehouse_data
|
||||||
? productData?.data?.uom.name
|
?.product?.uom?.name ?? ''}
|
||||||
: ''}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -826,9 +817,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty +
|
)?.qty +
|
||||||
' ' +
|
' ' +
|
||||||
(isResponseSuccess(productData)
|
(initialValues?.marketing_product?.product_warehouse_data
|
||||||
? productData?.data?.uom.name
|
?.product?.uom?.name ?? '')
|
||||||
: '')
|
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -252,6 +252,11 @@ const SalesOrderProductForm = ({
|
|||||||
setSelectedProductWarehouse(productWarehouse || null);
|
setSelectedProductWarehouse(productWarehouse || null);
|
||||||
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
||||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||||
|
|
||||||
|
if (productWarehouse?.quantity) {
|
||||||
|
handleFieldChange('qty', productWarehouse?.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
||||||
if (
|
if (
|
||||||
productWarehouse?.week !== undefined &&
|
productWarehouse?.week !== undefined &&
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ const CustomersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'bank_name',
|
||||||
|
header: 'Nama Bank',
|
||||||
|
cell: (props) => props.row.original.bank_name || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Customer, unknown>) => {
|
cell: (props: CellContext<Customer, unknown>) => {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export const CustomerFormSchema = Yup.object({
|
|||||||
.email('Format email tidak valid!')
|
.email('Format email tidak valid!')
|
||||||
.required('Email wajib diisi!'),
|
.required('Email wajib diisi!'),
|
||||||
|
|
||||||
|
bank_name: Yup.string()
|
||||||
|
.min(3, 'Nama bank minimal 3 karakter!')
|
||||||
|
.required('Nama bank wajib diisi!'),
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ const CustomerForm = ({
|
|||||||
},
|
},
|
||||||
type: normalizeType(initialValues?.type),
|
type: normalizeType(initialValues?.type),
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
|
bank_name: initialValues?.bank_name ?? '',
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
@@ -164,6 +165,7 @@ const CustomerForm = ({
|
|||||||
pic_id: values.picId,
|
pic_id: values.picId,
|
||||||
type: (values.type as OptionType).value as string,
|
type: (values.type as OptionType).value as string,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
|
bank_name: values.bank_name,
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,6 +288,22 @@ const CustomerForm = ({
|
|||||||
errorMessage={formik.errors.phone}
|
errorMessage={formik.errors.phone}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Nama Bank'
|
||||||
|
name='bank_name'
|
||||||
|
placeholder='Masukkan nama bank customer'
|
||||||
|
value={formik.values.bank_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
||||||
|
}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.bank_name}
|
||||||
|
readOnly={formType === 'detail'}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -326,6 +326,11 @@ const SuppliersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'bank_name',
|
||||||
|
header: 'Nama Bank',
|
||||||
|
cell: (props) => props.row.original.bank_name || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'address',
|
accessorKey: 'address',
|
||||||
header: 'Alamat',
|
header: 'Alamat',
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export const SupplierFormSchema = Yup.object({
|
|||||||
npwp: Yup.string()
|
npwp: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
||||||
.required('Nomor NPWP wajib diisi!'),
|
.required('Nomor NPWP wajib diisi!'),
|
||||||
|
bank_name: Yup.string()
|
||||||
|
.min(3, 'Nama bank minimal 3 karakter!')
|
||||||
|
.required('Nama bank wajib diisi!'),
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const SupplierForm = ({
|
|||||||
email: initialValues?.email ?? '',
|
email: initialValues?.email ?? '',
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
npwp: initialValues?.npwp ?? '',
|
npwp: initialValues?.npwp ?? '',
|
||||||
|
bank_name: initialValues?.bank_name ?? '',
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
due_date: initialValues?.due_date ?? 1,
|
due_date: initialValues?.due_date ?? 1,
|
||||||
};
|
};
|
||||||
@@ -149,6 +150,7 @@ const SupplierForm = ({
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
npwp: values.npwp,
|
npwp: values.npwp,
|
||||||
|
bank_name: values.bank_name,
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
due_date: parseInt(values.due_date.toString()),
|
due_date: parseInt(values.due_date.toString()),
|
||||||
};
|
};
|
||||||
@@ -368,6 +370,22 @@ const SupplierForm = ({
|
|||||||
errorMessage={formik.errors.npwp}
|
errorMessage={formik.errors.npwp}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Nama Bank'
|
||||||
|
name='bank_name'
|
||||||
|
placeholder='Masukkan nama bank supplier'
|
||||||
|
value={formik.values.bank_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
||||||
|
}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.bank_name}
|
||||||
|
readOnly={formType === 'detail'}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import {
|
||||||
|
CellContext,
|
||||||
|
ColumnDef,
|
||||||
|
SortingState,
|
||||||
|
Updater,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -34,6 +39,8 @@ import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
|||||||
|
|
||||||
type PurchaseTableFilters = {
|
type PurchaseTableFilters = {
|
||||||
search: string;
|
search: string;
|
||||||
|
sort_by: string;
|
||||||
|
order_by: string;
|
||||||
po_date: string;
|
po_date: string;
|
||||||
approval_status: string;
|
approval_status: string;
|
||||||
product_category_id: string;
|
product_category_id: string;
|
||||||
@@ -157,18 +164,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PurchaseTable = () => {
|
const PurchaseTable = () => {
|
||||||
// ===== STATE MANAGEMENT =====
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
|
||||||
useState(false);
|
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
|
||||||
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
|
||||||
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
|
||||||
|
|
||||||
// ===== TABLE FILTER STATE =====
|
// ===== TABLE FILTER STATE =====
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
@@ -180,6 +175,8 @@ const PurchaseTable = () => {
|
|||||||
} = useTableFilter<PurchaseTableFilters>({
|
} = useTableFilter<PurchaseTableFilters>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
|
sort_by: '',
|
||||||
|
order_by: '',
|
||||||
po_date: '',
|
po_date: '',
|
||||||
approval_status: '',
|
approval_status: '',
|
||||||
product_category_id: '',
|
product_category_id: '',
|
||||||
@@ -198,6 +195,8 @@ const PurchaseTable = () => {
|
|||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
sort_by: 'sort_by',
|
||||||
|
order_by: 'sort_order',
|
||||||
po_date: 'po_date',
|
po_date: 'po_date',
|
||||||
approval_status: 'approval_status',
|
approval_status: 'approval_status',
|
||||||
product_category_id: 'product_category_id',
|
product_category_id: 'product_category_id',
|
||||||
@@ -219,6 +218,36 @@ const PurchaseTable = () => {
|
|||||||
storeName: 'purchase-table',
|
storeName: 'purchase-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== STATE MANAGEMENT =====
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
|
useState(false);
|
||||||
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
|
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
||||||
|
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
||||||
|
const sorting: SortingState = tableFilterState.sort_by
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: tableFilterState.sort_by,
|
||||||
|
desc: tableFilterState.order_by === '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('order_by', next[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('order_by', '', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ===== MODAL HOOKS =====
|
// ===== MODAL HOOKS =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
@@ -237,6 +266,7 @@ const PurchaseTable = () => {
|
|||||||
// ===== TABLE COLUMNS DEFINITION =====
|
// ===== TABLE COLUMNS DEFINITION =====
|
||||||
const purchaseColumns: ColumnDef<Purchase>[] = [
|
const purchaseColumns: ColumnDef<Purchase>[] = [
|
||||||
{
|
{
|
||||||
|
accessorKey: 'po_number',
|
||||||
header: 'No. PR/PO',
|
header: 'No. PR/PO',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const { pr_number, po_number } = props.row.original;
|
const { pr_number, po_number } = props.row.original;
|
||||||
@@ -278,7 +308,7 @@ const PurchaseTable = () => {
|
|||||||
cell: (props) => props.row.original.requester_name || '-',
|
cell: (props) => props.row.original.requester_name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'products.name',
|
accessorKey: 'products',
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const products = props.row.original.products;
|
const products = props.row.original.products;
|
||||||
@@ -293,7 +323,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'location.name',
|
accessorKey: 'location',
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
cell: (props) => props.row.original.location?.name || '-',
|
cell: (props) => props.row.original.location?.name || '-',
|
||||||
},
|
},
|
||||||
@@ -323,6 +353,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aging',
|
header: 'Aging',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const purchase = props.row.original;
|
const purchase = props.row.original;
|
||||||
if (!purchase.po_date) return '-';
|
if (!purchase.po_date) return '-';
|
||||||
@@ -334,6 +365,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
header: 'Status Approval',
|
header: 'Status Approval',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -378,6 +410,14 @@ const PurchaseTable = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.created_at
|
||||||
|
? formatDate(props.row.original.created_at, 'DD MMM YYYY')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -658,6 +698,7 @@ const PurchaseTable = () => {
|
|||||||
'search',
|
'search',
|
||||||
'filter_by',
|
'filter_by',
|
||||||
'sort_by',
|
'sort_by',
|
||||||
|
'order_by',
|
||||||
'product_category_name',
|
'product_category_name',
|
||||||
'supplier_name',
|
'supplier_name',
|
||||||
'area_name',
|
'area_name',
|
||||||
@@ -771,7 +812,8 @@ const PurchaseTable = () => {
|
|||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={setSorting}
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
headerColumnClassName: 'text-nowrap',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
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 { httpClient } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
@@ -73,6 +73,25 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
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 handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
@@ -252,6 +271,8 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
if (filterParams.category) {
|
if (filterParams.category) {
|
||||||
params.append('category', 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]) => {
|
Object.entries(extraParams ?? {}).forEach(([key, value]) => {
|
||||||
params.set(key, value);
|
params.set(key, value);
|
||||||
@@ -259,7 +280,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
|
|
||||||
return params.toString();
|
return params.toString();
|
||||||
},
|
},
|
||||||
[filterParams]
|
[filterParams, sortBy, orderBy]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
@@ -443,19 +464,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: 'No',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. PO',
|
header: 'No. PO',
|
||||||
accessorKey: 'po_number',
|
accessorKey: 'po_number',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. Referensi',
|
header: 'No. Referensi',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Realisasi',
|
header: 'Tanggal Realisasi',
|
||||||
accessorKey: 'realization_date',
|
accessorKey: 'realization_date',
|
||||||
|
enableSorting: true,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -463,6 +488,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Tanggal Transaksi',
|
header: 'Tanggal Transaksi',
|
||||||
accessorKey: 'transaction_date',
|
accessorKey: 'transaction_date',
|
||||||
|
enableSorting: true,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -470,21 +496,30 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
|
accessorKey: 'product',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Supplier',
|
header: 'Supplier',
|
||||||
|
accessorKey: 'supplier',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.supplier?.name,
|
accessorFn: (row) => row.supplier?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
|
accessorKey: 'location',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.kandang?.location?.name,
|
accessorFn: (row) => row.kandang?.location?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Kandang',
|
header: 'Kandang',
|
||||||
|
accessorKey: 'kandang',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.kandang?.name,
|
accessorFn: (row) => row.kandang?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -492,23 +527,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
id: 'qty_pengajuan',
|
accessorKey: 'qty_pengajuan',
|
||||||
accessorFn: (row) => row.pengajuan?.qty,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
id: 'harga_pengajuan',
|
accessorKey: 'price_pengajuan',
|
||||||
accessorFn: (row) => row.pengajuan?.price,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.pengajuan?.price || 0),
|
formatCurrency(row.original.pengajuan?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
id: 'total_pengajuan',
|
accessorKey: 'total_pengajuan',
|
||||||
accessorFn: (row) =>
|
|
||||||
(row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0),
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.pengajuan?.qty || 0) *
|
(row.original.pengajuan?.qty || 0) *
|
||||||
@@ -523,23 +554,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
id: 'qty_realisasi',
|
accessorKey: 'qty_realisasi',
|
||||||
accessorFn: (row) => row.realisasi?.qty,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
id: 'harga_realisasi',
|
accessorKey: 'price_realisasi',
|
||||||
accessorFn: (row) => row.realisasi?.price,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.realisasi?.price || 0),
|
formatCurrency(row.original.realisasi?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
id: 'total_realisasi',
|
accessorKey: 'total_realisasi',
|
||||||
accessorFn: (row) =>
|
|
||||||
(row.realisasi?.qty || 0) * (row.realisasi?.price || 0),
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.realisasi?.qty || 0) *
|
(row.original.realisasi?.qty || 0) *
|
||||||
@@ -550,6 +577,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'realization_status',
|
||||||
header: 'Status Pencairan',
|
header: 'Status Pencairan',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RealizationStatusBadge
|
<RealizationStatusBadge
|
||||||
@@ -558,6 +586,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'bop_status',
|
||||||
header: 'Status BOP',
|
header: 'Status BOP',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
||||||
@@ -602,6 +631,9 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
totalItems={meta?.total_results || 0}
|
totalItems={meta?.total_results || 0}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-0!',
|
containerClassName: 'w-full mb-0!',
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface DatePickerProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
formatDisplay?: (date: string) => string;
|
formatDisplay?: (date: string) => string;
|
||||||
|
hasError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DatePicker({
|
export function DatePicker({
|
||||||
@@ -28,6 +29,7 @@ export function DatePicker({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
placeholder = 'Select date',
|
placeholder = 'Select date',
|
||||||
formatDisplay,
|
formatDisplay,
|
||||||
|
hasError = false,
|
||||||
}: DatePickerProps) {
|
}: DatePickerProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [currentMonth, setCurrentMonth] = useState(() => {
|
const [currentMonth, setCurrentMonth] = useState(() => {
|
||||||
@@ -154,7 +156,7 @@ export function DatePicker({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
disabled={disabled}
|
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' />
|
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
||||||
{date ? (
|
{date ? (
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ export function DailyChecklistContent() {
|
|||||||
const [initialLoading, setInitialLoading] = useState(!!checklistIdFromUrl);
|
const [initialLoading, setInitialLoading] = useState(!!checklistIdFromUrl);
|
||||||
|
|
||||||
const [emptyKandangEndDate, setEmptyKandangEndDate] = useState<string>('');
|
const [emptyKandangEndDate, setEmptyKandangEndDate] = useState<string>('');
|
||||||
|
const [emptyKandangEndDateError, setEmptyKandangEndDateError] =
|
||||||
|
useState<string>('');
|
||||||
|
|
||||||
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
|
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
|
||||||
const [documents, setDocuments] = useState<File[]>([]);
|
const [documents, setDocuments] = useState<File[]>([]);
|
||||||
@@ -233,8 +235,12 @@ export function DailyChecklistContent() {
|
|||||||
setEmptyKandang(isEmptyKandang);
|
setEmptyKandang(isEmptyKandang);
|
||||||
setSelectedCategory(isEmptyKandang ? 'empty_kandang' : data.category);
|
setSelectedCategory(isEmptyKandang ? 'empty_kandang' : data.category);
|
||||||
|
|
||||||
if (isEmptyKandang && data.empty_kandang_end_date) {
|
if (
|
||||||
const rawEnd = data.empty_kandang_end_date;
|
isEmptyKandang &&
|
||||||
|
data.empty_kandang &&
|
||||||
|
data.empty_kandang.end_date
|
||||||
|
) {
|
||||||
|
const rawEnd = data.empty_kandang.end_date;
|
||||||
setEmptyKandangEndDate(
|
setEmptyKandangEndDate(
|
||||||
rawEnd.length > 10 ? rawEnd.slice(0, 10) : rawEnd
|
rawEnd.length > 10 ? rawEnd.slice(0, 10) : rawEnd
|
||||||
);
|
);
|
||||||
@@ -784,6 +790,11 @@ export function DailyChecklistContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emptyKandang && !emptyKandangEndDate) {
|
||||||
|
setEmptyKandangEndDateError('Tanggal akhir kandang kosong wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoadingDraft(true);
|
setIsLoadingDraft(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -861,6 +872,11 @@ export function DailyChecklistContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emptyKandang && !emptyKandangEndDate) {
|
||||||
|
setEmptyKandangEndDateError('Tanggal akhir kandang kosong wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isKandangEmpty) {
|
if (!isKandangEmpty) {
|
||||||
if (selectedEmployees.length === 0) {
|
if (selectedEmployees.length === 0) {
|
||||||
toast.error('Pilih minimal 1 ABK');
|
toast.error('Pilih minimal 1 ABK');
|
||||||
@@ -1220,11 +1236,20 @@ export function DailyChecklistContent() {
|
|||||||
<div className='mt-1.5'>
|
<div className='mt-1.5'>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
date={emptyKandangEndDate}
|
date={emptyKandangEndDate}
|
||||||
onDateChange={setEmptyKandangEndDate}
|
onDateChange={(val) => {
|
||||||
|
setEmptyKandangEndDate(val);
|
||||||
|
if (val) setEmptyKandangEndDateError('');
|
||||||
|
}}
|
||||||
disabled={!isChecklistStatusDraft}
|
disabled={!isChecklistStatusDraft}
|
||||||
placeholder='Pilih tanggal'
|
placeholder='Pilih tanggal'
|
||||||
formatDisplay={formatDateForDisplay}
|
formatDisplay={formatDateForDisplay}
|
||||||
|
hasError={!!emptyKandangEndDateError}
|
||||||
/>
|
/>
|
||||||
|
{emptyKandangEndDateError && (
|
||||||
|
<p className='text-xs text-red-500 mt-1'>
|
||||||
|
{emptyKandangEndDateError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+19
@@ -60,6 +60,7 @@ interface ChecklistHeader {
|
|||||||
progress_percent: number;
|
progress_percent: number;
|
||||||
total_phases: number;
|
total_phases: number;
|
||||||
total_activities: number;
|
total_activities: number;
|
||||||
|
empty_kandang_end_date?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PhaseGroup {
|
interface PhaseGroup {
|
||||||
@@ -179,6 +180,9 @@ export function DetailDailyChecklistContent() {
|
|||||||
|
|
||||||
setDocuments(rawDetailChecklist?.document_urls || []);
|
setDocuments(rawDetailChecklist?.document_urls || []);
|
||||||
|
|
||||||
|
const emptyKandangEndDate =
|
||||||
|
rawDetailChecklist?.empty_kandang?.end_date ?? null;
|
||||||
|
|
||||||
const checklistData = {
|
const checklistData = {
|
||||||
id: rawDetailChecklist?.id,
|
id: rawDetailChecklist?.id,
|
||||||
date: rawDetailChecklist?.date,
|
date: rawDetailChecklist?.date,
|
||||||
@@ -205,6 +209,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: 0,
|
total_phases: 0,
|
||||||
total_activities: 0,
|
total_activities: 0,
|
||||||
|
empty_kandang_end_date: emptyKandangEndDate,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -272,6 +277,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
||||||
total_activities: tasks.length,
|
total_activities: tasks.length,
|
||||||
|
empty_kandang_end_date: emptyKandangEndDate,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -322,6 +328,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: progressPercent,
|
progress_percent: progressPercent,
|
||||||
total_phases: uniquePhases.size,
|
total_phases: uniquePhases.size,
|
||||||
total_activities: uniqueActivities.size,
|
total_activities: uniqueActivities.size,
|
||||||
|
empty_kandang_end_date: emptyKandangEndDate,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching checklist detail:', error);
|
console.error('Error fetching checklist detail:', error);
|
||||||
@@ -777,6 +784,18 @@ export function DetailDailyChecklistContent() {
|
|||||||
{CATEGORY_LABELS[header.category] || header.category}
|
{CATEGORY_LABELS[header.category] || header.category}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{header.category === 'empty_kandang' && (
|
||||||
|
<div>
|
||||||
|
<Label className='text-xs text-gray-500'>
|
||||||
|
Tanggal Selesai Kandang Kosong
|
||||||
|
</Label>
|
||||||
|
<p className='text-sm font-medium text-gray-900 mt-1'>
|
||||||
|
{header.empty_kandang_end_date
|
||||||
|
? formatDate(header.empty_kandang_end_date)
|
||||||
|
: '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Label className='text-xs text-gray-500'>Status</Label>
|
<Label className='text-xs text-gray-500'>Status</Label>
|
||||||
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
||||||
|
|||||||
+5
-2
@@ -12,8 +12,11 @@ export type BaseDailyChecklist = {
|
|||||||
status: string;
|
status: string;
|
||||||
category: string;
|
category: string;
|
||||||
date: string;
|
date: string;
|
||||||
empty_kandang?: boolean;
|
empty_kandang?: {
|
||||||
empty_kandang_end_date?: string | null;
|
id: boolean;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
};
|
||||||
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
|
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
|
||||||
total_phase: number;
|
total_phase: number;
|
||||||
total_activity: number;
|
total_activity: number;
|
||||||
|
|||||||
+2
@@ -10,6 +10,7 @@ export type BaseCustomer = {
|
|||||||
phone: string;
|
phone: string;
|
||||||
email: string;
|
email: string;
|
||||||
account_number: string;
|
account_number: string;
|
||||||
|
bank_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Customer = BaseMetadata & BaseCustomer;
|
export type Customer = BaseMetadata & BaseCustomer;
|
||||||
@@ -22,6 +23,7 @@ export type CreateCustomerPayload = {
|
|||||||
phone: string;
|
phone: string;
|
||||||
email: string;
|
email: string;
|
||||||
account_number: string;
|
account_number: string;
|
||||||
|
bank_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateCustomerPayload = CreateCustomerPayload;
|
export type UpdateCustomerPayload = CreateCustomerPayload;
|
||||||
|
|||||||
+2
@@ -16,6 +16,7 @@ export type BaseSupplier = {
|
|||||||
account_number: string;
|
account_number: string;
|
||||||
due_date: number;
|
due_date: number;
|
||||||
balance?: number;
|
balance?: number;
|
||||||
|
bank_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Supplier = BaseMetadata & BaseSupplier;
|
export type Supplier = BaseMetadata & BaseSupplier;
|
||||||
@@ -45,6 +46,7 @@ export type CreateSupplierPayload = {
|
|||||||
account_number: string;
|
account_number: string;
|
||||||
due_date: number;
|
due_date: number;
|
||||||
balance?: number;
|
balance?: number;
|
||||||
|
bank_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateSupplierPayload = CreateSupplierPayload;
|
export type UpdateSupplierPayload = CreateSupplierPayload;
|
||||||
|
|||||||
Reference in New Issue
Block a user