mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bf5f36a77 | |||
| 989e30fbed | |||
| 40139cd636 | |||
| 8c03f10043 | |||
| 89a6e51b48 | |||
| f6727dc4dc | |||
| 1284b22345 | |||
| f73ea182ae | |||
| 047266b6d8 | |||
| 6b95edfb72 | |||
| 4b62b02a13 | |||
| 12a50c6100 | |||
| 09537d84d0 | |||
| 1aa2ca9b31 | |||
| c87107b4ee | |||
| 55b13988bf | |||
| 19033278b3 | |||
| 4a6ac8a57d | |||
| 2b9847e1a9 | |||
| 167769a711 | |||
| 417dbba458 |
@@ -48,6 +48,3 @@ next-env.d.ts
|
|||||||
|
|
||||||
# rtk
|
# rtk
|
||||||
rtk.exe
|
rtk.exe
|
||||||
|
|
||||||
# local specs
|
|
||||||
/local-specs
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# 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"
|
|
||||||
@@ -80,124 +80,76 @@ Data tables across all modules (master-data, inventory, finance, purchase, etc.)
|
|||||||
- Apply to: search handlers, filter form submissions, reset handlers
|
- Apply to: search handlers, filter form submissions, reset handlers
|
||||||
|
|
||||||
3. **Create custom formikResetHandler function**
|
3. **Create custom formikResetHandler function**
|
||||||
- Call `resetFilter()` (single call — resets all `useTableFilter` state to defaults)
|
- Clear each filter with `updateFilter(fieldName, defaultValue, true)`
|
||||||
- Reset any local error state (e.g. `setHasDateError(false)`, dismiss toasts)
|
- Call `formik.resetForm({ values: { ...defaults } })`
|
||||||
- Call `formik.resetForm({ values: { ...defaults } })` to sync formik to defaults
|
- Close the modal at the end
|
||||||
- Call `filterModal.closeModal()` at the end
|
- Attach to both button `onClick` and form `onReset` handler
|
||||||
- Attach to form `onReset` handler (not `formik.handleReset`)
|
|
||||||
|
|
||||||
```tsx
|
**Optimization: Avoid useCallback for simple handlers**
|
||||||
const formikResetHandler = () => {
|
|
||||||
resetFilter();
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); }
|
|
||||||
formik.resetForm({ values: { start_date: '', end_date: '', customers: [], filterBy: undefined } });
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
// ...
|
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Optimization: Avoid useCallback and useMemo for trivial operations**
|
- `useCallback` adds overhead and is only useful for complex logic or memoized child components
|
||||||
|
- Simple pass-through handlers don't need it:
|
||||||
- `useCallback` and `useMemo` add overhead; only use them when the computation is expensive or the result is passed to a memoized child
|
|
||||||
- Simple derivations and pass-through handlers don't need them:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// ✅ Good: plain derivation
|
// ✅ Good: Simple handler without useCallback
|
||||||
const data = isResponseSuccess(response) ? (response.data ?? []) : [];
|
const handleFilterChange = (val) => setFieldValue('location', val);
|
||||||
const meta =
|
|
||||||
isResponseSuccess(response) && response.meta ? response.meta : null;
|
|
||||||
|
|
||||||
// ❌ Avoid: useMemo for trivial conditional access
|
// ❌ Avoid: Unnecessary useCallback overhead
|
||||||
const data = useMemo(
|
const handleFilterChange = useCallback(
|
||||||
() => (isResponseSuccess(response) ? (response.data ?? []) : []),
|
|
||||||
[response]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ✅ Good: simple handler
|
|
||||||
const handleChange = (val) => setFieldValue('location', val);
|
|
||||||
|
|
||||||
// ❌ Avoid: unnecessary useCallback
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(val) => setFieldValue('location', val),
|
(val) => setFieldValue('location', val),
|
||||||
[setFieldValue]
|
[setFieldValue]
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
- `useMemo` IS justified for large column definition arrays (TanStack Table re-processes on every render)
|
|
||||||
|
|
||||||
**Best practice: Store OptionType objects directly, not IDs**
|
**Best practice: Store OptionType objects directly, not IDs**
|
||||||
|
|
||||||
For select inputs, store the complete `OptionType` object (or `OptionType[]` for multi-select) in both formik state and tableFilterState. `useTableFilter`'s `serializeValue` handles serialization automatically:
|
For select inputs, store the complete `OptionType` object in both formik state and tableFilterState. This eliminates the need for computed helper values (like searching options arrays to find the matching object).
|
||||||
|
|
||||||
- `OptionType<T>` → serialized as `String(value)` in the query string
|
|
||||||
- `OptionType<T>[]` → serialized as comma-separated values (CSV) — ideal for multi-select API params like `customer_ids`, `sales_ids`
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
// Type the useTableFilter with the filter state structure
|
||||||
const { state: tableFilterState, updateFilter, ... } = useTableFilter<{
|
const { state: tableFilterState, updateFilter, ... } = useTableFilter<{
|
||||||
search: string;
|
search: string;
|
||||||
customers: OptionType<number>[]; // multi-select → serializes as CSV
|
locationFilter?: OptionType<string>;
|
||||||
location?: OptionType<string>; // single-select → serializes as value string
|
picFilter?: OptionType<string>;
|
||||||
filterBy?: OptionType<string>; // single-select radio
|
|
||||||
}>({
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
customers: [],
|
locationFilter: undefined,
|
||||||
location: undefined,
|
picFilter: undefined
|
||||||
filterBy: undefined,
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
customers: 'customer_ids', // serializes OptionType[] → "1,2,3"
|
locationFilter: 'location_id',
|
||||||
location: 'location_id', // serializes OptionType → "abc"
|
picFilter: 'pic_id',
|
||||||
filterBy: 'filter_by',
|
|
||||||
},
|
},
|
||||||
persist: true,
|
persist: true,
|
||||||
storeName: 'my-table',
|
storeName: 'kandangs-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize formik directly from tableFilterState (no hardcoded defaults)
|
// Initialize formik with tableFilterState values (now typed OptionType objects)
|
||||||
const formik = useFormik({
|
const formik = useFormik<KandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
customers: tableFilterState.customers,
|
location: tableFilterState.locationFilter,
|
||||||
location: tableFilterState.location,
|
pic: tableFilterState.picFilter,
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use formik values directly — no computed helpers needed
|
// Handlers store the complete OptionType, not just the ID
|
||||||
<SelectInputCheckbox value={formik.values.customers} onChange={(val) => formik.setFieldValue('customers', Array.isArray(val) ? val : [])} />
|
const handleFilterLocationChange = useCallback(
|
||||||
<SelectInput value={formik.values.location} onChange={(val) => formik.setFieldValue('location', val)} />
|
(val) => setFieldValue('location', val),
|
||||||
<SelectInputRadio value={formik.values.filterBy ?? null} onChange={(val) => formik.setFieldValue('filterBy', !Array.isArray(val) ? (val ?? undefined) : undefined)} />
|
[setFieldValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use formik values directly in select inputs (no computed helpers needed)
|
||||||
|
<SelectInput
|
||||||
|
value={formik.values.location}
|
||||||
|
onChange={handleFilterLocationChange}
|
||||||
|
...
|
||||||
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Filter field naming convention**
|
|
||||||
|
|
||||||
- Multi-select fields: use plural entity name — `customers`, `salesPersons`, `locations`
|
|
||||||
- Single-select fields: use descriptive camelCase — `filterBy`, `status`, `category`
|
|
||||||
- No `Filter` suffix (e.g. avoid `customerFilter`, `locationFilter`)
|
|
||||||
|
|
||||||
**Filter modal: pass `openModal` directly, never use `enableReinitialize`**
|
|
||||||
|
|
||||||
`enableReinitialize: true` resets formik mid-interaction whenever `tableFilterState` changes, breaking the modal UX. Pass `filterModal.openModal` directly to the button — no ref wrapper needed. Formik retains its last state across open/close, which is acceptable UX (values sync with `tableFilterState` on submit and reset anyway).
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// ❌ Avoid: enableReinitialize breaks modal mid-interaction
|
|
||||||
const formik = useFormik({ initialValues: { ... }, enableReinitialize: true });
|
|
||||||
|
|
||||||
// ❌ Avoid: unnecessary ref indirection
|
|
||||||
const handleFilterModalOpenRef = useRef(() => {});
|
|
||||||
handleFilterModalOpenRef.current = () => { formik.setValues({...}); filterModal.openModal(); };
|
|
||||||
|
|
||||||
// ✅ Correct: pass openModal directly
|
|
||||||
<ButtonFilter onClick={filterModal.openModal} ... />
|
|
||||||
```
|
|
||||||
|
|
||||||
Include `filterModal.openModal` in the `useEffect` deps array when it's used inside the effect.
|
|
||||||
|
|
||||||
**Apply this pattern to:**
|
**Apply this pattern to:**
|
||||||
|
|
||||||
- Any data table component across any module that needs persistent filters
|
- Any data table component across any module that needs persistent filters
|
||||||
@@ -207,31 +159,7 @@ Include `filterModal.openModal` in the `useEffect` deps array when it's used ins
|
|||||||
**Reference implementations:**
|
**Reference implementations:**
|
||||||
|
|
||||||
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
||||||
- `BalanceMonitoringTab` in `src/components/pages/report/finance/tab/` — multi-select + radio + date range
|
- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.)
|
||||||
|
|
||||||
## SWR fetch pattern
|
|
||||||
|
|
||||||
Use `FinanceApi.getAllFetcher` (or the relevant service's `getAllFetcher`) when the result type matches the service generic `T`. When it differs, use `httpClientFetcher` with an explicit type:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// ✅ Same type as service generic — use getAllFetcher
|
|
||||||
const { data } = useSWR(
|
|
||||||
`${Api.basePath}${getTableFilterQueryString()}`,
|
|
||||||
Api.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// ✅ Different type — use httpClientFetcher with explicit useSWR type
|
|
||||||
const { data } = useSWR<
|
|
||||||
BaseApiResponse<BalanceMonitoringRow[]>,
|
|
||||||
AxiosError<BaseApiResponse>,
|
|
||||||
SWRHttpKey
|
|
||||||
>(
|
|
||||||
`${FinanceApi.basePath}/balance-monitoring${getTableFilterQueryString()}`,
|
|
||||||
httpClientFetcher
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Always name the `toQueryString` alias `getTableFilterQueryString` when destructuring from `useTableFilter`.
|
|
||||||
|
|
||||||
## Server-side sorting pattern
|
## Server-side sorting pattern
|
||||||
|
|
||||||
@@ -332,155 +260,3 @@ 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 -->
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -173,7 +173,6 @@ 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(),
|
||||||
|
|||||||
+11
-14
@@ -6,7 +6,6 @@ export interface TabItem {
|
|||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
content?: ReactNode;
|
content?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hide?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsProps
|
export interface TabsProps
|
||||||
@@ -123,19 +122,17 @@ const Tabs = ({
|
|||||||
>
|
>
|
||||||
<div className={getSideContentClasses()}>
|
<div className={getSideContentClasses()}>
|
||||||
<div role='tablist' className={getTabsClasses()}>
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
{tabs.map(({ id, label, disabled, hide }) =>
|
{tabs.map(({ id, label, disabled }) => (
|
||||||
hide ? null : (
|
<button
|
||||||
<button
|
key={id}
|
||||||
key={id}
|
role='tab'
|
||||||
role='tab'
|
className={getTabClasses(id === activeTabId, disabled)}
|
||||||
className={getTabClasses(id === activeTabId, disabled)}
|
onClick={() => !disabled && handleTabChange(id)}
|
||||||
onClick={() => !disabled && handleTabChange(id)}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
>
|
||||||
>
|
{label}
|
||||||
{label}
|
</button>
|
||||||
</button>
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{sideContent && sideContent}
|
{sideContent && sideContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ 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,13 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import {
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -42,8 +47,7 @@ import { BaseApiResponse } from '@/types/api/api-general';
|
|||||||
|
|
||||||
type ExpenseTableFilters = {
|
type ExpenseTableFilters = {
|
||||||
search: string;
|
search: string;
|
||||||
sort_by: string;
|
nameSort: string;
|
||||||
order_by: string;
|
|
||||||
transactionDate: string;
|
transactionDate: string;
|
||||||
realizationDate: string;
|
realizationDate: string;
|
||||||
locationId: string;
|
locationId: string;
|
||||||
@@ -238,8 +242,7 @@ const ExpensesTable = () => {
|
|||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
search: '',
|
search: '',
|
||||||
sort_by: '',
|
nameSort: '',
|
||||||
order_by: '',
|
|
||||||
transactionDate: '',
|
transactionDate: '',
|
||||||
realizationDate: '',
|
realizationDate: '',
|
||||||
locationId: '',
|
locationId: '',
|
||||||
@@ -258,8 +261,7 @@ const ExpensesTable = () => {
|
|||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
sort_by: 'sort_by',
|
nameSort: 'sort_name',
|
||||||
order_by: 'sort_order',
|
|
||||||
transactionDate: 'transaction_date',
|
transactionDate: 'transaction_date',
|
||||||
realizationDate: 'realization_date',
|
realizationDate: 'realization_date',
|
||||||
locationId: 'location_id',
|
locationId: 'location_id',
|
||||||
@@ -317,26 +319,7 @@ const ExpensesTable = () => {
|
|||||||
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
||||||
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
||||||
|
|
||||||
const sorting: SortingState = tableFilterState.sort_by
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
? [
|
|
||||||
{
|
|
||||||
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)
|
||||||
@@ -454,12 +437,10 @@ 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',
|
||||||
},
|
},
|
||||||
@@ -473,20 +454,17 @@ 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 (
|
||||||
@@ -500,14 +478,6 @@ 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) => {
|
||||||
@@ -912,6 +882,17 @@ 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'>
|
||||||
@@ -1070,8 +1051,7 @@ const ExpensesTable = () => {
|
|||||||
'page',
|
'page',
|
||||||
'pageSize',
|
'pageSize',
|
||||||
'search',
|
'search',
|
||||||
'sort_by',
|
'nameSort',
|
||||||
'order_by',
|
|
||||||
'userId',
|
'userId',
|
||||||
'locationName',
|
'locationName',
|
||||||
'vendorName',
|
'vendorName',
|
||||||
@@ -1172,8 +1152,7 @@ const ExpensesTable = () => {
|
|||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={handleSortingChange}
|
setSorting={setSorting}
|
||||||
manualSorting
|
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
enableRowSelection={tableEnableRowSelectionHandler}
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
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';
|
||||||
@@ -188,8 +183,7 @@ const FinanceTable = () => {
|
|||||||
bankIds: '',
|
bankIds: '',
|
||||||
customerIds: '',
|
customerIds: '',
|
||||||
supplierIds: '',
|
supplierIds: '',
|
||||||
sort_by: '',
|
sortBy: '',
|
||||||
orderBy: '',
|
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
bankNames: '',
|
bankNames: '',
|
||||||
@@ -203,8 +197,7 @@ const FinanceTable = () => {
|
|||||||
bankIds: 'bank_ids',
|
bankIds: 'bank_ids',
|
||||||
customerIds: 'customer_ids',
|
customerIds: 'customer_ids',
|
||||||
supplierIds: 'supplier_ids',
|
supplierIds: 'supplier_ids',
|
||||||
sort_by: 'sort_by',
|
sortBy: 'sort_date',
|
||||||
orderBy: 'sort_order',
|
|
||||||
startDate: 'start_date',
|
startDate: 'start_date',
|
||||||
endDate: 'end_date',
|
endDate: 'end_date',
|
||||||
},
|
},
|
||||||
@@ -255,7 +248,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('sort_by', values.sort_by, true);
|
updateFilter('sortBy', 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
|
||||||
@@ -283,8 +276,7 @@ const FinanceTable = () => {
|
|||||||
updateFilter('bankIds', '', true);
|
updateFilter('bankIds', '', true);
|
||||||
updateFilter('customerIds', '', true);
|
updateFilter('customerIds', '', true);
|
||||||
updateFilter('supplierIds', '', true);
|
updateFilter('supplierIds', '', true);
|
||||||
updateFilter('sort_by', '', true);
|
updateFilter('sortBy', '', true);
|
||||||
updateFilter('orderBy', '', true);
|
|
||||||
updateFilter('startDate', '', true);
|
updateFilter('startDate', '', true);
|
||||||
updateFilter('endDate', '', true);
|
updateFilter('endDate', '', true);
|
||||||
updateFilter('bankNames', '', true);
|
updateFilter('bankNames', '', true);
|
||||||
@@ -402,26 +394,6 @@ 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;
|
||||||
@@ -533,7 +505,7 @@ const FinanceTable = () => {
|
|||||||
// Restore sort by
|
// Restore sort by
|
||||||
const restoredSortBy =
|
const restoredSortBy =
|
||||||
sortByOptions.find(
|
sortByOptions.find(
|
||||||
(opt) => String(opt.value) === tableFilterState.sort_by
|
(opt) => String(opt.value) === tableFilterState.sortBy
|
||||||
) || null;
|
) || null;
|
||||||
setSelectedSortBy(restoredSortBy);
|
setSelectedSortBy(restoredSortBy);
|
||||||
|
|
||||||
@@ -544,7 +516,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.sort_by || '',
|
sort_by: tableFilterState.sortBy || '',
|
||||||
start_date: tableFilterState.startDate || '',
|
start_date: tableFilterState.startDate || '',
|
||||||
end_date: tableFilterState.endDate || '',
|
end_date: tableFilterState.endDate || '',
|
||||||
});
|
});
|
||||||
@@ -568,12 +540,10 @@ 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>;
|
||||||
@@ -582,7 +552,6 @@ 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('_')
|
||||||
@@ -592,8 +561,7 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorKey: 'customer_name',
|
accessorFn: (finance: Finance) => finance.party?.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>;
|
||||||
@@ -603,22 +571,16 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Pembayaran',
|
header: 'Tanggal Pembayaran',
|
||||||
accessorKey: 'payment_date',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
||||||
cell: (props) =>
|
|
||||||
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Dibuat',
|
header: 'Tanggal Dibuat',
|
||||||
accessorKey: 'created_at',
|
accessorFn: (finance) => formatDate(finance.created_at, 'DD MMM YYYY'),
|
||||||
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>;
|
||||||
@@ -626,26 +588,20 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorKey: 'bank',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
finance.bank
|
||||||
cell: (props) =>
|
? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`
|
||||||
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)',
|
||||||
accessorKey: 'expense_amount',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
formatCurrency(Math.abs(finance.expense_amount)),
|
||||||
cell: (props) =>
|
|
||||||
formatCurrency(Math.abs(props.row.original.expense_amount)),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
accessorKey: 'income_amount',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
formatCurrency(Math.abs(finance.income_amount)),
|
||||||
cell: (props) =>
|
|
||||||
formatCurrency(Math.abs(props.row.original.income_amount)),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
@@ -751,7 +707,6 @@ const FinanceTable = () => {
|
|||||||
'page',
|
'page',
|
||||||
'pageSize',
|
'pageSize',
|
||||||
'search',
|
'search',
|
||||||
'orderBy',
|
|
||||||
'bankNames',
|
'bankNames',
|
||||||
'customerNames',
|
'customerNames',
|
||||||
'supplierNames',
|
'supplierNames',
|
||||||
@@ -794,9 +749,6 @@ 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,11 +849,7 @@ 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}
|
||||||
>
|
>
|
||||||
{marketing?.data?.latest_approval?.step_number === 1 &&
|
Approve
|
||||||
'Approve'}
|
|
||||||
|
|
||||||
{marketing?.data?.latest_approval?.step_number === 2 &&
|
|
||||||
'Deliver Item'}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -297,8 +297,6 @@ 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);
|
||||||
@@ -454,33 +452,23 @@ const MarketingTable = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsApproveLoading(true);
|
const approveMarketingRes: BaseApiResponse<unknown> | undefined =
|
||||||
|
approveAction === 'APPROVED'
|
||||||
|
? await MarketingApi.bulkApprovals(
|
||||||
|
idsToProcess,
|
||||||
|
nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER',
|
||||||
|
'',
|
||||||
|
notes || `APPROVED marketing ${idsToProcess.join(', ')}`
|
||||||
|
)
|
||||||
|
: await SalesOrderApi.bulkApprovals(idsToProcess, approveAction, notes);
|
||||||
|
|
||||||
try {
|
if (isResponseSuccess(approveMarketingRes)) {
|
||||||
const approveMarketingRes: BaseApiResponse<unknown> | undefined =
|
confirmationModal.closeModal();
|
||||||
approveAction === 'APPROVED'
|
toast.success(approveMarketingRes?.message as string);
|
||||||
? await MarketingApi.bulkApprovals(
|
setRowSelection({});
|
||||||
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> = (
|
||||||
@@ -542,21 +530,13 @@ const MarketingTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||||
setIsDeliveryLoading(true);
|
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
||||||
try {
|
deliveryModal.closeModal();
|
||||||
const res = await SalesOrderApi.delivery(
|
toast.success(res?.message as string);
|
||||||
selectedItem?.id as number,
|
refreshMarketing?.();
|
||||||
notes
|
router.push(
|
||||||
);
|
`/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(
|
||||||
@@ -792,14 +772,6 @@ 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,
|
||||||
@@ -1040,13 +1012,11 @@ 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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1070,12 +1040,10 @@ 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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1135,7 +1103,6 @@ const MarketingTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
disabled={isSubmittingBulkDelivery}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
bulkDeliveryModal.closeModal();
|
bulkDeliveryModal.closeModal();
|
||||||
setBulkDeliveryDate('');
|
setBulkDeliveryDate('');
|
||||||
@@ -1148,7 +1115,6 @@ const MarketingTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
color='success'
|
color='success'
|
||||||
isLoading={isSubmittingBulkDelivery}
|
isLoading={isSubmittingBulkDelivery}
|
||||||
disabled={isSubmittingBulkDelivery}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
submitBulkDeliveryApprovalHandler(
|
submitBulkDeliveryApprovalHandler(
|
||||||
idsToProcess,
|
idsToProcess,
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ 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);
|
||||||
@@ -262,7 +261,11 @@ 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',
|
||||||
'Seluruh data pengiriman harus diisi lengkap!',
|
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
|
||||||
function (items) {
|
function (items) {
|
||||||
if (!items || items.length === 0) return false;
|
if (!items || items.length === 0) return false;
|
||||||
|
|
||||||
// VALIDASI: seluruh item harus valid full
|
// VALIDASI: minimal 1 item valid full
|
||||||
const itemSchema = DeliveryOrderProductSchema;
|
const itemSchema = DeliveryOrderProductSchema;
|
||||||
|
|
||||||
const hasValidItem = items.every((item) => {
|
const hasValidItem = items.some((item) => {
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
return itemSchema.isValidSync(item, { abortEarly: true });
|
return itemSchema.isValidSync(item, { abortEarly: true });
|
||||||
});
|
});
|
||||||
@@ -123,17 +123,8 @@ export const SalesProductToFieldValues = (
|
|||||||
total_price: product.total_price,
|
total_price: product.total_price,
|
||||||
marketing_type: product.marketing_type
|
marketing_type: product.marketing_type
|
||||||
? {
|
? {
|
||||||
value:
|
value: product.marketing_type,
|
||||||
product.marketing_type === 'AYAM' ||
|
label: formatTitleCase(product.marketing_type),
|
||||||
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
|
||||||
@@ -194,17 +185,8 @@ 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:
|
value: salesOrder?.marketing_type,
|
||||||
salesOrder?.marketing_type === 'AYAM' ||
|
label: formatTitleCase(salesOrder?.marketing_type),
|
||||||
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
|
||||||
|
|||||||
+16
-6
@@ -146,6 +146,15 @@ 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(() => {
|
||||||
@@ -431,8 +440,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
handleBlurField(currentInput);
|
handleBlurField(currentInput);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'uom',
|
'uom',
|
||||||
initialValues?.marketing_product?.product_warehouse_data?.product?.uom
|
isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
|
||||||
?.name ?? ''
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -805,8 +813,9 @@ 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'>
|
||||||
{initialValues?.marketing_product?.product_warehouse_data
|
{isResponseSuccess(productData)
|
||||||
?.product?.uom?.name ?? ''}
|
? productData?.data?.uom.name
|
||||||
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -817,8 +826,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty +
|
)?.qty +
|
||||||
' ' +
|
' ' +
|
||||||
(initialValues?.marketing_product?.product_warehouse_data
|
(isResponseSuccess(productData)
|
||||||
?.product?.uom?.name ?? '')
|
? productData?.data?.uom.name
|
||||||
|
: '')
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -252,11 +252,6 @@ 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,11 +189,6 @@ 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,9 +27,6 @@ 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,7 +142,6 @@ 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]);
|
||||||
@@ -165,7 +164,6 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,22 +286,6 @@ 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,11 +326,6 @@ 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,9 +31,6 @@ 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,7 +122,6 @@ 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,
|
||||||
};
|
};
|
||||||
@@ -150,7 +149,6 @@ 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()),
|
||||||
};
|
};
|
||||||
@@ -370,22 +368,6 @@ 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,12 +2,7 @@
|
|||||||
|
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
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';
|
||||||
@@ -39,8 +34,6 @@ 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;
|
||||||
@@ -164,6 +157,18 @@ 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,
|
||||||
@@ -175,8 +180,6 @@ 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: '',
|
||||||
@@ -195,8 +198,6 @@ 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',
|
||||||
@@ -218,36 +219,6 @@ 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();
|
||||||
@@ -266,7 +237,6 @@ 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;
|
||||||
@@ -308,7 +278,7 @@ const PurchaseTable = () => {
|
|||||||
cell: (props) => props.row.original.requester_name || '-',
|
cell: (props) => props.row.original.requester_name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'products',
|
accessorKey: 'products.name',
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const products = props.row.original.products;
|
const products = props.row.original.products;
|
||||||
@@ -323,7 +293,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'location',
|
accessorKey: 'location.name',
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
cell: (props) => props.row.original.location?.name || '-',
|
cell: (props) => props.row.original.location?.name || '-',
|
||||||
},
|
},
|
||||||
@@ -353,7 +323,6 @@ 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 '-';
|
||||||
@@ -365,7 +334,6 @@ 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;
|
||||||
@@ -410,14 +378,6 @@ 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) => {
|
||||||
@@ -698,7 +658,6 @@ 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',
|
||||||
@@ -812,8 +771,7 @@ const PurchaseTable = () => {
|
|||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={handleSortingChange}
|
setSorting={setSorting}
|
||||||
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, SortingState, Updater } from '@tanstack/react-table';
|
import { ColumnDef } 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,25 +73,6 @@ 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();
|
||||||
@@ -271,8 +252,6 @@ 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);
|
||||||
@@ -280,7 +259,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
|
|
||||||
return params.toString();
|
return params.toString();
|
||||||
},
|
},
|
||||||
[filterParams, sortBy, orderBy]
|
[filterParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
@@ -464,23 +443,19 @@ 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');
|
||||||
},
|
},
|
||||||
@@ -488,7 +463,6 @@ 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');
|
||||||
},
|
},
|
||||||
@@ -496,30 +470,21 @@ 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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -527,19 +492,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
accessorKey: 'qty_pengajuan',
|
id: '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',
|
||||||
accessorKey: 'price_pengajuan',
|
id: 'harga_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',
|
||||||
accessorKey: 'total_pengajuan',
|
id: '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) *
|
||||||
@@ -554,19 +523,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
accessorKey: 'qty_realisasi',
|
id: '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',
|
||||||
accessorKey: 'price_realisasi',
|
id: 'harga_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',
|
||||||
accessorKey: 'total_realisasi',
|
id: '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) *
|
||||||
@@ -577,7 +550,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'realization_status',
|
|
||||||
header: 'Status Pencairan',
|
header: 'Status Pencairan',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RealizationStatusBadge
|
<RealizationStatusBadge
|
||||||
@@ -586,7 +558,6 @@ 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} />
|
||||||
@@ -631,9 +602,6 @@ 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',
|
||||||
|
|||||||
@@ -1,47 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { useState } from 'react';
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab';
|
import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab';
|
||||||
import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab';
|
import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab';
|
||||||
import BalanceMonitoringTab from '@/components/pages/report/finance/tab/BalanceMonitoringTab';
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
|
|
||||||
const VALID_TAB_IDS = [
|
|
||||||
'debt-supplier',
|
|
||||||
'customer-payment',
|
|
||||||
'balance-monitoring',
|
|
||||||
];
|
|
||||||
|
|
||||||
const FinanceTabs = () => {
|
const FinanceTabs = () => {
|
||||||
const router = useRouter();
|
const [activeTabId, setActiveTabId] = useState<string>('1');
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const tabParam = searchParams.get('tab') ?? 'debt-supplier';
|
|
||||||
const activeTabId = VALID_TAB_IDS.includes(tabParam)
|
|
||||||
? tabParam
|
|
||||||
: 'debt-supplier';
|
|
||||||
const tabActions = useTabActionsStore((state) => state.tabActions);
|
const tabActions = useTabActionsStore((state) => state.tabActions);
|
||||||
|
|
||||||
const handleTabChange = (tabId: string) => {
|
|
||||||
router.push(`${pathname}?tab=${tabId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
id: 'debt-supplier',
|
id: '1',
|
||||||
label: 'Rekapitulasi Hutang Ke Supplier',
|
label: 'Rekapitulasi Hutang Ke Supplier',
|
||||||
content: <DebtSupplierTab tabId={'debt-supplier'} />,
|
content: <DebtSupplierTab tabId={'1'} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'customer-payment',
|
id: '2',
|
||||||
label: 'Kontrol Pembayaran Customer',
|
label: 'Kontrol Pembayaran Customer',
|
||||||
content: <CustomerPaymentTab tabId={'customer-payment'} />,
|
content: <CustomerPaymentTab tabId={'2'} />,
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'balance-monitoring',
|
|
||||||
label: 'Monitoring Saldo',
|
|
||||||
content: <BalanceMonitoringTab tabId={'balance-monitoring'} />,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -51,7 +29,7 @@ const FinanceTabs = () => {
|
|||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
variant='boxed'
|
variant='boxed'
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={setActiveTabId}
|
||||||
className={{
|
className={{
|
||||||
tabHeaderWrapper:
|
tabHeaderWrapper:
|
||||||
'justify-between items-center p-3 border-b border-base-content/10',
|
'justify-between items-center p-3 border-b border-base-content/10',
|
||||||
|
|||||||
@@ -1,602 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useFormik } from 'formik';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { FinanceApi } from '@/services/api/report/finance-report';
|
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
|
||||||
import { UserApi } from '@/services/api/user';
|
|
||||||
import { useSelect, OptionType } from '@/components/input/SelectInput';
|
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
|
||||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { BalanceMonitoringRow } from '@/types/api/report/balance-monitoring';
|
|
||||||
import { CustomerPaymentRow } from '@/types/api/report/customer-payment';
|
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
|
||||||
import Button from '@/components/Button';
|
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
|
||||||
|
|
||||||
interface BalanceMonitoringTabProps {
|
|
||||||
tabId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterByOptions: OptionType<string>[] = [
|
|
||||||
{ label: 'Tanggal Penjualan (SO Date)', value: 'sold_at' },
|
|
||||||
{ label: 'Tanggal Realisasi (Delivery Date)', value: 'realized_at' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => {
|
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
|
||||||
|
|
||||||
const filterModal = useModal();
|
|
||||||
|
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
|
||||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
|
||||||
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
updateFilter,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
reset: resetFilter,
|
|
||||||
} = useTableFilter<{
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
customers: OptionType<number>[];
|
|
||||||
salesPersons: OptionType<number>[];
|
|
||||||
filterBy?: OptionType<string>;
|
|
||||||
sort_by: string;
|
|
||||||
order_by: string;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
customers: [],
|
|
||||||
salesPersons: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
sort_by: '',
|
|
||||||
order_by: '',
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
start_date: 'start_date',
|
|
||||||
end_date: 'end_date',
|
|
||||||
customers: 'customer_ids',
|
|
||||||
salesPersons: 'sales_ids',
|
|
||||||
filterBy: 'filter_by',
|
|
||||||
sort_by: 'sort_by',
|
|
||||||
order_by: 'sort_order',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'balance-monitoring-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
options: customerOptions,
|
|
||||||
setInputValue: setCustomerInput,
|
|
||||||
isLoadingOptions: isLoadingCustomers,
|
|
||||||
loadMore: loadMoreCustomers,
|
|
||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
|
||||||
|
|
||||||
const {
|
|
||||||
options: salesOptions,
|
|
||||||
setInputValue: setSalesInput,
|
|
||||||
isLoadingOptions: isLoadingSales,
|
|
||||||
loadMore: loadMoreSales,
|
|
||||||
} = useSelect(UserApi.basePath, 'id', 'name', 'search');
|
|
||||||
|
|
||||||
const formik = useFormik({
|
|
||||||
initialValues: {
|
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
customers: tableFilterState.customers,
|
|
||||||
salesPersons: tableFilterState.salesPersons,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
},
|
|
||||||
onSubmit: (values) => {
|
|
||||||
updateFilter('start_date', values.start_date, true);
|
|
||||||
updateFilter('end_date', values.end_date, true);
|
|
||||||
updateFilter('customers', values.customers, true);
|
|
||||||
updateFilter('salesPersons', values.salesPersons, true);
|
|
||||||
updateFilter('filterBy', values.filterBy, true);
|
|
||||||
filterModal.closeModal();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const formikResetHandler = () => {
|
|
||||||
resetFilter();
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
customers: [],
|
|
||||||
salesPersons: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('start_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.end_date) {
|
|
||||||
if (new Date(formik.values.end_date) < new Date(value)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasDateError(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('end_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.start_date) {
|
|
||||||
if (new Date(value) < new Date(formik.values.start_date)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: balanceMonitoringsResponse, isLoading } = useSWR<
|
|
||||||
BaseApiResponse<BalanceMonitoringRow[]>,
|
|
||||||
AxiosError<BaseApiResponse>,
|
|
||||||
SWRHttpKey
|
|
||||||
>(
|
|
||||||
`${FinanceApi.basePath}/balance-monitoring${getTableFilterQueryString()}`,
|
|
||||||
httpClientFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const balanceMonitorings: BalanceMonitoringRow[] = isResponseSuccess(
|
|
||||||
balanceMonitoringsResponse
|
|
||||||
)
|
|
||||||
? ((balanceMonitoringsResponse.data as BalanceMonitoringRow[]) ?? [])
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const meta =
|
|
||||||
isResponseSuccess(balanceMonitoringsResponse) &&
|
|
||||||
balanceMonitoringsResponse.meta
|
|
||||||
? balanceMonitoringsResponse.meta
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Inject tab actions directly — no nested component, no remount cycle
|
|
||||||
useEffect(() => {
|
|
||||||
setTabActions(
|
|
||||||
tabId,
|
|
||||||
<div className='flex flex-row gap-3'>
|
|
||||||
<ButtonFilter
|
|
||||||
values={{
|
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
customers: tableFilterState.customers,
|
|
||||||
salesPersons: tableFilterState.salesPersons,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
}}
|
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
|
||||||
onClick={filterModal.openModal}
|
|
||||||
variant='outline'
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [tabId, setTabActions, tableFilterState, filterModal.openModal]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => clearTabActions(tabId);
|
|
||||||
}, [tabId, clearTabActions]);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
(): ColumnDef<BalanceMonitoringRow>[] => [
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) =>
|
|
||||||
(tableFilterState.page - 1) * tableFilterState.pageSize +
|
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Customer',
|
|
||||||
accessorKey: 'customer.name',
|
|
||||||
enableSorting: true,
|
|
||||||
id: 'customer_name',
|
|
||||||
cell: ({ row }) => row.original.customer.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Saldo Awal',
|
|
||||||
accessorKey: 'saldo_awal',
|
|
||||||
id: 'saldo_awal',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.saldo_awal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penjualan Ayam',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: 'Ekor',
|
|
||||||
accessorKey: 'penjualan_ayam.ekor',
|
|
||||||
id: 'penjualan_ayam_ekor',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_ayam.ekor)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Kg',
|
|
||||||
accessorKey: 'penjualan_ayam.kg',
|
|
||||||
id: 'penjualan_ayam_kg',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_ayam.kg)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nominal',
|
|
||||||
accessorKey: 'penjualan_ayam.nominal',
|
|
||||||
id: 'penjualan_ayam_nominal',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.penjualan_ayam.nominal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penjualan Telur',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: 'Butir',
|
|
||||||
accessorKey: 'penjualan_telur.butir',
|
|
||||||
id: 'penjualan_telur_butir',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_telur.butir)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Kg',
|
|
||||||
accessorKey: 'penjualan_telur.kg',
|
|
||||||
id: 'penjualan_telur_kg',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_telur.kg)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nominal',
|
|
||||||
accessorKey: 'penjualan_telur.nominal',
|
|
||||||
id: 'penjualan_telur_nominal',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.penjualan_telur.nominal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penjualan Trading',
|
|
||||||
accessorKey: 'penjualan_trading.nominal',
|
|
||||||
id: 'penjualan_trading',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.penjualan_trading.nominal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Pembayaran',
|
|
||||||
accessorKey: 'pembayaran',
|
|
||||||
id: 'pembayaran',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.pembayaran)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aging',
|
|
||||||
accessorKey: 'aging',
|
|
||||||
id: 'aging',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-center'>
|
|
||||||
{formatNumber(row.original.aging)} hari
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aging Rata-Rata',
|
|
||||||
accessorKey: 'aging_rata_rata',
|
|
||||||
id: 'aging_rata_rata',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-center'>
|
|
||||||
{formatNumber(row.original.aging_rata_rata)} hari
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Saldo Akhir',
|
|
||||||
accessorKey: 'saldo_akhir',
|
|
||||||
id: 'saldo_akhir',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div
|
|
||||||
className={`text-right font-semibold ${row.original.saldo_akhir < 0 ? 'text-error' : ''}`}
|
|
||||||
>
|
|
||||||
{formatCurrency(row.original.saldo_akhir)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[tableFilterState.page, tableFilterState.pageSize]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
|
||||||
{isLoading && (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && balanceMonitorings.length === 0 && (
|
|
||||||
<CustomerSupplierSkeleton
|
|
||||||
columns={columns as unknown as ColumnDef<CustomerPaymentRow>[]}
|
|
||||||
icon={
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:chart-bar'
|
|
||||||
className='text-white'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title='Data Not Yet Available'
|
|
||||||
subtitle='Please change your filters to get the data.'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && balanceMonitorings.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className='w-full overflow-x-auto'>
|
|
||||||
<Table
|
|
||||||
data={balanceMonitorings}
|
|
||||||
columns={columns}
|
|
||||||
pageSize={tableFilterState.pageSize || 10}
|
|
||||||
page={tableFilterState.page || 1}
|
|
||||||
totalItems={meta?.total_results || 0}
|
|
||||||
onPageChange={setPage}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
// sorting={sorting}
|
|
||||||
// setSorting={handleSortingChange}
|
|
||||||
// manualSorting
|
|
||||||
className={{
|
|
||||||
containerClassName: 'w-full mb-0!',
|
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200 text-nowrap',
|
|
||||||
bodyRowClassName:
|
|
||||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Modal */}
|
|
||||||
<Modal
|
|
||||||
ref={filterModal.ref}
|
|
||||||
className={{
|
|
||||||
modal: 'p-0',
|
|
||||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Modal Header */}
|
|
||||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
|
||||||
<div className='flex items-center gap-2 text-primary'>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant='link'
|
|
||||||
onClick={filterModal.closeModal}
|
|
||||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
|
||||||
<div className='p-4 flex flex-col gap-3'>
|
|
||||||
<div>
|
|
||||||
<label className='block text-xs font-semibold text-base-content py-2'>
|
|
||||||
Tanggal
|
|
||||||
</label>
|
|
||||||
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
|
||||||
<DateInput
|
|
||||||
name='start_date'
|
|
||||||
value={formik.values.start_date || ''}
|
|
||||||
onChange={handleStartDateChange}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
isNestedModal
|
|
||||||
/>
|
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
|
||||||
<DateInput
|
|
||||||
name='end_date'
|
|
||||||
value={formik.values.end_date || ''}
|
|
||||||
onChange={handleEndDateChange}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
isNestedModal
|
|
||||||
isError={hasDateError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SelectInputCheckbox
|
|
||||||
label='Customer'
|
|
||||||
placeholder='Pilih Customer'
|
|
||||||
options={customerOptions}
|
|
||||||
value={formik.values.customers}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue('customers', Array.isArray(val) ? val : [])
|
|
||||||
}
|
|
||||||
onInputChange={setCustomerInput}
|
|
||||||
isLoading={isLoadingCustomers}
|
|
||||||
isClearable
|
|
||||||
onMenuScrollToBottom={loadMoreCustomers}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInputCheckbox
|
|
||||||
label='Sales'
|
|
||||||
placeholder='Pilih Sales'
|
|
||||||
options={salesOptions}
|
|
||||||
value={formik.values.salesPersons}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'salesPersons',
|
|
||||||
Array.isArray(val) ? val : []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onInputChange={setSalesInput}
|
|
||||||
isLoading={isLoadingSales}
|
|
||||||
isClearable
|
|
||||||
onMenuScrollToBottom={loadMoreSales}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInputRadio
|
|
||||||
label='Filter Berdasarkan'
|
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
|
||||||
options={filterByOptions}
|
|
||||||
value={formik.values.filterBy ?? null}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'filterBy',
|
|
||||||
!Array.isArray(val) ? (val ?? undefined) : undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isClearable
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modal Footer */}
|
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
|
||||||
<Button
|
|
||||||
type='reset'
|
|
||||||
variant='soft'
|
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
|
||||||
>
|
|
||||||
Reset Filter
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
|
||||||
disabled={hasDateError}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BalanceMonitoringTab;
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import { useSelect, OptionType } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { FinanceApi } from '@/services/api/report/finance-report';
|
import { FinanceApi } from '@/services/api/report/finance-report';
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import {
|
import {
|
||||||
@@ -30,70 +27,56 @@ import Dropdown from '@/components/Dropdown';
|
|||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
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 { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
||||||
|
import { OptionType } from '@/components/table/TableRowSizeSelector';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
|
|
||||||
interface CustomerPaymentTabProps {
|
interface CustomerPaymentTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataTypeOptions: OptionType<string>[] = [
|
interface FilterParams {
|
||||||
{ value: 'trans_date', label: 'Tanggal Jual/Bayar' },
|
customer_ids?: string;
|
||||||
{ value: 'realization_date', label: 'Tanggal Realisasi' },
|
start_date?: string;
|
||||||
];
|
end_date?: string;
|
||||||
|
filter_by?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
useState(false);
|
|
||||||
const isAnyExportLoading =
|
|
||||||
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
|
||||||
|
|
||||||
|
// ===== PAGINATION STATE =====
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
|
// ===== SUBMISSION STATE =====
|
||||||
|
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
const dataTypeOptions = useMemo(
|
||||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
() => [
|
||||||
|
{ value: 'trans_date', label: 'Tanggal Jual/Bayar' },
|
||||||
const {
|
{ value: 'realization_date', label: 'Tanggal Realisasi' },
|
||||||
state: tableFilterState,
|
],
|
||||||
updateFilter,
|
[]
|
||||||
setPage,
|
);
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
reset: resetFilter,
|
|
||||||
} = useTableFilter<{
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
customers: OptionType<number>[];
|
|
||||||
filterBy?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
customers: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
start_date: 'start_date',
|
|
||||||
end_date: 'end_date',
|
|
||||||
customers: 'customer_ids',
|
|
||||||
filterBy: 'filter_by',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'customer-payment-report-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: customerOptions,
|
options: customerOptions,
|
||||||
@@ -103,188 +86,236 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik({
|
const formik = useFormik<CustomerPaymentFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
start_date: tableFilterState.start_date,
|
start_date: null,
|
||||||
end_date: tableFilterState.end_date,
|
end_date: null,
|
||||||
customers: tableFilterState.customers,
|
customer_ids: null,
|
||||||
filterBy: tableFilterState.filterBy,
|
filter_by: null,
|
||||||
},
|
},
|
||||||
onSubmit: (values) => {
|
validationSchema: CustomerPaymentFilterSchema,
|
||||||
updateFilter('start_date', values.start_date, true);
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('end_date', values.end_date, true);
|
setFilterParams({
|
||||||
updateFilter('customers', values.customers, true);
|
start_date: values.start_date || undefined,
|
||||||
updateFilter('filterBy', values.filterBy, true);
|
end_date: values.end_date || undefined,
|
||||||
|
customer_ids: values.customer_ids || undefined,
|
||||||
|
filter_by: values.filter_by || undefined,
|
||||||
|
});
|
||||||
|
filterModal.closeModal();
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
setFilterParams({});
|
||||||
|
setCurrentPage(1);
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const formikResetHandler = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
resetFilter();
|
formik.setValues({
|
||||||
|
start_date: filterParams.start_date || null,
|
||||||
setHasDateError(false);
|
end_date: filterParams.end_date || null,
|
||||||
if (dateErrorShown) {
|
customer_ids: filterParams.customer_ids || null,
|
||||||
toast.dismiss();
|
filter_by: filterParams.filter_by || null,
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
customers: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
filterModal.openModal();
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPaymentStatusBadgeColor = (notes: string): Color => {
|
const getPaymentStatusBadgeColor = (notes: string): Color => {
|
||||||
const normalizedValue = notes.toLowerCase();
|
const normalizedValue = notes.toLowerCase();
|
||||||
if (normalizedValue === 'lunas') return 'primary';
|
|
||||||
if (normalizedValue.includes('belum')) return 'warning';
|
if (normalizedValue === 'lunas') {
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedValue.includes('belum')) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
return 'neutral';
|
return 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== DATE CHANGE HANDLERS =====
|
// ===== DATE CHANGE HANDLERS =====
|
||||||
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleStartDateChange = useCallback(
|
||||||
const value = e.target.value;
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
formik.setFieldValue('start_date', value);
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('start_date', value || null);
|
||||||
|
|
||||||
if (value && formik.values.end_date) {
|
if (value && formik.values.end_date) {
|
||||||
if (new Date(formik.values.end_date) < new Date(value)) {
|
const startDate = new Date(value);
|
||||||
setHasDateError(true);
|
const endDateObj = new Date(formik.values.end_date);
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
if (endDateObj < startDate) {
|
||||||
duration: Infinity,
|
setHasDateError(true);
|
||||||
});
|
if (!dateErrorShown) {
|
||||||
setDateErrorShown(true);
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setHasDateError(false);
|
setHasDateError(false);
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
setHasDateError(false);
|
[formik, dateErrorShown]
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('end_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.start_date) {
|
|
||||||
if (new Date(value) < new Date(formik.values.start_date)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
|
||||||
const { data: customerPayment, isLoading } = useSWR<
|
|
||||||
BaseApiResponse<CustomerPaymentReport>,
|
|
||||||
AxiosError<BaseApiResponse>,
|
|
||||||
SWRHttpKey
|
|
||||||
>(
|
|
||||||
`${FinanceApi.basePath}/customer-payment${getTableFilterQueryString()}`,
|
|
||||||
httpClientFetcher
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const data: CustomerPaymentReport[] = isResponseSuccess(customerPayment)
|
const handleEndDateChange = useCallback(
|
||||||
? (customerPayment?.data as unknown as CustomerPaymentReport[]) || []
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
: [];
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('end_date', value || null);
|
||||||
|
|
||||||
const meta =
|
if (value && formik.values.start_date) {
|
||||||
isResponseSuccess(customerPayment) && customerPayment.meta
|
const startDateObj = new Date(formik.values.start_date);
|
||||||
? customerPayment.meta
|
const endDate = new Date(value);
|
||||||
: null;
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[formik, dateErrorShown]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const customerIdsValue = useMemo(() => {
|
||||||
|
if (!formik.values.customer_ids) return [];
|
||||||
|
return customerOptions.filter((opt) =>
|
||||||
|
formik.values.customer_ids?.split(',').includes(String(opt.value))
|
||||||
|
);
|
||||||
|
}, [formik.values.customer_ids, customerOptions]);
|
||||||
|
|
||||||
|
const filterByValue = useMemo(() => {
|
||||||
|
if (!formik.values.filter_by) return null;
|
||||||
|
return (
|
||||||
|
dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.filter_by, dataTypeOptions]);
|
||||||
|
|
||||||
|
// ===== DATA FETCHING =====
|
||||||
|
const { data: customerPayment, isLoading } = useSWR(
|
||||||
|
() => {
|
||||||
|
const params = {
|
||||||
|
customer_ids: filterParams.customer_ids,
|
||||||
|
filter_by: filterParams.filter_by as
|
||||||
|
| 'trans_date'
|
||||||
|
| 'realization_date'
|
||||||
|
| undefined,
|
||||||
|
start_date: filterParams.start_date,
|
||||||
|
end_date: filterParams.end_date,
|
||||||
|
page: currentPage,
|
||||||
|
limit: pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ['customer-payment-report', params];
|
||||||
|
},
|
||||||
|
([, params]) =>
|
||||||
|
FinanceApi.getCustomerPaymentReport(
|
||||||
|
params.customer_ids,
|
||||||
|
params.filter_by,
|
||||||
|
params.start_date,
|
||||||
|
params.end_date,
|
||||||
|
params.page,
|
||||||
|
params.limit
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: CustomerPaymentReport[] = useMemo(
|
||||||
|
() =>
|
||||||
|
isResponseSuccess(customerPayment)
|
||||||
|
? (customerPayment?.data as unknown as CustomerPaymentReport[]) || []
|
||||||
|
: [],
|
||||||
|
[customerPayment]
|
||||||
|
);
|
||||||
|
|
||||||
|
const meta = useMemo(
|
||||||
|
() =>
|
||||||
|
isResponseSuccess(customerPayment) && customerPayment.meta
|
||||||
|
? customerPayment.meta
|
||||||
|
: null,
|
||||||
|
[customerPayment]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const customerPaymentExport = useCallback(async (): Promise<
|
const customerPaymentExport = useCallback(async (): Promise<
|
||||||
CustomerPaymentReport[] | null
|
CustomerPaymentReport[] | null
|
||||||
> => {
|
> => {
|
||||||
const customer_ids =
|
const params = {
|
||||||
tableFilterState.customers.length > 0
|
customer_ids: filterParams.customer_ids,
|
||||||
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
filter_by: filterParams.filter_by as
|
||||||
: undefined;
|
| 'trans_date'
|
||||||
const filter_by = tableFilterState.filterBy?.value as
|
| 'realization_date'
|
||||||
| 'trans_date'
|
| undefined,
|
||||||
| 'realization_date'
|
start_date: filterParams.start_date,
|
||||||
| undefined;
|
end_date: filterParams.end_date,
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await FinanceApi.getCustomerPaymentReport(
|
const response = await FinanceApi.getCustomerPaymentReport(
|
||||||
customer_ids,
|
params.customer_ids,
|
||||||
filter_by,
|
params.filter_by,
|
||||||
tableFilterState.start_date || undefined,
|
params.start_date,
|
||||||
tableFilterState.end_date || undefined,
|
params.end_date,
|
||||||
1,
|
params.page,
|
||||||
100
|
params.limit
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response)
|
return isResponseSuccess(response)
|
||||||
? (response.data as unknown as CustomerPaymentReport[])
|
? (response.data as unknown as CustomerPaymentReport[])
|
||||||
: null;
|
: null;
|
||||||
}, [tableFilterState]);
|
}, [filterParams]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcelGeneral = useCallback(async () => {
|
|
||||||
setIsExcelGeneralExportLoading(true);
|
|
||||||
try {
|
|
||||||
const customer_ids =
|
|
||||||
tableFilterState.customers.length > 0
|
|
||||||
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
|
||||||
: undefined;
|
|
||||||
await FinanceApi.exportCustomerPaymentToExcelGeneral(
|
|
||||||
customer_ids,
|
|
||||||
tableFilterState.filterBy?.value,
|
|
||||||
tableFilterState.start_date || undefined,
|
|
||||||
tableFilterState.end_date || undefined
|
|
||||||
);
|
|
||||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExcelGeneralExportLoading(false);
|
|
||||||
}
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const customer_ids =
|
const allDataForExport = await customerPaymentExport();
|
||||||
tableFilterState.customers.length > 0
|
|
||||||
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
if (
|
||||||
: undefined;
|
!allDataForExport ||
|
||||||
await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet(
|
!Array.isArray(allDataForExport) ||
|
||||||
customer_ids,
|
allDataForExport.length === 0
|
||||||
tableFilterState.filterBy?.value,
|
) {
|
||||||
tableFilterState.start_date || undefined,
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
tableFilterState.end_date || undefined
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
await generateCustomerPaymentExcel({ data: allDataForExport });
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [tableFilterState]);
|
}, [customerPaymentExport]);
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -300,18 +331,22 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerName =
|
const customerName = filterParams.customer_ids
|
||||||
tableFilterState.customers.length > 0
|
? customerOptions
|
||||||
? tableFilterState.customers.map((o) => o.label).join(', ')
|
.filter((opt) =>
|
||||||
: 'Semua Customer';
|
filterParams.customer_ids?.split(',').includes(String(opt.value))
|
||||||
|
)
|
||||||
|
.map((opt) => opt.label)
|
||||||
|
.join(', ') || 'Semua Customer'
|
||||||
|
: 'Semua Customer';
|
||||||
|
|
||||||
await generateCustomerPaymentPDF({
|
await generateCustomerPaymentPDF({
|
||||||
data: allDataForExport,
|
data: allDataForExport,
|
||||||
params: {
|
params: {
|
||||||
customer_name: customerName,
|
customer_name: customerName,
|
||||||
start_date: tableFilterState.start_date || undefined,
|
start_date: filterParams.start_date,
|
||||||
end_date: tableFilterState.end_date || undefined,
|
end_date: filterParams.end_date,
|
||||||
filter_by: tableFilterState.filterBy?.value as
|
filter_by: filterParams.filter_by as
|
||||||
| 'trans_date'
|
| 'trans_date'
|
||||||
| 'realization_date'
|
| 'realization_date'
|
||||||
| undefined,
|
| undefined,
|
||||||
@@ -323,103 +358,106 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsPdfExportLoading(false);
|
setIsPdfExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [customerPaymentExport, tableFilterState]);
|
}, [customerPaymentExport, filterParams, customerOptions]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
useEffect(() => {
|
const TabActions = useMemo(() => {
|
||||||
setTabActions(
|
return function TabActionsComponent() {
|
||||||
tabId,
|
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||||
<div className='flex flex-row gap-3'>
|
const clearTabActions = useTabActionsStore(
|
||||||
<ButtonFilter
|
(state) => state.clearTabActions
|
||||||
values={{
|
);
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
customers: tableFilterState.customers,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
}}
|
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
|
||||||
onClick={filterModal.openModal}
|
|
||||||
variant='outline'
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
useEffect(() => {
|
||||||
align='end'
|
setTabActions(
|
||||||
direction='bottom'
|
tabId,
|
||||||
className={{
|
<div className='flex flex-row gap-3'>
|
||||||
content:
|
<ButtonFilter
|
||||||
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
values={filterParams}
|
||||||
}}
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
trigger={
|
onClick={() => handleFilterModalOpenRef.current()}
|
||||||
<Button
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
className='px-3 py-2.5'
|
||||||
isLoading={isAnyExportLoading}
|
/>
|
||||||
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
|
||||||
|
<Dropdown
|
||||||
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
|
className={{
|
||||||
|
content:
|
||||||
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
isLoading={isAnyExportLoading}
|
||||||
|
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>Export</span>
|
||||||
|
|
||||||
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-down'
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
<Button
|
||||||
<Icon
|
variant='ghost'
|
||||||
icon='heroicons:cloud-arrow-down'
|
color='none'
|
||||||
width={20}
|
onClick={handleExportExcel}
|
||||||
height={20}
|
isLoading={isExcelExportLoading}
|
||||||
/>
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
<span>Export</span>
|
>
|
||||||
<div className='w-px self-stretch bg-base-content/10' />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
<Icon icon='heroicons:chevron-down' width={14} height={14} />
|
Export to Excel
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
}
|
variant='ghost'
|
||||||
>
|
color='none'
|
||||||
<Button
|
onClick={handleExportPdf}
|
||||||
variant='ghost'
|
isLoading={isPdfExportLoading}
|
||||||
color='none'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
onClick={handleExportExcel}
|
>
|
||||||
isLoading={isExcelExportLoading}
|
<Icon icon='heroicons:document' width={20} height={20} />
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
Export to PDF
|
||||||
>
|
</Button>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
</Dropdown>
|
||||||
Export to Excel - Customer Per Sheet
|
</div>
|
||||||
</Button>
|
);
|
||||||
<Button
|
}, [setTabActions]);
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
useEffect(() => {
|
||||||
onClick={handleExportExcelGeneral}
|
return () => {
|
||||||
isLoading={isExcelGeneralExportLoading}
|
clearTabActions(tabId);
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
};
|
||||||
>
|
}, [clearTabActions]);
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Export to Excel - General
|
return null;
|
||||||
</Button>
|
};
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={handleExportPdf}
|
|
||||||
isLoading={isPdfExportLoading}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:document' width={20} height={20} />
|
|
||||||
Export to PDF
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
setTabActions,
|
|
||||||
tableFilterState,
|
|
||||||
filterModal.openModal,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportExcelGeneral,
|
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
isExcelExportLoading,
|
isExcelExportLoading,
|
||||||
isExcelGeneralExportLoading,
|
|
||||||
isPdfExportLoading,
|
isPdfExportLoading,
|
||||||
|
filterParams,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
||||||
return () => clearTabActions(tabId);
|
|
||||||
}, [tabId, clearTabActions]);
|
|
||||||
|
|
||||||
const getTableColumns = (
|
const getTableColumns = (
|
||||||
summary: CustomerPaymentSummary
|
summary: CustomerPaymentSummary
|
||||||
@@ -626,7 +664,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.status;
|
const value = props.row.original.status;
|
||||||
if (!value) return '-';
|
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
color={getPaymentStatusBadgeColor(value)}
|
color={getPaymentStatusBadgeColor(value)}
|
||||||
@@ -665,6 +707,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{TabActionsElement}
|
||||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
@@ -693,16 +736,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<Pagination
|
<Pagination
|
||||||
totalItems={meta.total_results || 0}
|
totalItems={meta.total_results || 0}
|
||||||
itemsPerPage={meta.limit || 0}
|
itemsPerPage={meta.limit || 0}
|
||||||
currentPage={tableFilterState.page}
|
currentPage={meta.page || 0}
|
||||||
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
onPrevPage={() =>
|
||||||
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||||
|
}
|
||||||
onNextPage={() =>
|
onNextPage={() =>
|
||||||
setPage(
|
setCurrentPage((curr) =>
|
||||||
meta.total_pages && tableFilterState.page < meta.total_pages
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||||
? tableFilterState.page + 1
|
|
||||||
: tableFilterState.page
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||||
rowOptions={[10, 20, 50, 100]}
|
rowOptions={[10, 20, 50, 100]}
|
||||||
onRowChange={setPageSize}
|
onRowChange={setPageSize}
|
||||||
/>
|
/>
|
||||||
@@ -809,16 +852,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<Pagination
|
<Pagination
|
||||||
totalItems={meta.total_results || 0}
|
totalItems={meta.total_results || 0}
|
||||||
itemsPerPage={meta.limit || 0}
|
itemsPerPage={meta.limit || 0}
|
||||||
currentPage={tableFilterState.page}
|
currentPage={meta.page || 0}
|
||||||
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
onPrevPage={() =>
|
||||||
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||||
|
}
|
||||||
onNextPage={() =>
|
onNextPage={() =>
|
||||||
setPage(
|
setCurrentPage((curr) =>
|
||||||
meta.total_pages && tableFilterState.page < meta.total_pages
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||||
? tableFilterState.page + 1
|
|
||||||
: tableFilterState.page
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||||
rowOptions={[10, 20, 50, 100]}
|
rowOptions={[10, 20, 50, 100]}
|
||||||
onRowChange={setPageSize}
|
onRowChange={setPageSize}
|
||||||
/>
|
/>
|
||||||
@@ -848,7 +891,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<div>
|
<div>
|
||||||
<label className='block text-xs font-semibold text-base-content py-2'>
|
<label className='block text-xs font-semibold text-base-content py-2'>
|
||||||
@@ -858,18 +901,29 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<DateInput
|
<DateInput
|
||||||
name='start_date'
|
name='start_date'
|
||||||
value={formik.values.start_date || ''}
|
value={formik.values.start_date || ''}
|
||||||
|
errorMessage={formik.errors.start_date}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
|
isError={
|
||||||
|
formik.touched.start_date &&
|
||||||
|
Boolean(formik.errors.start_date)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='end_date'
|
||||||
value={formik.values.end_date || ''}
|
value={formik.values.end_date || ''}
|
||||||
|
errorMessage={formik.errors.end_date}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
isError={hasDateError}
|
isError={
|
||||||
|
(formik.touched.end_date &&
|
||||||
|
Boolean(formik.errors.end_date)) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -878,10 +932,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
label='Customer'
|
label='Customer'
|
||||||
placeholder='Pilih Customer'
|
placeholder='Pilih Customer'
|
||||||
options={customerOptions}
|
options={customerOptions}
|
||||||
value={formik.values.customers}
|
value={customerIdsValue}
|
||||||
onChange={(val) =>
|
onChange={(val) => {
|
||||||
formik.setFieldValue('customers', Array.isArray(val) ? val : [])
|
formik.setFieldValue(
|
||||||
}
|
'customer_ids',
|
||||||
|
Array.isArray(val) && val.length > 0
|
||||||
|
? val.map((v: OptionType) => String(v.value)).join(',')
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}}
|
||||||
onInputChange={setCustomerInputValue}
|
onInputChange={setCustomerInputValue}
|
||||||
isLoading={isLoadingCustomers}
|
isLoading={isLoadingCustomers}
|
||||||
isClearable
|
isClearable
|
||||||
@@ -893,15 +952,14 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
label='Filter Berdasarkan'
|
label='Filter Berdasarkan'
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
options={dataTypeOptions}
|
options={dataTypeOptions}
|
||||||
value={formik.values.filterBy ?? null}
|
value={filterByValue}
|
||||||
onChange={(val) =>
|
onChange={(val) => {
|
||||||
formik.setFieldValue(
|
if (!Array.isArray(val)) {
|
||||||
'filterBy',
|
formik.setFieldValue('filter_by', val?.value || null);
|
||||||
!Array.isArray(val) ? (val ?? undefined) : undefined
|
}
|
||||||
)
|
}}
|
||||||
}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isClearable
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -917,7 +975,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={hasDateError}
|
disabled={hasDateError || !formik.isValid || formik.isSubmitting}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,15 +9,24 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
|
import {
|
||||||
|
DebtRow,
|
||||||
|
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 { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
|
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
import {
|
||||||
|
DebtSupplierFilterSchema,
|
||||||
|
DebtSupplierFilterType,
|
||||||
|
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
@@ -26,10 +35,6 @@ import SelectInputRadio from '@/components/input/SelectInputRadio';
|
|||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton';
|
import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
const dueStatus: Record<string, Color> = {
|
const dueStatus: Record<string, Color> = {
|
||||||
'Sudah Jatuh Tempo': 'error',
|
'Sudah Jatuh Tempo': 'error',
|
||||||
@@ -47,6 +52,7 @@ const getPillBadge = (
|
|||||||
statusText: string,
|
statusText: string,
|
||||||
type: 'due' | 'payment' = 'payment'
|
type: 'due' | 'payment' = 'payment'
|
||||||
) => {
|
) => {
|
||||||
|
// Get color based on type
|
||||||
const color =
|
const color =
|
||||||
type === 'due'
|
type === 'due'
|
||||||
? dueStatus[statusText] || 'neutral'
|
? dueStatus[statusText] || 'neutral'
|
||||||
@@ -63,11 +69,6 @@ const getPillBadge = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataTypeOptions: OptionType<string>[] = [
|
|
||||||
{ value: 'received_date', label: 'Tanggal Terima' },
|
|
||||||
{ value: 'po_date', label: 'Tanggal PO' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface DebtSupplierTabProps {
|
interface DebtSupplierTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
}
|
}
|
||||||
@@ -76,50 +77,28 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
useState(false);
|
|
||||||
const isAnyExportLoading =
|
|
||||||
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
|
||||||
|
|
||||||
|
// ===== PAGINATION STATE =====
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
|
// ===== SUBMISSION STATE =====
|
||||||
|
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
|
||||||
|
start_date: undefined,
|
||||||
|
end_date: undefined,
|
||||||
|
supplier_ids: undefined,
|
||||||
|
filter_by: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== DATE ERROR STATE =====
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
|
||||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
|
||||||
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
updateFilter,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
reset: resetFilter,
|
|
||||||
} = useTableFilter<{
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
suppliers: OptionType<number>[];
|
|
||||||
filterBy?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
suppliers: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
start_date: 'start_date',
|
|
||||||
end_date: 'end_date',
|
|
||||||
suppliers: 'supplier_ids',
|
|
||||||
filterBy: 'filter_by',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'debt-supplier-report-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setSupplierInputValue,
|
setInputValue: setSupplierInputValue,
|
||||||
options: supplierOptions,
|
options: supplierOptions,
|
||||||
@@ -127,180 +106,168 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
loadMore: loadMoreSuppliers,
|
loadMore: loadMoreSuppliers,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const dataTypeOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'received_date', label: 'Tanggal Terima' },
|
||||||
|
{ value: 'po_date', label: 'Tanggal PO' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik({
|
const formik = useFormik<DebtSupplierFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
start_date: tableFilterState.start_date,
|
startDate: null,
|
||||||
end_date: tableFilterState.end_date,
|
endDate: null,
|
||||||
suppliers: tableFilterState.suppliers,
|
supplierIds: null,
|
||||||
filterBy: tableFilterState.filterBy,
|
filterBy: null,
|
||||||
},
|
},
|
||||||
|
validationSchema: DebtSupplierFilterSchema,
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
updateFilter('start_date', values.start_date, true);
|
setFilterParams({
|
||||||
updateFilter('end_date', values.end_date, true);
|
start_date: values.startDate?.toString() || undefined,
|
||||||
updateFilter('suppliers', values.suppliers, true);
|
end_date: values.endDate?.toString() || undefined,
|
||||||
updateFilter('filterBy', values.filterBy, true);
|
supplier_ids:
|
||||||
|
values.supplierIds?.map((v) => String(v.value)).join(',') ||
|
||||||
|
undefined,
|
||||||
|
filter_by: values.filterBy?.value?.toString() || undefined,
|
||||||
|
});
|
||||||
|
filterModal.closeModal();
|
||||||
|
setCurrentPage(1);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
setFilterParams({
|
||||||
|
start_date: undefined,
|
||||||
|
end_date: undefined,
|
||||||
|
supplier_ids: undefined,
|
||||||
|
filter_by: undefined,
|
||||||
|
});
|
||||||
|
setCurrentPage(1);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const formikResetHandler = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
resetFilter();
|
const restoredFilterBy =
|
||||||
|
dataTypeOptions.find((opt) => opt.value === filterParams.filter_by) ||
|
||||||
|
null;
|
||||||
|
|
||||||
setHasDateError(false);
|
const supplierIdList = filterParams.supplier_ids
|
||||||
if (dateErrorShown) {
|
? filterParams.supplier_ids.split(',')
|
||||||
toast.dismiss();
|
: [];
|
||||||
setDateErrorShown(false);
|
const restoredSupplierIds = supplierOptions.filter((opt) =>
|
||||||
}
|
supplierIdList.includes(String(opt.value))
|
||||||
|
);
|
||||||
|
|
||||||
formik.resetForm({
|
formik.setValues({
|
||||||
values: {
|
startDate: filterParams.start_date || null,
|
||||||
start_date: '',
|
endDate: filterParams.end_date || null,
|
||||||
end_date: '',
|
supplierIds: restoredSupplierIds.length > 0 ? restoredSupplierIds : null,
|
||||||
suppliers: [],
|
filterBy: restoredFilterBy,
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
filterModal.openModal();
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== DATE CHANGE HANDLERS =====
|
|
||||||
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('start_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.end_date) {
|
|
||||||
if (new Date(formik.values.end_date) < new Date(value)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasDateError(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('end_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.start_date) {
|
|
||||||
if (new Date(value) < new Date(formik.values.start_date)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: debtSupplierResponse, isLoading } = useSWR<
|
const { data: debtSupplier, isLoading } = useSWR(
|
||||||
BaseApiResponse<DebtSupplier[]>,
|
() => {
|
||||||
AxiosError<BaseApiResponse>,
|
const params = {
|
||||||
SWRHttpKey
|
supplier_ids: filterParams.supplier_ids,
|
||||||
>(
|
filter_by: filterParams.filter_by,
|
||||||
`${DebtSupplierApi.basePath}/debt-supplier${getTableFilterQueryString()}`,
|
start_date: filterParams.start_date,
|
||||||
httpClientFetcher
|
end_date: filterParams.end_date,
|
||||||
|
page: currentPage,
|
||||||
|
limit: pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ['debt-supplier-report', params];
|
||||||
|
},
|
||||||
|
([, params]) =>
|
||||||
|
DebtSupplierApi.getDebtSupplierReport(
|
||||||
|
params.supplier_ids,
|
||||||
|
params.filter_by,
|
||||||
|
params.start_date,
|
||||||
|
params.end_date,
|
||||||
|
params.page,
|
||||||
|
params.limit
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const data: DebtSupplier[] = isResponseSuccess(debtSupplierResponse)
|
const data: DebtSupplier[] = useMemo(
|
||||||
? ((debtSupplierResponse?.data as unknown as DebtSupplier[]) ?? [])
|
() =>
|
||||||
: [];
|
isResponseSuccess(debtSupplier)
|
||||||
|
? (debtSupplier?.data as unknown as DebtSupplier[]) || []
|
||||||
|
: [],
|
||||||
|
[debtSupplier]
|
||||||
|
);
|
||||||
|
|
||||||
const meta =
|
const meta = useMemo(
|
||||||
isResponseSuccess(debtSupplierResponse) && debtSupplierResponse.meta
|
() =>
|
||||||
? debtSupplierResponse.meta
|
isResponseSuccess(debtSupplier) && debtSupplier.meta
|
||||||
: null;
|
? debtSupplier.meta
|
||||||
|
: null,
|
||||||
|
[debtSupplier]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const debtSupplierExport = useCallback(async (): Promise<
|
const debtSupplierExport = useCallback(async (): Promise<
|
||||||
DebtSupplier[] | null
|
DebtSupplier[] | null
|
||||||
> => {
|
> => {
|
||||||
const supplier_ids =
|
const params = {
|
||||||
tableFilterState.suppliers.length > 0
|
supplier_ids:
|
||||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
formik.values.supplierIds && formik.values.supplierIds.length > 0
|
||||||
: undefined;
|
? formik.values.supplierIds.map((v) => String(v.value)).join(',')
|
||||||
|
: undefined,
|
||||||
|
filter_by: formik.values.filterBy?.value?.toString() || undefined,
|
||||||
|
start_date: formik.values.startDate || undefined,
|
||||||
|
end_date: formik.values.endDate || undefined,
|
||||||
|
date_type: formik.values.filterBy
|
||||||
|
? formik.values.filterBy.value
|
||||||
|
: undefined,
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await DebtSupplierApi.getDebtSupplierReport(
|
const response = await DebtSupplierApi.getDebtSupplierReport(
|
||||||
supplier_ids,
|
params.supplier_ids,
|
||||||
tableFilterState.filterBy?.value,
|
params.filter_by,
|
||||||
tableFilterState.start_date || undefined,
|
params.start_date,
|
||||||
tableFilterState.end_date || undefined,
|
params.end_date
|
||||||
1,
|
|
||||||
100
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response)
|
return isResponseSuccess(response)
|
||||||
? (response.data as unknown as DebtSupplier[])
|
? (response.data as unknown as DebtSupplier[])
|
||||||
: null;
|
: null;
|
||||||
}, [tableFilterState]);
|
}, [
|
||||||
|
formik.values.supplierIds,
|
||||||
|
formik.values.startDate,
|
||||||
|
formik.values.endDate,
|
||||||
|
formik.values.filterBy,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const supplier_ids =
|
const allDataForExport = await debtSupplierExport();
|
||||||
tableFilterState.suppliers.length > 0
|
|
||||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
if (
|
||||||
: undefined;
|
!allDataForExport ||
|
||||||
await DebtSupplierApi.exportToExcelSupplierPerSheet(
|
!Array.isArray(allDataForExport) ||
|
||||||
supplier_ids,
|
allDataForExport.length === 0
|
||||||
tableFilterState.filterBy?.value,
|
) {
|
||||||
tableFilterState.start_date || undefined,
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
tableFilterState.end_date || undefined
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
generateDebtSupplierExcel({ data: allDataForExport });
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [tableFilterState]);
|
}, [debtSupplierExport]);
|
||||||
|
|
||||||
const handleExportExcelGeneral = useCallback(async () => {
|
|
||||||
setIsExcelGeneralExportLoading(true);
|
|
||||||
try {
|
|
||||||
const supplier_ids =
|
|
||||||
tableFilterState.suppliers.length > 0
|
|
||||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await DebtSupplierApi.exportToExcelGeneral(
|
|
||||||
supplier_ids,
|
|
||||||
tableFilterState.filterBy?.value,
|
|
||||||
tableFilterState.start_date || undefined,
|
|
||||||
tableFilterState.end_date || undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExcelGeneralExportLoading(false);
|
|
||||||
}
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -316,18 +283,15 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supplierName =
|
|
||||||
tableFilterState.suppliers.length > 0
|
|
||||||
? tableFilterState.suppliers.map((o) => o.label).join(', ')
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await generateDebtSupplierPDF({
|
await generateDebtSupplierPDF({
|
||||||
data: allDataForExport,
|
data: allDataForExport,
|
||||||
params: {
|
params: {
|
||||||
supplier_name: supplierName,
|
supplier_name: formik.values.supplierIds
|
||||||
filter_by: tableFilterState.filterBy?.label,
|
?.map((v) => v.label)
|
||||||
start_date: tableFilterState.start_date || undefined,
|
.join(', '),
|
||||||
end_date: tableFilterState.end_date || undefined,
|
filter_by: formik.values.filterBy?.label,
|
||||||
|
start_date: formik.values.startDate || undefined,
|
||||||
|
end_date: formik.values.endDate || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
@@ -336,103 +300,129 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsPdfExportLoading(false);
|
setIsPdfExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [debtSupplierExport, tableFilterState]);
|
}, [
|
||||||
|
debtSupplierExport,
|
||||||
|
formik.values.supplierIds,
|
||||||
|
formik.values.filterBy,
|
||||||
|
formik.values.startDate,
|
||||||
|
formik.values.endDate,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
useEffect(() => {
|
const TabActions = useMemo(() => {
|
||||||
setTabActions(
|
return function TabActionsComponent() {
|
||||||
tabId,
|
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||||
<div className='flex flex-row gap-3'>
|
const clearTabActions = useTabActionsStore(
|
||||||
<ButtonFilter
|
(state) => state.clearTabActions
|
||||||
values={{
|
);
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
suppliers: tableFilterState.suppliers,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
}}
|
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
|
||||||
onClick={filterModal.openModal}
|
|
||||||
variant='outline'
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
useEffect(() => {
|
||||||
align='end'
|
setTabActions(
|
||||||
direction='bottom'
|
tabId,
|
||||||
className={{
|
<div className='flex flex-row gap-3'>
|
||||||
content:
|
<ButtonFilter
|
||||||
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
values={filterParams}
|
||||||
}}
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
trigger={
|
onClick={() => handleFilterModalOpenRef.current()}
|
||||||
<Button
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
className='px-3 py-2.5'
|
||||||
isLoading={isAnyExportLoading}
|
/>
|
||||||
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
|
||||||
|
<Dropdown
|
||||||
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
|
className={{
|
||||||
|
content:
|
||||||
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
isLoading={isAnyExportLoading}
|
||||||
|
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>Export</span>
|
||||||
|
|
||||||
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-down'
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
<Button
|
||||||
<Icon
|
variant='ghost'
|
||||||
icon='heroicons:cloud-arrow-down'
|
color='none'
|
||||||
width={20}
|
onClick={handleExportExcel}
|
||||||
height={20}
|
isLoading={isExcelExportLoading}
|
||||||
/>
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
<span>Export</span>
|
>
|
||||||
<div className='w-px self-stretch bg-base-content/10' />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
<Icon icon='heroicons:chevron-down' width={14} height={14} />
|
Export to Excel
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
}
|
variant='ghost'
|
||||||
>
|
color='none'
|
||||||
<Button
|
onClick={handleExportPdf}
|
||||||
variant='ghost'
|
isLoading={isPdfExportLoading}
|
||||||
color='none'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
onClick={handleExportExcel}
|
>
|
||||||
isLoading={isExcelExportLoading}
|
<Icon icon='heroicons:document' width={20} height={20} />
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
Export to PDF
|
||||||
>
|
</Button>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
</Dropdown>
|
||||||
Export to Excel - Supplier Per Sheet
|
</div>
|
||||||
</Button>
|
);
|
||||||
<Button
|
}, [setTabActions]);
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
useEffect(() => {
|
||||||
onClick={handleExportExcelGeneral}
|
return () => {
|
||||||
isLoading={isExcelGeneralExportLoading}
|
clearTabActions(tabId);
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
};
|
||||||
>
|
}, [clearTabActions]);
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Export to Excel - General
|
return null;
|
||||||
</Button>
|
};
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={handleExportPdf}
|
|
||||||
isLoading={isPdfExportLoading}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:document' width={20} height={20} />
|
|
||||||
Export to PDF
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
setTabActions,
|
filterParams,
|
||||||
tableFilterState,
|
|
||||||
filterModal.openModal,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportExcelGeneral,
|
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
isExcelExportLoading,
|
isExcelExportLoading,
|
||||||
isExcelGeneralExportLoading,
|
|
||||||
isPdfExportLoading,
|
isPdfExportLoading,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => clearTabActions(tabId);
|
return () => {
|
||||||
}, [tabId, clearTabActions]);
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [filterModal.open, dateErrorShown]);
|
||||||
|
|
||||||
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
||||||
{
|
{
|
||||||
@@ -647,9 +637,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{TabActionsElement}
|
||||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
@@ -678,16 +668,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
<Pagination
|
<Pagination
|
||||||
totalItems={meta.total_results || 0}
|
totalItems={meta.total_results || 0}
|
||||||
itemsPerPage={meta.limit || 0}
|
itemsPerPage={meta.limit || 0}
|
||||||
currentPage={tableFilterState.page}
|
currentPage={meta.page || 0}
|
||||||
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
onPrevPage={() =>
|
||||||
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||||
|
}
|
||||||
onNextPage={() =>
|
onNextPage={() =>
|
||||||
setPage(
|
setCurrentPage((curr) =>
|
||||||
meta.total_pages && tableFilterState.page < meta.total_pages
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||||
? tableFilterState.page + 1
|
|
||||||
: tableFilterState.page
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||||
rowOptions={[10, 20, 50, 100]}
|
rowOptions={[10, 20, 50, 100]}
|
||||||
onRowChange={setPageSize}
|
onRowChange={setPageSize}
|
||||||
/>
|
/>
|
||||||
@@ -787,16 +777,16 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
<Pagination
|
<Pagination
|
||||||
totalItems={meta.total_results || 0}
|
totalItems={meta.total_results || 0}
|
||||||
itemsPerPage={meta.limit || 0}
|
itemsPerPage={meta.limit || 0}
|
||||||
currentPage={tableFilterState.page}
|
currentPage={meta.page || 0}
|
||||||
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
onPrevPage={() =>
|
||||||
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||||
|
}
|
||||||
onNextPage={() =>
|
onNextPage={() =>
|
||||||
setPage(
|
setCurrentPage((curr) =>
|
||||||
meta.total_pages && tableFilterState.page < meta.total_pages
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||||
? tableFilterState.page + 1
|
|
||||||
: tableFilterState.page
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||||
rowOptions={[10, 20, 50, 100]}
|
rowOptions={[10, 20, 50, 100]}
|
||||||
onRowChange={setPageSize}
|
onRowChange={setPageSize}
|
||||||
/>
|
/>
|
||||||
@@ -812,23 +802,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
{/* Modal Header */}
|
||||||
<div className='flex items-center gap-2 text-primary'>
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
type='button'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant='link'
|
|
||||||
type='button'
|
|
||||||
onClick={filterModal.closeModal}
|
|
||||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<div>
|
<div>
|
||||||
@@ -837,68 +827,153 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
</label>
|
</label>
|
||||||
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='start_date'
|
name='startDate'
|
||||||
value={formik.values.start_date || ''}
|
value={formik.values.startDate || ''}
|
||||||
onChange={handleStartDateChange}
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('startDate', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.endDate) {
|
||||||
|
const startDate = new Date(value);
|
||||||
|
const endDateObj = new Date(formik.values.endDate);
|
||||||
|
|
||||||
|
if (endDateObj < startDate) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
formik.touched.startDate && !!formik.errors.startDate
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.startDate}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
/>
|
/>
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
<hr className='w-full max-w-3 h-px border-base-content/10'></hr>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='endDate'
|
||||||
value={formik.values.end_date || ''}
|
value={formik.values.endDate || ''}
|
||||||
onChange={handleEndDateChange}
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('endDate', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.startDate) {
|
||||||
|
const startDateObj = new Date(formik.values.startDate);
|
||||||
|
const endDate = new Date(value);
|
||||||
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
(formik.touched.endDate && !!formik.errors.endDate) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.endDate}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
isError={hasDateError}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectInputCheckbox
|
<div>
|
||||||
label='Supplier'
|
<SelectInputCheckbox
|
||||||
placeholder='Pilih Supplier'
|
label='Supplier'
|
||||||
options={supplierOptions}
|
placeholder='Pilih Supplier'
|
||||||
value={formik.values.suppliers}
|
isMulti
|
||||||
onChange={(val) =>
|
options={supplierOptions}
|
||||||
formik.setFieldValue('suppliers', Array.isArray(val) ? val : [])
|
value={
|
||||||
}
|
(formik.values.supplierIds as
|
||||||
onInputChange={setSupplierInputValue}
|
| { value: number; label: string }
|
||||||
onMenuScrollToBottom={loadMoreSuppliers}
|
| { value: number; label: string }[]
|
||||||
isLoading={isLoadingSupplierOptions}
|
| null
|
||||||
isClearable
|
| undefined) || []
|
||||||
className={{ wrapper: 'w-full' }}
|
}
|
||||||
/>
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'supplierIds',
|
||||||
|
Array.isArray(val) ? val : val ? [val] : null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isLoading={isLoadingSupplierOptions}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
formik.touched.supplierIds && !!formik.errors.supplierIds
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.supplierIds as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelectInputRadio
|
<div>
|
||||||
label='Filter Berdasarkan'
|
<SelectInputRadio
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
label='Filter Berdasarkan'
|
||||||
options={dataTypeOptions}
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
value={formik.values.filterBy ?? null}
|
options={dataTypeOptions}
|
||||||
onChange={(val) =>
|
value={
|
||||||
formik.setFieldValue(
|
(formik.values.filterBy as
|
||||||
'filterBy',
|
| { value: string; label: string }
|
||||||
!Array.isArray(val) ? (val ?? undefined) : undefined
|
| { value: string; label: string }[]
|
||||||
)
|
| null
|
||||||
}
|
| undefined) || null
|
||||||
className={{ wrapper: 'w-full' }}
|
}
|
||||||
isClearable
|
onChange={(val) => {
|
||||||
/>
|
formik.setFieldValue(
|
||||||
|
'filterBy',
|
||||||
|
val ? (val as OptionType) : null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isClearable
|
||||||
|
isError={formik.touched.filterBy && !!formik.errors.filterBy}
|
||||||
|
errorMessage={formik.errors.filterBy as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Action Buttons */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
|
||||||
variant='soft'
|
variant='soft'
|
||||||
|
color='none'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
type='reset'
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={hasDateError}
|
type='submit'
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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({
|
||||||
@@ -29,7 +28,6 @@ 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(() => {
|
||||||
@@ -156,7 +154,7 @@ export function DatePicker({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`w-full justify-start text-left font-normal hover:bg-gray-50 ${hasError ? 'border-red-500 focus:ring-red-500' : 'border-gray-200'}`}
|
className='w-full justify-start text-left font-normal border-gray-200 hover:bg-gray-50'
|
||||||
>
|
>
|
||||||
<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,13 +181,6 @@ 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 [preloadedKandang, setPreloadedKandang] = useState<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
|
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
|
||||||
const [documents, setDocuments] = useState<File[]>([]);
|
const [documents, setDocuments] = useState<File[]>([]);
|
||||||
@@ -233,23 +226,15 @@ export function DailyChecklistContent() {
|
|||||||
const rawDate = data.date || '';
|
const rawDate = data.date || '';
|
||||||
setDate(rawDate.length > 10 ? rawDate.slice(0, 10) : rawDate);
|
setDate(rawDate.length > 10 ? rawDate.slice(0, 10) : rawDate);
|
||||||
skipKandangClearRef.current = true;
|
skipKandangClearRef.current = true;
|
||||||
const loadedKandangId = String(data.kandang?.id || '');
|
setKandangId(String(data.kandang?.id || ''));
|
||||||
setKandangId(loadedKandangId);
|
|
||||||
if (data.kandang?.name) {
|
|
||||||
setPreloadedKandang({ id: loadedKandangId, name: data.kandang.name });
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEmptyKandang =
|
const isEmptyKandang =
|
||||||
!!data.empty_kandang || data.category === 'empty_kandang';
|
!!data.empty_kandang || data.category === 'empty_kandang';
|
||||||
setEmptyKandang(isEmptyKandang);
|
setEmptyKandang(isEmptyKandang);
|
||||||
setSelectedCategory(isEmptyKandang ? 'empty_kandang' : data.category);
|
setSelectedCategory(isEmptyKandang ? 'empty_kandang' : data.category);
|
||||||
|
|
||||||
if (
|
if (isEmptyKandang && data.empty_kandang_end_date) {
|
||||||
isEmptyKandang &&
|
const rawEnd = data.empty_kandang_end_date;
|
||||||
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
|
||||||
);
|
);
|
||||||
@@ -799,11 +784,6 @@ export function DailyChecklistContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emptyKandang && !emptyKandangEndDate) {
|
|
||||||
setEmptyKandangEndDateError('Tanggal akhir kandang kosong wajib diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoadingDraft(true);
|
setIsLoadingDraft(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -881,11 +861,6 @@ 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');
|
||||||
@@ -1171,17 +1146,9 @@ export function DailyChecklistContent() {
|
|||||||
<SelectValue placeholder='Pilih kandang' />
|
<SelectValue placeholder='Pilih kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent onScroll={handleKandangScroll}>
|
<SelectContent onScroll={handleKandangScroll}>
|
||||||
{preloadedKandang &&
|
{kandangOptions.map((kandang) => (
|
||||||
!kandangOptions.some(
|
|
||||||
(k) => String(k.value) === preloadedKandang.id
|
|
||||||
) && (
|
|
||||||
<SelectItem value={preloadedKandang.id}>
|
|
||||||
{preloadedKandang.name}
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
{kandangOptions.map((kandang, kandangIdx) => (
|
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={`${kandang.value}-${kandangIdx}`}
|
key={kandang.value}
|
||||||
value={String(kandang.value)}
|
value={String(kandang.value)}
|
||||||
>
|
>
|
||||||
{kandang.label}
|
{kandang.label}
|
||||||
@@ -1253,20 +1220,11 @@ export function DailyChecklistContent() {
|
|||||||
<div className='mt-1.5'>
|
<div className='mt-1.5'>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
date={emptyKandangEndDate}
|
date={emptyKandangEndDate}
|
||||||
onDateChange={(val) => {
|
onDateChange={setEmptyKandangEndDate}
|
||||||
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,7 +60,6 @@ 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 {
|
||||||
@@ -180,9 +179,6 @@ 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,
|
||||||
@@ -209,7 +205,6 @@ 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;
|
||||||
@@ -277,7 +272,6 @@ 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;
|
||||||
@@ -328,7 +322,6 @@ 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);
|
||||||
@@ -784,18 +777,6 @@ 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>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { BaseApiService } from '@/services/api/base';
|
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 { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||||
|
|
||||||
@@ -13,82 +11,6 @@ export class DebtSupplierApiService extends BaseApiService<
|
|||||||
super(basePath);
|
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(
|
async getDebtSupplierReport(
|
||||||
supplier_ids?: string,
|
supplier_ids?: string,
|
||||||
filter_by?: string,
|
filter_by?: string,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { BaseApiService } from '@/services/api/base';
|
import { BaseApiService } from '@/services/api/base';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
|
import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
|
||||||
import { httpClient } from '@/services/http/client';
|
|
||||||
import { formatDate } from '@/lib/helper';
|
|
||||||
import { BalanceMonitoringRow } from '@/types/api/report/balance-monitoring';
|
|
||||||
|
|
||||||
export class FinanceApiService extends BaseApiService<
|
export class FinanceApiService extends BaseApiService<
|
||||||
CustomerPaymentReport,
|
CustomerPaymentReport,
|
||||||
@@ -14,95 +11,6 @@ export class FinanceApiService extends BaseApiService<
|
|||||||
super(basePath);
|
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 getBalanceMonitoringReport(params: {
|
|
||||||
start_date?: string;
|
|
||||||
end_date?: string;
|
|
||||||
customer_ids?: string;
|
|
||||||
sales_ids?: string;
|
|
||||||
filter_by?: string;
|
|
||||||
sort_by?: string;
|
|
||||||
sort_order?: string;
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
}): Promise<BaseApiResponse<BalanceMonitoringRow[]> | undefined> {
|
|
||||||
return await this.customRequest<BaseApiResponse<BalanceMonitoringRow[]>>(
|
|
||||||
'balance-monitoring',
|
|
||||||
{ method: 'GET', params }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCustomerPaymentReport(
|
async getCustomerPaymentReport(
|
||||||
customer_ids?: string,
|
customer_ids?: string,
|
||||||
// TODO: Uncomment when BE is ready
|
// TODO: Uncomment when BE is ready
|
||||||
|
|||||||
+2
-5
@@ -12,11 +12,8 @@ export type BaseDailyChecklist = {
|
|||||||
status: string;
|
status: string;
|
||||||
category: string;
|
category: string;
|
||||||
date: string;
|
date: string;
|
||||||
empty_kandang?: {
|
empty_kandang?: boolean;
|
||||||
id: boolean;
|
empty_kandang_end_date?: string | null;
|
||||||
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,7 +10,6 @@ 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;
|
||||||
@@ -23,7 +22,6 @@ 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,7 +16,6 @@ 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;
|
||||||
@@ -46,7 +45,6 @@ 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;
|
||||||
|
|||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
import { Customer } from '@/services/api/master-data';
|
|
||||||
|
|
||||||
export type BalanceMonitoringRow = {
|
|
||||||
customer: Customer;
|
|
||||||
saldo_awal: number;
|
|
||||||
penjualan_ayam: {
|
|
||||||
ekor: number;
|
|
||||||
kg: number;
|
|
||||||
nominal: number;
|
|
||||||
};
|
|
||||||
penjualan_telur: {
|
|
||||||
butir: number;
|
|
||||||
kg: number;
|
|
||||||
nominal: number;
|
|
||||||
};
|
|
||||||
penjualan_trading: {
|
|
||||||
qty: number;
|
|
||||||
kg: number;
|
|
||||||
nominal: number;
|
|
||||||
};
|
|
||||||
pembayaran: number;
|
|
||||||
aging: number;
|
|
||||||
aging_rata_rata: number;
|
|
||||||
saldo_akhir: number;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user