mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
253 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e1fab9a69 | |||
| ef56f87e45 | |||
| c4827bb810 | |||
| 9abb8b0b58 | |||
| 8d014a8fea | |||
| 3d37fb2ecb | |||
| d60877d391 | |||
| b3b60018bb | |||
| c98a51326f | |||
| 7437e2e584 | |||
| ac6f6ecf78 | |||
| 7f961b2f8b | |||
| a8c02243a4 | |||
| 82dca3b57e | |||
| 94d623d793 | |||
| f76b5b981c | |||
| 8df5af0124 | |||
| 3c175d4586 | |||
| 9350a6bd3e | |||
| 6668c7b1f9 | |||
| ce4f50c92a | |||
| 146192a5b3 | |||
| 27c24e7c82 | |||
| a99a399f09 | |||
| 37d0041a4f | |||
| 3647f1a1ea | |||
| 7b5049165a | |||
| 3839b46edc | |||
| b7f2bca931 | |||
| 802bf77bc5 | |||
| fd7b49ab93 | |||
| 456070491f | |||
| c12beca4d7 | |||
| 910981645b | |||
| 82b5429d02 | |||
| 6c6f739fc0 | |||
| 001dafecb7 | |||
| 4bb3ada779 | |||
| 0b63dcb532 | |||
| 23dd220b2f | |||
| 770c293257 | |||
| 3374ab4779 | |||
| 7a668c0cf9 | |||
| 14151f6f5a | |||
| 0275e66eda | |||
| 9bc5842493 | |||
| 4cad8aba64 | |||
| 7b5af69dd1 | |||
| 2e179b74ba | |||
| fe2a2dfb43 | |||
| 910a36857e | |||
| 58ddd9b991 | |||
| bb9c6ab969 | |||
| ddffdd1b27 | |||
| f097620c4b | |||
| 280d790f0c | |||
| 3a2e74b559 | |||
| b9ef0fa338 | |||
| 6d8cdeffe9 | |||
| 2e36247a1a | |||
| 37cd990b4f | |||
| bdc7ac4d22 | |||
| b6c2f36dd1 | |||
| 10cc4bee72 | |||
| ff6bcf019b | |||
| bb0508d456 | |||
| d6dd5e6709 | |||
| 3c75a7631a | |||
| e3d3e744b0 | |||
| 5767a078d9 | |||
| 67c7e85ba8 | |||
| c5a5582147 | |||
| 46cb8a7d61 | |||
| 0189733dec | |||
| d0c3581f57 | |||
| e7569b7448 | |||
| 69b998a61a | |||
| c50c110005 | |||
| 3775bb6093 | |||
| 3dc64d01db | |||
| 2ed8ecbbb7 | |||
| e5f6ef8a85 | |||
| 7ff0891ad5 | |||
| a9a5098a21 | |||
| 7f9bb8e11d | |||
| bef3f365bb | |||
| a0e8c60082 | |||
| e7f378823c | |||
| ba3cb98e2c | |||
| 7643645643 | |||
| 3b1e7e3b03 | |||
| 725111dc0c | |||
| 073d7eee03 | |||
| cce5a8df43 | |||
| 978067ac6c | |||
| 6255367366 | |||
| af9cb8ec6b | |||
| e0a1922ed4 | |||
| 4b5ad0dcab | |||
| ca62b31aa6 | |||
| 4ec32c51b2 | |||
| cdee616e18 | |||
| 50378a2ee2 | |||
| ab093467c4 | |||
| 79e41d8a6f | |||
| 35001ff422 | |||
| 7026619249 | |||
| 3945142966 | |||
| b19099cea2 | |||
| f65593de25 | |||
| a5f1a6ea75 | |||
| 4e58f20ba3 | |||
| dc41d6ce73 | |||
| 8869c9df2c | |||
| f2b3f2b584 | |||
| 31cea258a7 | |||
| 53d7439300 | |||
| 28a1852de8 | |||
| ff92073d19 | |||
| 6ffc2c2806 | |||
| 9e402e373c | |||
| 2e4c19b714 | |||
| 039c926e2d | |||
| e52ba7b394 | |||
| 90dc7c80f2 | |||
| 15c883ca73 | |||
| 47f74b8842 | |||
| ef9009b304 | |||
| ce25758a17 | |||
| 371b236e25 | |||
| a54dd1fa9e | |||
| 31205a44f9 | |||
| 3c9c55e049 | |||
| 7a4f93cf0c | |||
| a738d58c37 | |||
| 46daed8fc4 | |||
| 29347c24f4 | |||
| a0f603b707 | |||
| 631e3959cd | |||
| 2a340a26f9 | |||
| e75246ff8d | |||
| 64aee33452 | |||
| 1851f0e12f | |||
| 8b3f44708d | |||
| a5fd97a175 | |||
| 2ee5d1f7bd | |||
| 6eb257705f | |||
| 9ea1d06972 | |||
| ff8833b5b3 | |||
| 2dd98fd7e3 | |||
| 76fff98d9d | |||
| 18eeabd353 | |||
| 06b5a97de3 | |||
| 5cccc0b3c6 | |||
| 7ab9518a55 | |||
| ac51229398 | |||
| 5a2532a0fa | |||
| f9d2a875e2 | |||
| 6cf8e463c6 | |||
| 4206408db1 | |||
| ff2ed8757f | |||
| 0c5ee08f90 | |||
| bbf9581d3a | |||
| 5830ab4c67 | |||
| a1a0b71814 | |||
| 2b3b6b9549 | |||
| be3034a94e | |||
| a11d05e720 | |||
| cb454e7eb7 | |||
| a6d6c53069 | |||
| c875ebd951 | |||
| a369386922 | |||
| b3198a44e9 | |||
| b2dfb8fec6 | |||
| d4d77bb13a | |||
| 7dfa5233f3 | |||
| 3d910f78db | |||
| 2dfac0be72 | |||
| afe0d2161d | |||
| 68c13c48c7 | |||
| b9a1e94a29 | |||
| d8c6a90c55 | |||
| 4d01ad7d1d | |||
| c487e7f53e | |||
| a316120a78 | |||
| 9af0537587 | |||
| f668bcecb8 | |||
| a12b09eb5f | |||
| cfb96c45c9 | |||
| 747b0f9c2c | |||
| ee2f530d81 | |||
| 617124efe4 | |||
| c0337f4d67 | |||
| e5dcca3408 | |||
| f2b05856bb | |||
| 5d6aaace86 | |||
| 9dcb3d7269 | |||
| e96bb46cfd | |||
| 37edc957d2 | |||
| 60df577cc6 | |||
| e0e2b0c406 | |||
| 244be32b59 | |||
| 1080a26f93 | |||
| c12bf92723 | |||
| 5c5b49d0a9 | |||
| b7f886b51e | |||
| 587266e23d | |||
| 9293b6321f | |||
| ddfd1206a7 | |||
| 75910960c5 | |||
| aae633edee | |||
| f129329d52 | |||
| 2afcc5d1c9 | |||
| 7f578c5d03 | |||
| 180b129550 | |||
| 8b2277c8c3 | |||
| 68f4562395 | |||
| c374a4a4e9 | |||
| bda66381b8 | |||
| 28adeee7bd | |||
| 727ac8ccdb | |||
| b77a8ef56f | |||
| 50e0ccd9e4 | |||
| e43a25307f | |||
| db4750217e | |||
| 19793cdcd4 | |||
| 15bddc43e2 | |||
| 3b7c7bb13f | |||
| 633cece581 | |||
| e455d203cc | |||
| d2a5229282 | |||
| 2391d6ceeb | |||
| 4bb57ed0a0 | |||
| 5bf3d32636 | |||
| c5a0cfe118 | |||
| 898bbd57ec | |||
| 5b5113de6e | |||
| 267a6f37cc | |||
| 7d4898c266 | |||
| f49822d03d | |||
| aa4da686c6 | |||
| 5a668c469f | |||
| 2ca733de97 | |||
| 8afc1a6381 | |||
| d47142153e | |||
| 188385b638 | |||
| 08aa79a06b | |||
| 16741aaa46 | |||
| 93083c7d2a | |||
| 8333b5138a | |||
| c0ee2013f3 | |||
| 022656cd80 | |||
| 9d3c22fcf3 |
@@ -48,3 +48,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
# rtk
|
# rtk
|
||||||
rtk.exe
|
rtk.exe
|
||||||
|
|
||||||
|
# local specs
|
||||||
|
/local-specs
|
||||||
+21
-2
@@ -30,6 +30,10 @@ default:
|
|||||||
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
||||||
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
||||||
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
||||||
|
- echo "NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV"
|
||||||
|
- echo "NEXT_PUBLIC_HELPDESK_URL=$NEXT_PUBLIC_HELPDESK_URL"
|
||||||
|
- echo "NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL=$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
||||||
|
- echo "NEXT_PUBLIC_S3_PUBLIC_BASE_URL=$NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
||||||
- echo "Building Next.js static export..."
|
- echo "Building Next.js static export..."
|
||||||
- npx next build
|
- npx next build
|
||||||
- |
|
- |
|
||||||
@@ -41,7 +45,11 @@ default:
|
|||||||
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||||
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
||||||
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
||||||
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL"
|
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL",
|
||||||
|
"NEXT_PUBLIC_APP_ENV": "$NEXT_PUBLIC_APP_ENV",
|
||||||
|
"NEXT_PUBLIC_HELPDESK_URL": "$NEXT_PUBLIC_HELPDESK_URL",
|
||||||
|
"NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL": "$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
||||||
|
"NEXT_PUBLIC_S3_PUBLIC_BASE_URL": "NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -142,6 +150,10 @@ build:dev:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
NEXT_PUBLIC_APP_ENV: 'development'
|
||||||
|
NEXT_PUBLIC_HELPDESK_URL: 'https://dev-helpdesk.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dev-dashboard-ho.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com'
|
||||||
|
|
||||||
deploy:dev:
|
deploy:dev:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -170,6 +182,9 @@ build:staging:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
NEXT_PUBLIC_APP_ENV: 'staging'
|
||||||
|
NEXT_PUBLIC_HELPDESK_URL: 'https://stg-helpdesk.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://stg-dashboard-ho.mbugroup.id/'
|
||||||
|
|
||||||
deploy:staging:
|
deploy:staging:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -185,7 +200,7 @@ deploy:staging:
|
|||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# ====== STAGING (Branch production) ======
|
# ====== (Branch production) ======
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
build:production:
|
build:production:
|
||||||
<<: *build_template
|
<<: *build_template
|
||||||
@@ -198,6 +213,10 @@ build:production:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
NEXT_PUBLIC_APP_ENV: 'production'
|
||||||
|
NEXT_PUBLIC_HELPDESK_URL: 'https://helpdesk.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dashboard-ho.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com/'
|
||||||
|
|
||||||
deploy:production:
|
deploy:production:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
|
git add .
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Project-local RTK filters — commit this file with your repo.
|
||||||
|
# Filters here override user-global and built-in filters.
|
||||||
|
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
# Example: suppress build noise from a custom tool
|
||||||
|
# [filters.my-tool]
|
||||||
|
# description = "Compact my-tool output"
|
||||||
|
# match_command = "^my-tool\\s+build"
|
||||||
|
# strip_ansi = true
|
||||||
|
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
|
||||||
|
# max_lines = 30
|
||||||
|
# on_empty = "my-tool: ok"
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
# LTI Web Client
|
||||||
|
|
||||||
|
Next.js 15 (App Router) + React 19 + TypeScript front-end for the LTI ERP system.
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
- **Framework:** Next.js 15.5 (App Router, Turbopack)
|
||||||
|
- **UI:** React 19, Tailwind CSS v4, Radix UI, daisyUI, lucide-react
|
||||||
|
- **State:** zustand
|
||||||
|
- **Forms:** Formik + Yup, react-hook-form
|
||||||
|
- **Data fetching:** axios + SWR (custom `httpClient` / `httpClientFetcher` in `src/services/http`)
|
||||||
|
- **Tables:** @tanstack/react-table
|
||||||
|
- **Reporting:** @react-pdf/renderer, jspdf, exceljs, xlsx, recharts
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
- `npm run dev` — lint + dev server (Turbopack)
|
||||||
|
- `npm run build` — production build
|
||||||
|
- `npm run lint` — ESLint
|
||||||
|
- `npm run typecheck` — `next typegen && tsc --noEmit`
|
||||||
|
- `npm run format` — Prettier
|
||||||
|
- `npm run pre-commit` — format + lint + typecheck + build (Husky pre-commit hook)
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
app/ # Next.js App Router routes (one folder per feature)
|
||||||
|
components/
|
||||||
|
pages/{feature}/ # Page-specific components (mirrors src/app)
|
||||||
|
helper/ # Cross-cutting helpers (e.g. SuspenseHelper)
|
||||||
|
ui/ # Shared UI primitives
|
||||||
|
services/
|
||||||
|
api/ # API service classes (extend BaseApiService)
|
||||||
|
http/ # httpClient / httpClientFetcher
|
||||||
|
hooks/ # Service-level hooks
|
||||||
|
stores/ # zustand stores grouped by domain
|
||||||
|
types/api/ # Request/response types per feature
|
||||||
|
lib/ # Shared helpers (api-helper, formik-helper, utils, validation, …)
|
||||||
|
config/, styles/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature development standard
|
||||||
|
|
||||||
|
**Always follow this order when adding a new feature.** This is a team convention — deviating creates churn in code review.
|
||||||
|
|
||||||
|
1. **Types** — Define payload and response types in `src/types/api/{feature}` (or `{feature}.d.ts` for small features).
|
||||||
|
2. **API service** — Add `src/services/api/{feature}.ts` exporting a class that extends `BaseApiService<T, CreatePayload, UpdatePayload>` from [src/services/api/base.ts](src/services/api/base.ts). Use a subfolder (e.g. `src/services/api/daily-checklist/`) when the feature has multiple resource classes.
|
||||||
|
3. **Page** — Create the route under `src/app/{feature}` and a matching `src/components/pages/{feature}` folder for its components.
|
||||||
|
4. **Component slicing** — Break the page UI into components inside `src/components/pages/{feature}`.
|
||||||
|
5. **Wire up the API** — Consume the service class from step 2 inside the page/components (often via SWR).
|
||||||
|
6. **Detail layout** — When a route reads URL params via `useSearchParams` (e.g. `/feature/detail?id=123`), add `src/app/{feature}/detail/layout.tsx` that wraps `children` in `<SuspenseHelper>` from `@/components/helper/SuspenseHelper`.
|
||||||
|
7. **Shared state** — Use zustand stores in `src/stores/{domain}` when state must cross component boundaries.
|
||||||
|
8. **Helpers** — Reuse from [src/lib](src/lib) first (`api-helper.ts`, `formik-helper.ts`, `utils/`, `validation/`, etc.). Add new helpers there.
|
||||||
|
|
||||||
|
### Reference implementations
|
||||||
|
|
||||||
|
`closing`, `finance`, `expense`, `production`, `inventory`, `marketing`, `master-data`, `purchase`, `report`, `daily-checklist`, `dashboard` — all live in both `src/app/{feature}` and `src/components/pages/{feature}` and follow the standard above.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Path alias `@/` maps to `src/`.
|
||||||
|
- Detail pages that read `useSearchParams` MUST be wrapped in `<SuspenseHelper>` via a `layout.tsx` (see [src/app/finance/detail/layout.tsx](src/app/finance/detail/layout.tsx) for the canonical pattern).
|
||||||
|
- API service classes inherit CRUD methods (`getAll`, `getSingle`, etc.) from `BaseApiService` — extend the class for feature-specific endpoints rather than calling `httpClient` directly from components.
|
||||||
|
- Pre-commit runs format + lint + typecheck + build; do not bypass with `--no-verify`.
|
||||||
|
|
||||||
|
## Table filter persistence pattern
|
||||||
|
|
||||||
|
Data tables across all modules (master-data, inventory, finance, purchase, etc.) use `useTableFilter` with `persist: true` to persist filter state in localStorage. This allows users' search, pagination, and filter choices to survive page refreshes.
|
||||||
|
|
||||||
|
**Three core principles (apply to all table components):**
|
||||||
|
|
||||||
|
1. **Set formik initialValues from tableFilterState** (not hardcoded defaults)
|
||||||
|
- Ensures the filter modal displays currently active filters when opened
|
||||||
|
- Initialize directly from persisted state: `location: tableFilterState.locationFilter`
|
||||||
|
|
||||||
|
2. **Pass `true` as last parameter to updateFilter calls**
|
||||||
|
- `updateFilter('fieldName', value, true)` immediately persists to localStorage
|
||||||
|
- Resets pagination to page 1 when filters change (via SWR revalidation)
|
||||||
|
- Apply to: search handlers, filter form submissions, reset handlers
|
||||||
|
|
||||||
|
3. **Create custom formikResetHandler function**
|
||||||
|
- Call `resetFilter()` (single call — resets all `useTableFilter` state to defaults)
|
||||||
|
- Reset any local error state (e.g. `setHasDateError(false)`, dismiss toasts)
|
||||||
|
- Call `formik.resetForm({ values: { ...defaults } })` to sync formik to defaults
|
||||||
|
- Call `filterModal.closeModal()` at the end
|
||||||
|
- Attach to form `onReset` handler (not `formik.handleReset`)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
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` 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
|
||||||
|
// ✅ Good: plain derivation
|
||||||
|
const data = isResponseSuccess(response) ? (response.data ?? []) : [];
|
||||||
|
const meta =
|
||||||
|
isResponseSuccess(response) && response.meta ? response.meta : null;
|
||||||
|
|
||||||
|
// ❌ Avoid: useMemo for trivial conditional access
|
||||||
|
const data = useMemo(
|
||||||
|
() => (isResponseSuccess(response) ? (response.data ?? []) : []),
|
||||||
|
[response]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Good: simple handler
|
||||||
|
const handleChange = (val) => setFieldValue('location', val);
|
||||||
|
|
||||||
|
// ❌ Avoid: unnecessary useCallback
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(val) => setFieldValue('location', val),
|
||||||
|
[setFieldValue]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `useMemo` IS justified for large column definition arrays (TanStack Table re-processes on every render)
|
||||||
|
|
||||||
|
**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:
|
||||||
|
|
||||||
|
- `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
|
||||||
|
const { state: tableFilterState, updateFilter, ... } = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
customers: OptionType<number>[]; // multi-select → serializes as CSV
|
||||||
|
location?: OptionType<string>; // single-select → serializes as value string
|
||||||
|
filterBy?: OptionType<string>; // single-select radio
|
||||||
|
}>({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
customers: [],
|
||||||
|
location: undefined,
|
||||||
|
filterBy: undefined,
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
customers: 'customer_ids', // serializes OptionType[] → "1,2,3"
|
||||||
|
location: 'location_id', // serializes OptionType → "abc"
|
||||||
|
filterBy: 'filter_by',
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'my-table',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize formik directly from tableFilterState (no hardcoded defaults)
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues: {
|
||||||
|
customers: tableFilterState.customers,
|
||||||
|
location: tableFilterState.location,
|
||||||
|
filterBy: tableFilterState.filterBy,
|
||||||
|
},
|
||||||
|
...
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use formik values directly — no computed helpers needed
|
||||||
|
<SelectInputCheckbox value={formik.values.customers} onChange={(val) => formik.setFieldValue('customers', Array.isArray(val) ? val : [])} />
|
||||||
|
<SelectInput value={formik.values.location} onChange={(val) => formik.setFieldValue('location', val)} />
|
||||||
|
<SelectInputRadio value={formik.values.filterBy ?? null} onChange={(val) => formik.setFieldValue('filterBy', !Array.isArray(val) ? (val ?? undefined) : undefined)} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**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:**
|
||||||
|
|
||||||
|
- Any data table component across any module that needs persistent filters
|
||||||
|
- Master-data tables, inventory lists, finance reports, purchase orders, etc.
|
||||||
|
- Whenever users' filter/search/pagination choices should survive page refreshes
|
||||||
|
|
||||||
|
**Reference implementations:**
|
||||||
|
|
||||||
|
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
||||||
|
- `BalanceMonitoringTab` in `src/components/pages/report/finance/tab/` — multi-select + radio + date range
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
Data tables use TanStack Table's `SortingState` wired to `useTableFilter` so that sorting triggers a server re-fetch rather than client-side reordering.
|
||||||
|
|
||||||
|
**Four-part wiring:**
|
||||||
|
|
||||||
|
1. **Local sort state** — `const [sorting, setSorting] = useState<SortingState>([]);`
|
||||||
|
|
||||||
|
2. **`useTableFilter` config** — Add `sort_by` and `order_by` to `initial` and `paramMap`. The `paramMap` key is the internal name; the value is the query param name sent to the server (they can differ, e.g. `order_by` → `sort_order`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
initial: { sort_by: '', order_by: '' }
|
||||||
|
paramMap: { sort_by: 'sort_by', order_by: 'sort_order' }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **`useEffect` sync** — Watches `sorting` and pushes changes into `useTableFilter`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
useEffect(() => {
|
||||||
|
if (sorting.length > 0) {
|
||||||
|
updateFilter('sort_by', sorting[0].id, true);
|
||||||
|
updateFilter('order_by', sorting[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '');
|
||||||
|
updateFilter('order_by', '');
|
||||||
|
}
|
||||||
|
}, [sorting]);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **SWR key** — SWR uses `getTableFilterToQueryString()` as its key, so any filter change (including sort) automatically re-fetches with the new query params. TanStack Table's built-in client sorting is effectively disabled; the server does the sorting.
|
||||||
|
|
||||||
|
**Pass `sorting`, `setSorting`, and `manualSorting` to `<Table>`:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Table sorting={sorting} setSorting={handleSortingChange} manualSorting={true} ... />
|
||||||
|
```
|
||||||
|
|
||||||
|
`manualSorting={true}` is required — without it TanStack Table still applies its own client-side sort pass on top of the server-sorted data, producing incorrect order.
|
||||||
|
|
||||||
|
**Reference implementation:** `MarketingTable` in [src/components/pages/marketing/MarketingTable.tsx](src/components/pages/marketing/MarketingTable.tsx).
|
||||||
|
|
||||||
|
## Server-side file export pattern
|
||||||
|
|
||||||
|
All file exports (Excel, PDF, or any format) must use **server-side generation** — the server returns a binary blob and the browser triggers a download. Never generate files client-side with `xlsx`, `@react-pdf/renderer`, `jspdf`, or similar libraries.
|
||||||
|
|
||||||
|
**Rule:** Export methods live in the API service class, not in components. Components only build the query string and call the service method.
|
||||||
|
|
||||||
|
### Service method (in `src/services/api/{feature}.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async exportToExcel(initialQueryString: string) {
|
||||||
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('export', 'excel'); // or 'pdf', 'csv', etc.
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
|
const res = await httpClient<Blob>(`${this.basePath}?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', `filename-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Change `export=excel` → `export=pdf` (and the file extension) for PDF exports.
|
||||||
|
- Add one method per format; keep them side-by-side in the same service class.
|
||||||
|
|
||||||
|
### Component handler (in the page/tab component)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const handleExportExcel = useCallback(async () => {
|
||||||
|
setIsExcelExportLoading(true);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filterParams.foo) params.set('foo', filterParams.foo);
|
||||||
|
// ... map all active filter params ...
|
||||||
|
|
||||||
|
await FeatureApi.exportToExcel(params.toString());
|
||||||
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
|
} finally {
|
||||||
|
setIsExcelExportLoading(false);
|
||||||
|
}
|
||||||
|
}, [filterParams, searchValue]);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Do **not** fetch all rows into the component to build the file — delegate entirely to the service method.
|
||||||
|
- Do **not** import `xlsx`, `@react-pdf/renderer`, `jspdf`, `exceljs` in page/tab components.
|
||||||
|
|
||||||
|
**Reference implementation:** `MarketingReportApiService.exportDailyMarketingToExcel` / `exportDailyMarketingToPDF` in [src/services/api/report/marketing-report.ts](src/services/api/report/marketing-report.ts), consumed by [src/components/pages/report/marketing/tab/DailyMarketingTab.tsx](src/components/pages/report/marketing/tab/DailyMarketingTab.tsx).
|
||||||
|
|
||||||
|
<!-- rtk-instructions v2 -->
|
||||||
|
|
||||||
|
# RTK (Rust Token Killer) - Token-Optimized Commands
|
||||||
|
|
||||||
|
## Golden Rule
|
||||||
|
|
||||||
|
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
|
||||||
|
|
||||||
|
**Important**: Even in command chains with `&&`, use `rtk`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ Wrong
|
||||||
|
git add . && git commit -m "msg" && git push
|
||||||
|
|
||||||
|
# ✅ Correct
|
||||||
|
rtk git add . && rtk git commit -m "msg" && rtk git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## RTK Commands by Workflow
|
||||||
|
|
||||||
|
### Build & Compile (80-90% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk cargo build # Cargo build output
|
||||||
|
rtk cargo check # Cargo check output
|
||||||
|
rtk cargo clippy # Clippy warnings grouped by file (80%)
|
||||||
|
rtk tsc # TypeScript errors grouped by file/code (83%)
|
||||||
|
rtk lint # ESLint/Biome violations grouped (84%)
|
||||||
|
rtk prettier --check # Files needing format only (70%)
|
||||||
|
rtk next build # Next.js build with route metrics (87%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test (60-99% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk cargo test # Cargo test failures only (90%)
|
||||||
|
rtk go test # Go test failures only (90%)
|
||||||
|
rtk jest # Jest failures only (99.5%)
|
||||||
|
rtk vitest # Vitest failures only (99.5%)
|
||||||
|
rtk playwright test # Playwright failures only (94%)
|
||||||
|
rtk pytest # Python test failures only (90%)
|
||||||
|
rtk rake test # Ruby test failures only (90%)
|
||||||
|
rtk rspec # RSpec test failures only (60%)
|
||||||
|
rtk test <cmd> # Generic test wrapper - failures only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git (59-80% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk git status # Compact status
|
||||||
|
rtk git log # Compact log (works with all git flags)
|
||||||
|
rtk git diff # Compact diff (80%)
|
||||||
|
rtk git show # Compact show (80%)
|
||||||
|
rtk git add # Ultra-compact confirmations (59%)
|
||||||
|
rtk git commit # Ultra-compact confirmations (59%)
|
||||||
|
rtk git push # Ultra-compact confirmations
|
||||||
|
rtk git pull # Ultra-compact confirmations
|
||||||
|
rtk git branch # Compact branch list
|
||||||
|
rtk git fetch # Compact fetch
|
||||||
|
rtk git stash # Compact stash
|
||||||
|
rtk git worktree # Compact worktree
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
|
||||||
|
|
||||||
|
### GitHub (26-87% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk gh pr view <num> # Compact PR view (87%)
|
||||||
|
rtk gh pr checks # Compact PR checks (79%)
|
||||||
|
rtk gh run list # Compact workflow runs (82%)
|
||||||
|
rtk gh issue list # Compact issue list (80%)
|
||||||
|
rtk gh api # Compact API responses (26%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript Tooling (70-90% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk pnpm list # Compact dependency tree (70%)
|
||||||
|
rtk pnpm outdated # Compact outdated packages (80%)
|
||||||
|
rtk pnpm install # Compact install output (90%)
|
||||||
|
rtk npm run <script> # Compact npm script output
|
||||||
|
rtk npx <cmd> # Compact npx command output
|
||||||
|
rtk prisma # Prisma without ASCII art (88%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files & Search (60-75% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk ls <path> # Tree format, compact (65%)
|
||||||
|
rtk read <file> # Code reading with filtering (60%)
|
||||||
|
rtk grep <pattern> # Search grouped by file (75%)
|
||||||
|
rtk find <pattern> # Find grouped by directory (70%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Analysis & Debug (70-90% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk err <cmd> # Filter errors only from any command
|
||||||
|
rtk log <file> # Deduplicated logs with counts
|
||||||
|
rtk json <file> # JSON structure without values
|
||||||
|
rtk deps # Dependency overview
|
||||||
|
rtk env # Environment variables compact
|
||||||
|
rtk summary <cmd> # Smart summary of command output
|
||||||
|
rtk diff # Ultra-compact diffs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infrastructure (85% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk docker ps # Compact container list
|
||||||
|
rtk docker images # Compact image list
|
||||||
|
rtk docker logs <c> # Deduplicated logs
|
||||||
|
rtk kubectl get # Compact resource list
|
||||||
|
rtk kubectl logs # Deduplicated pod logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network (65-70% savings)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk curl <url> # Compact HTTP responses (70%)
|
||||||
|
rtk wget <url> # Compact download output (65%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meta Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk gain # View token savings statistics
|
||||||
|
rtk gain --history # View command history with savings
|
||||||
|
rtk discover # Analyze Claude Code sessions for missed RTK usage
|
||||||
|
rtk proxy <cmd> # Run command without filtering (for debugging)
|
||||||
|
rtk init # Add RTK instructions to CLAUDE.md
|
||||||
|
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token Savings Overview
|
||||||
|
|
||||||
|
| Category | Commands | Typical Savings |
|
||||||
|
| ---------------- | ------------------------------ | --------------- |
|
||||||
|
| Tests | vitest, playwright, cargo test | 90-99% |
|
||||||
|
| Build | next, tsc, lint, prettier | 70-87% |
|
||||||
|
| Git | status, log, diff, add, commit | 59-80% |
|
||||||
|
| GitHub | gh pr, gh run, gh issue | 26-87% |
|
||||||
|
| Package Managers | pnpm, npm, npx | 70-90% |
|
||||||
|
| Files | ls, read, grep, find | 60-75% |
|
||||||
|
| Infrastructure | docker, kubectl | 85% |
|
||||||
|
| Network | curl, wget | 65-70% |
|
||||||
|
|
||||||
|
Overall average: **60-90% token reduction** on common development operations.
|
||||||
|
|
||||||
|
<!-- /rtk-instructions -->
|
||||||
@@ -15,8 +15,8 @@ const ExpenseDetailPage = () => {
|
|||||||
const expenseId = searchParams.get('expenseId');
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
expenseId,
|
['expense-detail', expenseId],
|
||||||
(id: number) => ExpenseApi.getSingle(id)
|
([_, id]) => ExpenseApi.getSingle(Number(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!expenseId) {
|
if (!expenseId) {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { SystemConfigContent } from '@/figma-make/components/pages/master-data/system-config/SystemConfigContent';
|
||||||
|
|
||||||
|
const SystemConfigPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<SystemConfigContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemConfigPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -226,7 +226,7 @@ const Pagination = ({
|
|||||||
|
|
||||||
const PageInfo = () => (
|
const PageInfo = () => (
|
||||||
<span className='text-nowrap text-sm font-medium text-base-content/50'>
|
<span className='text-nowrap text-sm font-medium text-base-content/50'>
|
||||||
Page {currentPage} of {totalPages}
|
Total Item: {totalItems} | Page {currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ const Table = <TData extends object>({
|
|||||||
const tableOptions: TableOptions<TData> = {
|
const tableOptions: TableOptions<TData> = {
|
||||||
columns,
|
columns,
|
||||||
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
||||||
|
defaultColumn: { sortDescFirst: false },
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
|||||||
+14
-11
@@ -6,6 +6,7 @@ export interface TabItem {
|
|||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
content?: ReactNode;
|
content?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
hide?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsProps
|
export interface TabsProps
|
||||||
@@ -122,17 +123,19 @@ const Tabs = ({
|
|||||||
>
|
>
|
||||||
<div className={getSideContentClasses()}>
|
<div className={getSideContentClasses()}>
|
||||||
<div role='tablist' className={getTabsClasses()}>
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
{tabs.map(({ id, label, disabled }) => (
|
{tabs.map(({ id, label, disabled, hide }) =>
|
||||||
<button
|
hide ? null : (
|
||||||
key={id}
|
<button
|
||||||
role='tab'
|
key={id}
|
||||||
className={getTabClasses(id === activeTabId, disabled)}
|
role='tab'
|
||||||
onClick={() => !disabled && handleTabChange(id)}
|
className={getTabClasses(id === activeTabId, disabled)}
|
||||||
disabled={disabled}
|
onClick={() => !disabled && handleTabChange(id)}
|
||||||
>
|
disabled={disabled}
|
||||||
{label}
|
>
|
||||||
</button>
|
{label}
|
||||||
))}
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{sideContent && sideContent}
|
{sideContent && sideContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ const useSelect = <T,>(
|
|||||||
|
|
||||||
const qs = new URLSearchParams({
|
const qs = new URLSearchParams({
|
||||||
...(params ?? {}),
|
...(params ?? {}),
|
||||||
[searchKey]: inputValue ?? '',
|
[searchKey ? searchKey : 'search']: inputValue ?? '',
|
||||||
[pageKey]: String(pageIndex + 1),
|
[pageKey]: String(pageIndex + 1),
|
||||||
[limitKey]: String(limit),
|
[limitKey]: String(limit),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
secondaryButton={
|
secondaryButton={
|
||||||
secondaryButton
|
secondaryButton
|
||||||
? {
|
? {
|
||||||
|
...secondaryButton,
|
||||||
text: secondaryButton?.text ?? 'Tidak',
|
text: secondaryButton?.text ?? 'Tidak',
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
if (secondaryButton && secondaryButton?.onClick) {
|
if (secondaryButton && secondaryButton?.onClick) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -10,16 +10,14 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont
|
|||||||
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
|
||||||
|
|
||||||
interface ExpenseDetailProps {
|
interface ExpenseDetailProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||||
|
const router = useRouter();
|
||||||
const [activeTab, setActiveTab] = useState<string>('request');
|
const [activeTab, setActiveTab] = useState<string>('request');
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const returnTo = getExpenseListReturnTo(searchParams);
|
|
||||||
|
|
||||||
const expenseDetailTabs = useMemo(() => {
|
const expenseDetailTabs = useMemo(() => {
|
||||||
const validTabs = [
|
const validTabs = [
|
||||||
@@ -50,8 +48,8 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
<section className='w-full max-w-full pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href={returnTo}
|
|
||||||
variant='link'
|
variant='link'
|
||||||
|
onClick={router.back}
|
||||||
className='w-fit p-0 text-primary'
|
className='w-fit p-0 text-primary'
|
||||||
>
|
>
|
||||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
@@ -26,7 +28,7 @@ import {
|
|||||||
UploadRequestDocumentsFormSchema,
|
UploadRequestDocumentsFormSchema,
|
||||||
UploadRequestDocumentsFormValues,
|
UploadRequestDocumentsFormValues,
|
||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
@@ -46,6 +48,11 @@ const ExpenseRequestContent = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const returnTo = getExpenseListReturnTo(searchParams);
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const refreshExpense = () => {
|
||||||
|
mutate((key) => Array.isArray(key) && key[0] === 'expense-detail');
|
||||||
|
};
|
||||||
|
|
||||||
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
||||||
useApprovalSteps({
|
useApprovalSteps({
|
||||||
@@ -95,17 +102,24 @@ const ExpenseRequestContent = ({
|
|||||||
!isLatestApprovalRejected &&
|
!isLatestApprovalRejected &&
|
||||||
initialValues?.latest_approval.step_number === 4;
|
initialValues?.latest_approval.step_number === 4;
|
||||||
|
|
||||||
|
const isExpensePaidOff = initialValues?.is_paid;
|
||||||
|
|
||||||
|
const showPaidOffButton =
|
||||||
|
!isExpensePaidOff && (initialValues?.latest_approval.step_number ?? 0) >= 4;
|
||||||
|
|
||||||
// Modal hooks
|
// Modal hooks
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const completeModal = useModal();
|
const completeModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const paidOffModal = useModal();
|
||||||
|
|
||||||
// Modal loading state
|
// Modal loading state
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isPaidOffLoading, setIsPaidOffLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
@@ -146,7 +160,31 @@ const ExpenseRequestContent = ({
|
|||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paidOffClickHandler = () => {
|
||||||
|
paidOffModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
// Modal confirm click handler
|
// Modal confirm click handler
|
||||||
|
const confirmationModalPaidOffClickHandler = async () => {
|
||||||
|
setIsPaidOffLoading(true);
|
||||||
|
|
||||||
|
const paidOffResponse = await ExpenseApi.setExpensePaidOff(
|
||||||
|
initialValues?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(paidOffResponse)) {
|
||||||
|
toast.success('Berhasil menandai biaya operasional sebagai lunas!');
|
||||||
|
refreshExpense();
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
'Gagal menandai biaya operasional sebagai lunas!: ' +
|
||||||
|
paidOffResponse?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
setIsPaidOffLoading(false);
|
||||||
|
};
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -388,6 +426,24 @@ const ExpenseRequestContent = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPaidOffButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='success'
|
||||||
|
onClick={paidOffClickHandler}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Tandai Lunas
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.expense.update'>
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
@@ -533,6 +589,19 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status Lunas</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
<StatusBadge
|
||||||
|
color={initialValues?.is_paid ? 'primary' : 'warning'}
|
||||||
|
text={initialValues?.is_paid ? 'Lunas' : 'Belum Lunas'}
|
||||||
|
className={{
|
||||||
|
badge: 'w-fit whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Dokumen Pengajuan</th>
|
<th>Dokumen Pengajuan</th>
|
||||||
<th>:</th>
|
<th>:</th>
|
||||||
@@ -548,21 +617,15 @@ const ExpenseRequestContent = ({
|
|||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
{initialValues?.documents.map(
|
{initialValues?.documents.map(
|
||||||
(requestDocument, requestDocumentIdx) => {
|
(requestDocument, requestDocumentIdx) => {
|
||||||
const path = requestDocument.path.startsWith(
|
|
||||||
'/'
|
|
||||||
)
|
|
||||||
? requestDocument.path.slice(1)
|
|
||||||
: requestDocument.path;
|
|
||||||
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
|
|
||||||
return (
|
return (
|
||||||
<li key={requestDocumentIdx}>
|
<li key={requestDocumentIdx}>
|
||||||
<Link
|
<Link
|
||||||
href={documentUrl}
|
href={requestDocument.path}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-blue-500 underline'
|
className='text-blue-500 underline'
|
||||||
>
|
>
|
||||||
{requestDocument.path}{' '}
|
{requestDocument.name}{' '}
|
||||||
<Icon
|
<Icon
|
||||||
icon='cuida:open-in-new-tab-outline'
|
icon='cuida:open-in-new-tab-outline'
|
||||||
width={12}
|
width={12}
|
||||||
@@ -758,6 +821,21 @@ const ExpenseRequestContent = ({
|
|||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={paidOffModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin menandai biaya operasional ini sebagai lunas?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isPaidOffLoading,
|
||||||
|
onClick: confirmationModalPaidOffClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,26 +3,60 @@ import * as yup from 'yup';
|
|||||||
export type ExpensesFilterType = {
|
export type ExpensesFilterType = {
|
||||||
transaction_date: string | null;
|
transaction_date: string | null;
|
||||||
realization_date: string | null;
|
realization_date: string | null;
|
||||||
location_id: string | null;
|
location: { value: number; label: string } | null;
|
||||||
vendor_id: string | null;
|
vendor: { value: number; label: string } | null;
|
||||||
|
category: { value: string; label: string } | null;
|
||||||
|
approval_status: { value: string; label: string } | null;
|
||||||
|
realization_status: { value: string; label: string } | null;
|
||||||
|
project_flock: { value: number; label: string } | null;
|
||||||
|
project_flock_kandang: { value: number; label: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpensesFilterSchema = yup.object({
|
export const ExpensesFilterSchema = yup.object({
|
||||||
transaction_date: yup.string().nullable(),
|
transaction_date: yup.string().nullable(),
|
||||||
realization_date: yup
|
realization_date: yup.string().nullable(),
|
||||||
.string()
|
location: yup
|
||||||
.nullable()
|
.object({
|
||||||
.test(
|
value: yup.number().required(),
|
||||||
'is-greater-or-equal-transaction',
|
label: yup.string().required(),
|
||||||
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
|
})
|
||||||
function (value) {
|
.nullable(),
|
||||||
const { transaction_date } = this.parent;
|
vendor: yup
|
||||||
if (!transaction_date || !value) return true;
|
.object({
|
||||||
return new Date(value) >= new Date(transaction_date);
|
value: yup.number().required(),
|
||||||
}
|
label: yup.string().required(),
|
||||||
),
|
})
|
||||||
location_id: yup.string().nullable(),
|
.nullable(),
|
||||||
vendor_id: yup.string().nullable(),
|
category: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
approval_status: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
realization_status: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
project_flock: yup
|
||||||
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
project_flock_kandang: yup
|
||||||
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject } from 'react';
|
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -11,8 +11,11 @@ import SelectInput from '@/components/input/SelectInput';
|
|||||||
|
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import {
|
import {
|
||||||
ExpensesFilterSchema,
|
ExpensesFilterSchema,
|
||||||
ExpensesFilterValues,
|
ExpensesFilterValues,
|
||||||
@@ -31,64 +34,143 @@ const ExpensesFilterModal = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: ExpensesFilterModalProps) => {
|
}: ExpensesFilterModalProps) => {
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState<string>(
|
||||||
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
|
);
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categoryOptions = [
|
||||||
|
{ value: 'BOP', label: 'BOP' },
|
||||||
|
{ value: 'NON-BOP', label: 'NON-BOP' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const approvalStatusOptions = [
|
||||||
|
{ value: 'HEAD_AREA', label: 'Approval Head Area' },
|
||||||
|
{ value: 'UNIT_VICE_PRESIDENT', label: 'Approval Unit Vice President' },
|
||||||
|
{ value: 'FINANCE', label: 'Approval Finance' },
|
||||||
|
{ value: 'REALISASI', label: 'Realisasi' },
|
||||||
|
{ value: 'SELESAI', label: 'Selesai' },
|
||||||
|
{ value: 'DITOLAK', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const realizationStatusOptions = [
|
||||||
|
{ value: 'NOT_REALIZED', label: 'Belum Realisasi' },
|
||||||
|
{ value: 'REALIZED', label: 'Sudah Realisasi' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: vendorOptions,
|
options: vendorOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
loadMore: loadMoreVendors,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationId || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<ExpensesFilterValues>({
|
const formik = useFormik<ExpensesFilterValues>({
|
||||||
|
enableReinitialize: true,
|
||||||
initialValues: initialValues || {
|
initialValues: initialValues || {
|
||||||
transaction_date: null,
|
transaction_date: null,
|
||||||
realization_date: null,
|
realization_date: null,
|
||||||
location_id: null,
|
location: null,
|
||||||
vendor_id: null,
|
vendor: null,
|
||||||
|
category: null,
|
||||||
|
approval_status: null,
|
||||||
|
realization_status: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
validationSchema: ExpensesFilterSchema,
|
validationSchema: ExpensesFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
onSubmit?.(values);
|
onSubmit?.(values);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
onReset?.();
|
|
||||||
closeModalHandler();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const locationValue = formik.values.location_id
|
useEffect(() => {
|
||||||
? locationOptions.find(
|
setSelectedLocationId(
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
) || null
|
);
|
||||||
: null;
|
}, [initialValues?.location]);
|
||||||
|
|
||||||
const vendorValue = formik.values.vendor_id
|
const { resetForm } = formik;
|
||||||
? vendorOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.vendor_id
|
const formikResetHandler = useCallback(() => {
|
||||||
) || null
|
resetForm({
|
||||||
: null;
|
values: {
|
||||||
|
transaction_date: null,
|
||||||
|
realization_date: null,
|
||||||
|
location: null,
|
||||||
|
vendor: null,
|
||||||
|
category: null,
|
||||||
|
approval_status: null,
|
||||||
|
realization_status: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSelectedLocationId('');
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset, closeModalHandler]);
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const locationId =
|
const value = val as OptionType | null;
|
||||||
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
formik.setFieldValue('location', value);
|
||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedLocationId(value?.value ? String(value.value) : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const vendorId =
|
formik.setFieldValue('vendor', val as OptionType | null);
|
||||||
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
|
||||||
formik.setFieldValue('vendor_id', vendorId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -98,7 +180,7 @@ const ExpensesFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formikResetHandler}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -121,49 +203,41 @@ const ExpensesFilterModal = ({
|
|||||||
|
|
||||||
{/* 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 className='flex flex-col'>
|
<DateInput
|
||||||
<span className='py-2 text-xs font-semibold'>Tanggal</span>
|
name='transaction_date'
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
label='Tanggal Transaksi'
|
||||||
<DateInput
|
placeholder='Tanggal Transaksi'
|
||||||
name='transaction_date'
|
value={formik.values.transaction_date || ''}
|
||||||
placeholder='Tanggal Transaksi'
|
onChange={formik.handleChange}
|
||||||
value={formik.values.transaction_date || ''}
|
onBlur={formik.handleBlur}
|
||||||
onChange={formik.handleChange}
|
isError={
|
||||||
onBlur={formik.handleBlur}
|
formik.touched.transaction_date &&
|
||||||
isError={
|
!!formik.errors.transaction_date
|
||||||
formik.touched.transaction_date &&
|
}
|
||||||
!!formik.errors.transaction_date
|
/>
|
||||||
}
|
|
||||||
/>
|
<DateInput
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
name='realization_date'
|
||||||
<DateInput
|
label='Tanggal Realisasi'
|
||||||
name='realization_date'
|
placeholder='Tanggal Realisasi'
|
||||||
placeholder='Tanggal Realisasi'
|
value={formik.values.realization_date || ''}
|
||||||
value={formik.values.realization_date || ''}
|
onChange={formik.handleChange}
|
||||||
onChange={formik.handleChange}
|
onBlur={formik.handleBlur}
|
||||||
onBlur={formik.handleBlur}
|
isError={
|
||||||
isError={
|
formik.touched.realization_date &&
|
||||||
formik.touched.realization_date &&
|
!!formik.errors.realization_date
|
||||||
!!formik.errors.realization_date
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{formik.touched.realization_date &&
|
|
||||||
formik.errors.realization_date && (
|
|
||||||
<span className='text-xs text-error'>
|
|
||||||
{formik.errors.realization_date}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationValue}
|
value={formik.values.location}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -173,14 +247,87 @@ const ExpensesFilterModal = ({
|
|||||||
label='Vendor'
|
label='Vendor'
|
||||||
placeholder='Pilih Vendor'
|
placeholder='Pilih Vendor'
|
||||||
options={vendorOptions}
|
options={vendorOptions}
|
||||||
value={vendorValue}
|
value={formik.values.vendor}
|
||||||
onChange={vendorChangeHandler}
|
onChange={vendorChangeHandler}
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
isLoading={isLoadingVendorOptions}
|
isLoading={isLoadingVendorOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreVendors}
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={categoryOptions}
|
||||||
|
value={formik.values.category}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('category', val as OptionType | null)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status BOP'
|
||||||
|
placeholder='Pilih Status BOP'
|
||||||
|
options={approvalStatusOptions}
|
||||||
|
value={formik.values.approval_status}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('approval_status', val as OptionType | null)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status Pencairan'
|
||||||
|
placeholder='Pilih Status Pencairan'
|
||||||
|
options={realizationStatusOptions}
|
||||||
|
value={formik.values.realization_status}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'realization_status',
|
||||||
|
val as OptionType | null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
options={projectFlockOptions}
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('project_flock', val as OptionType | null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
val as OptionType | null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, {
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
useCallback,
|
import {
|
||||||
useEffect,
|
CellContext,
|
||||||
useMemo,
|
ColumnDef,
|
||||||
useRef,
|
SortingState,
|
||||||
useState,
|
Updater,
|
||||||
} from 'react';
|
} from '@tanstack/react-table';
|
||||||
import { CellContext, ColumnDef } 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';
|
||||||
@@ -39,7 +38,7 @@ import PopoverContent from '@/components/popover/PopoverContent';
|
|||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import {
|
import {
|
||||||
FinanceTableFilterSchema,
|
FinanceTableFilterSchema,
|
||||||
FinanceTableFilterValues,
|
FinanceTableFilterValues,
|
||||||
@@ -176,9 +175,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FinanceTable = () => {
|
const FinanceTable = () => {
|
||||||
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
|
||||||
const previousPathRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -187,14 +183,18 @@ const FinanceTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
transactionTypes: '',
|
transactionTypes: '',
|
||||||
bankIds: '',
|
bankIds: '',
|
||||||
customerIds: '',
|
customerIds: '',
|
||||||
supplierIds: '',
|
supplierIds: '',
|
||||||
sortBy: '',
|
sort_by: '',
|
||||||
|
orderBy: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
|
bankNames: '',
|
||||||
|
customerNames: '',
|
||||||
|
supplierNames: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -203,10 +203,14 @@ const FinanceTable = () => {
|
|||||||
bankIds: 'bank_ids',
|
bankIds: 'bank_ids',
|
||||||
customerIds: 'customer_ids',
|
customerIds: 'customer_ids',
|
||||||
supplierIds: 'supplier_ids',
|
supplierIds: 'supplier_ids',
|
||||||
sortBy: 'sort_date',
|
sort_by: 'sort_by',
|
||||||
|
orderBy: 'sort_order',
|
||||||
startDate: 'start_date',
|
startDate: 'start_date',
|
||||||
endDate: 'end_date',
|
endDate: 'end_date',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: ['bankNames', 'customerNames', 'supplierNames'],
|
||||||
|
persist: true,
|
||||||
|
storeName: 'finance-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -235,7 +239,7 @@ const FinanceTable = () => {
|
|||||||
// ===== Formik for Filter =====
|
// ===== Formik for Filter =====
|
||||||
const filterFormik = useFormik<FinanceTableFilterValues>({
|
const filterFormik = useFormik<FinanceTableFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
search: searchValue,
|
search: tableFilterState.search || '',
|
||||||
transaction_types: '',
|
transaction_types: '',
|
||||||
bank_ids: '',
|
bank_ids: '',
|
||||||
customer_ids: '',
|
customer_ids: '',
|
||||||
@@ -245,29 +249,48 @@ const FinanceTable = () => {
|
|||||||
end_date: '',
|
end_date: '',
|
||||||
},
|
},
|
||||||
validationSchema: FinanceTableFilterSchema,
|
validationSchema: FinanceTableFilterSchema,
|
||||||
enableReinitialize: true,
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
onSubmit: (values) => {
|
updateFilter('search', values.search, true);
|
||||||
updateFilter('search', values.search);
|
updateFilter('transactionTypes', values.transaction_types, true);
|
||||||
setSearchValue(values.search);
|
updateFilter('bankIds', values.bank_ids, true);
|
||||||
updateFilter('transactionTypes', values.transaction_types);
|
updateFilter('customerIds', values.customer_ids, true);
|
||||||
updateFilter('bankIds', values.bank_ids);
|
updateFilter('supplierIds', values.supplier_ids, true);
|
||||||
updateFilter('customerIds', values.customer_ids);
|
updateFilter('sort_by', values.sort_by, true);
|
||||||
updateFilter('supplierIds', values.supplier_ids);
|
updateFilter('startDate', values.start_date, true);
|
||||||
updateFilter('sortBy', values.sort_by);
|
updateFilter('endDate', values.end_date, true);
|
||||||
updateFilter('startDate', values.start_date);
|
// Save display names for restoration on modal reopen
|
||||||
updateFilter('endDate', values.end_date);
|
const toNames = (val: OptionType | OptionType[] | null) =>
|
||||||
|
val
|
||||||
|
? (Array.isArray(val) ? val : [val])
|
||||||
|
.map((o) => String(o.label))
|
||||||
|
.join(',')
|
||||||
|
: '';
|
||||||
|
updateFilter('bankNames', toNames(selectedBank), true);
|
||||||
|
updateFilter('customerNames', toNames(selectedCustomerId), true);
|
||||||
|
updateFilter('supplierNames', toNames(selectedSupplierId), true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('search', '');
|
setSelectedTransactionType(null);
|
||||||
resetSearchValue();
|
setSelectedBank(null);
|
||||||
updateFilter('transactionTypes', '');
|
setSelectedCustomerId(null);
|
||||||
updateFilter('bankIds', '');
|
setSelectedSupplierId(null);
|
||||||
updateFilter('customerIds', '');
|
setSelectedSortBy(null);
|
||||||
updateFilter('supplierIds', '');
|
updateFilter('search', '', true);
|
||||||
updateFilter('sortBy', '');
|
updateFilter('transactionTypes', '', true);
|
||||||
updateFilter('startDate', '');
|
updateFilter('bankIds', '', true);
|
||||||
updateFilter('endDate', '');
|
updateFilter('customerIds', '', true);
|
||||||
|
updateFilter('supplierIds', '', true);
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('orderBy', '', true);
|
||||||
|
updateFilter('startDate', '', true);
|
||||||
|
updateFilter('endDate', '', true);
|
||||||
|
updateFilter('bankNames', '', true);
|
||||||
|
updateFilter('customerNames', '', true);
|
||||||
|
updateFilter('supplierNames', '', true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,40 +343,10 @@ const FinanceTable = () => {
|
|||||||
});
|
});
|
||||||
}, [bankOptions, bankRawData]);
|
}, [bankOptions, bankRawData]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (tableFilterState.transactionTypes) count += 1;
|
|
||||||
if (tableFilterState.bankIds) count += 1;
|
|
||||||
if (tableFilterState.customerIds) count += 1;
|
|
||||||
if (tableFilterState.supplierIds) count += 1;
|
|
||||||
if (tableFilterState.sortBy) count += 1;
|
|
||||||
if (tableFilterState.startDate) count += 1;
|
|
||||||
if (tableFilterState.endDate) count += 1;
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [
|
|
||||||
tableFilterState.transactionTypes,
|
|
||||||
tableFilterState.bankIds,
|
|
||||||
tableFilterState.customerIds,
|
|
||||||
tableFilterState.supplierIds,
|
|
||||||
tableFilterState.sortBy,
|
|
||||||
tableFilterState.startDate,
|
|
||||||
tableFilterState.endDate,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== Handler =====
|
// ===== Handler =====
|
||||||
const searchChangeHandler = useCallback(
|
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
};
|
||||||
setSearchValue(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
},
|
|
||||||
[updateFilter, setSearchValue, setPage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const transactionTypeChangeHandler = (
|
const transactionTypeChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
@@ -409,6 +402,26 @@ const FinanceTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sorting: SortingState = tableFilterState.sort_by
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: tableFilterState.sort_by,
|
||||||
|
desc: tableFilterState.orderBy === 'desc',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleSortingChange = (updater: Updater<SortingState>) => {
|
||||||
|
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
||||||
|
if (next.length > 0) {
|
||||||
|
updateFilter('sort_by', next[0].id, true);
|
||||||
|
updateFilter('orderBy', next[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('orderBy', '', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const endDate = filterFormik.values.end_date;
|
const endDate = filterFormik.values.end_date;
|
||||||
@@ -469,28 +482,74 @@ const FinanceTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
|
// Restore transaction types from stored comma-separated IDs
|
||||||
|
const txIds = tableFilterState.transactionTypes
|
||||||
|
? tableFilterState.transactionTypes.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredTxTypes = FINANCE_TRANSACTION_TYPE_OPTIONS.filter((opt) =>
|
||||||
|
txIds.includes(String(opt.value))
|
||||||
|
);
|
||||||
|
setSelectedTransactionType(restoredTxTypes.length ? restoredTxTypes : null);
|
||||||
|
|
||||||
|
// Restore banks from stored IDs and names
|
||||||
|
const bankIdList = tableFilterState.bankIds
|
||||||
|
? tableFilterState.bankIds.split(',')
|
||||||
|
: [];
|
||||||
|
const bankNameList = tableFilterState.bankNames
|
||||||
|
? tableFilterState.bankNames.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredBanks = bankIdList.map((id, i) => ({
|
||||||
|
value: id,
|
||||||
|
label: bankNameList[i] || id,
|
||||||
|
}));
|
||||||
|
setSelectedBank(restoredBanks.length ? restoredBanks : null);
|
||||||
|
|
||||||
|
// Restore customers from stored IDs and names
|
||||||
|
const customerIdList = tableFilterState.customerIds
|
||||||
|
? tableFilterState.customerIds.split(',')
|
||||||
|
: [];
|
||||||
|
const customerNameList = tableFilterState.customerNames
|
||||||
|
? tableFilterState.customerNames.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredCustomers = customerIdList.map((id, i) => ({
|
||||||
|
value: id,
|
||||||
|
label: customerNameList[i] || id,
|
||||||
|
}));
|
||||||
|
setSelectedCustomerId(restoredCustomers.length ? restoredCustomers : null);
|
||||||
|
|
||||||
|
// Restore suppliers from stored IDs and names
|
||||||
|
const supplierIdList = tableFilterState.supplierIds
|
||||||
|
? tableFilterState.supplierIds.split(',')
|
||||||
|
: [];
|
||||||
|
const supplierNameList = tableFilterState.supplierNames
|
||||||
|
? tableFilterState.supplierNames.split(',')
|
||||||
|
: [];
|
||||||
|
const restoredSuppliers = supplierIdList.map((id, i) => ({
|
||||||
|
value: id,
|
||||||
|
label: supplierNameList[i] || id,
|
||||||
|
}));
|
||||||
|
setSelectedSupplierId(restoredSuppliers.length ? restoredSuppliers : null);
|
||||||
|
|
||||||
|
// Restore sort by
|
||||||
|
const restoredSortBy =
|
||||||
|
sortByOptions.find(
|
||||||
|
(opt) => String(opt.value) === tableFilterState.sort_by
|
||||||
|
) || null;
|
||||||
|
setSelectedSortBy(restoredSortBy);
|
||||||
|
|
||||||
|
// Restore formik values
|
||||||
|
filterFormik.setValues({
|
||||||
|
search: tableFilterState.search || '',
|
||||||
|
transaction_types: tableFilterState.transactionTypes || '',
|
||||||
|
bank_ids: tableFilterState.bankIds || '',
|
||||||
|
customer_ids: tableFilterState.customerIds || '',
|
||||||
|
supplier_ids: tableFilterState.supplierIds || '',
|
||||||
|
sort_by: tableFilterState.sort_by || '',
|
||||||
|
start_date: tableFilterState.startDate || '',
|
||||||
|
end_date: tableFilterState.endDate || '',
|
||||||
|
});
|
||||||
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
filterFormik.validateForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFilterHandler = () => {
|
|
||||||
setSelectedTransactionType(null);
|
|
||||||
setSelectedBank(null);
|
|
||||||
setSelectedCustomerId(null);
|
|
||||||
setSelectedSupplierId(null);
|
|
||||||
setSelectedSortBy(null);
|
|
||||||
|
|
||||||
filterFormik.resetForm();
|
|
||||||
|
|
||||||
updateFilter('search', '');
|
|
||||||
resetSearchValue();
|
|
||||||
updateFilter('transactionTypes', '');
|
|
||||||
updateFilter('bankIds', '');
|
|
||||||
updateFilter('customerIds', '');
|
|
||||||
updateFilter('supplierIds', '');
|
|
||||||
updateFilter('sortBy', '');
|
|
||||||
updateFilter('startDate', '');
|
|
||||||
updateFilter('endDate', '');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -509,10 +568,12 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'ID',
|
header: 'ID',
|
||||||
accessorKey: 'payment_code',
|
accessorKey: 'payment_code',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'References Number',
|
header: 'References Number',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.reference_number;
|
const value = props.row.original.reference_number;
|
||||||
return <span>{value ?? '-'}</span>;
|
return <span>{value ?? '-'}</span>;
|
||||||
@@ -521,6 +582,7 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Jenis Transaksi',
|
header: 'Jenis Transaksi',
|
||||||
accessorKey: 'transaction_type',
|
accessorKey: 'transaction_type',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.transaction_type
|
const value = props.row.original.transaction_type
|
||||||
.split('_')
|
.split('_')
|
||||||
@@ -530,7 +592,8 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorFn: (finance: Finance) => finance.party?.name,
|
accessorKey: 'customer_name',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
if (props.row.original.party?.id) {
|
if (props.row.original.party?.id) {
|
||||||
return <span>{props.row.original.party?.name}</span>;
|
return <span>{props.row.original.party?.name}</span>;
|
||||||
@@ -539,13 +602,23 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal',
|
header: 'Tanggal Pembayaran',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'payment_date',
|
||||||
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatDate(props.row.original.created_at, 'DD MMM YYYY'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Metode Pembayaran',
|
header: 'Metode Pembayaran',
|
||||||
accessorKey: 'payment_method',
|
accessorKey: 'payment_method',
|
||||||
|
enableSorting: true,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.payment_method.split('_').join(' ');
|
const value = props.row.original.payment_method.split('_').join(' ');
|
||||||
return <span>{formatTitleCase(value)}</span>;
|
return <span>{formatTitleCase(value)}</span>;
|
||||||
@@ -553,20 +626,26 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'bank',
|
||||||
finance.bank
|
enableSorting: true,
|
||||||
? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`
|
cell: (props) =>
|
||||||
|
props.row.original.bank
|
||||||
|
? `${props.row.original.bank?.alias} - ${props.row.original.bank?.account_number} - ${props.row.original.bank?.owner}`
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pengeluaran (Rp)',
|
header: 'Pengeluaran (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'expense_amount',
|
||||||
formatCurrency(Math.abs(finance.expense_amount)),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatCurrency(Math.abs(props.row.original.expense_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorKey: 'income_amount',
|
||||||
formatCurrency(Math.abs(finance.income_amount)),
|
enableSorting: true,
|
||||||
|
cell: (props) =>
|
||||||
|
formatCurrency(Math.abs(props.row.original.income_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
@@ -605,27 +684,6 @@ const FinanceTable = () => {
|
|||||||
};
|
};
|
||||||
}, [dateErrorShown]);
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
previousPathRef.current = window.location.pathname;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
|
|
||||||
const isCurrentPathFinance = currentPath.includes('/finance');
|
|
||||||
const isPreviousPathFinance =
|
|
||||||
previousPathRef.current?.includes('/finance');
|
|
||||||
|
|
||||||
if (isPreviousPathFinance && !isCurrentPathFinance) {
|
|
||||||
resetSearchValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [resetSearchValue, dateErrorShown]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -687,25 +745,20 @@ const FinanceTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'orderBy',
|
||||||
|
'bankNames',
|
||||||
|
'customerNames',
|
||||||
|
'supplierNames',
|
||||||
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -741,6 +794,9 @@ const FinanceTable = () => {
|
|||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
headerColumnClassName: 'text-nowrap',
|
||||||
@@ -874,19 +930,9 @@ const FinanceTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<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='button'
|
type='reset'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
|
||||||
filterFormik.resetForm();
|
|
||||||
setSelectedTransactionType(null);
|
|
||||||
setSelectedBank(null);
|
|
||||||
setSelectedCustomerId(null);
|
|
||||||
setSelectedSupplierId(null);
|
|
||||||
setSelectedSortBy(null);
|
|
||||||
resetFilterHandler();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import useSWR from 'swr';
|
||||||
import useSWR, { mutate } from 'swr';
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -25,7 +24,6 @@ import { cn, formatNumber, formatDate, formatCurrency } from '@/lib/helper';
|
|||||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -100,25 +98,31 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InventoryAdjustmentTable = () => {
|
const InventoryAdjustmentTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
productCategorySort: string;
|
||||||
|
productSort: string;
|
||||||
|
warehouseSort: string;
|
||||||
|
stockSort: string;
|
||||||
|
productFilter?: OptionType<string>;
|
||||||
|
warehouseFilter?: OptionType<string>;
|
||||||
|
transactionTypeFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productCategorySort: '',
|
productCategorySort: '',
|
||||||
productSort: '',
|
productSort: '',
|
||||||
warehouseSort: '',
|
warehouseSort: '',
|
||||||
stockSort: '',
|
stockSort: '',
|
||||||
productFilter: '',
|
productFilter: undefined,
|
||||||
warehouseFilter: '',
|
warehouseFilter: undefined,
|
||||||
transactionTypeFilter: '',
|
transactionTypeFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -131,6 +135,8 @@ const InventoryAdjustmentTable = () => {
|
|||||||
warehouseFilter: 'warehouse_id',
|
warehouseFilter: 'warehouse_id',
|
||||||
transactionTypeFilter: 'transaction_type',
|
transactionTypeFilter: 'transaction_type',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'inventory-adjustment-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -139,22 +145,27 @@ const InventoryAdjustmentTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<AdjustmentFilterType>({
|
const formik = useFormik<AdjustmentFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_id: null,
|
product: tableFilterState.productFilter,
|
||||||
warehouse: null,
|
warehouse: tableFilterState.warehouseFilter,
|
||||||
transaction_type: null,
|
transaction_type: tableFilterState.transactionTypeFilter,
|
||||||
},
|
},
|
||||||
validationSchema: AdjustmentFilterSchema,
|
validationSchema: AdjustmentFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product_id || '');
|
updateFilter('productFilter', values.product || undefined, true);
|
||||||
updateFilter('warehouseFilter', String(values.warehouse?.value) || '');
|
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
||||||
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
updateFilter(
|
||||||
|
'transactionTypeFilter',
|
||||||
|
values.transaction_type || undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', '');
|
updateFilter('productFilter', undefined, true);
|
||||||
updateFilter('warehouseFilter', '');
|
updateFilter('warehouseFilter', undefined, true);
|
||||||
updateFilter('transactionTypeFilter', '');
|
updateFilter('transactionTypeFilter', undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,14 +204,9 @@ const InventoryAdjustmentTable = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = useCallback(
|
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('product', val);
|
||||||
const product = val as OptionType | null;
|
};
|
||||||
const productId = product?.value ? String(product.value) : null;
|
|
||||||
formik.setFieldValue('product_id', productId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterWarehouseChange = (
|
const handleFilterWarehouseChange = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
@@ -208,38 +214,20 @@ const InventoryAdjustmentTable = () => {
|
|||||||
formik.setFieldValue('warehouse', val);
|
formik.setFieldValue('warehouse', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterTransactionTypeChange = useCallback(
|
const handleFilterTransactionTypeChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const type = val as OptionType | null;
|
) => {
|
||||||
const typeValue = type?.value ? String(type.value) : null;
|
formik.setFieldValue('transaction_type', val);
|
||||||
formik.setFieldValue('transaction_type', typeValue);
|
};
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const productIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.product_id) return null;
|
|
||||||
return (
|
|
||||||
productOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.product_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.product_id, productOptions]);
|
|
||||||
|
|
||||||
const transactionTypeValue = useMemo(() => {
|
|
||||||
if (!formik.values.transaction_type) return null;
|
|
||||||
return (
|
|
||||||
transactionTypeOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.transaction_type
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.transaction_type, transactionTypeOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
|
formik.setValues({
|
||||||
|
product: tableFilterState.productFilter ?? undefined,
|
||||||
|
warehouse: tableFilterState.warehouseFilter ?? undefined,
|
||||||
|
transaction_type: tableFilterState.transactionTypeFilter ?? undefined,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -276,17 +264,8 @@ const InventoryAdjustmentTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('inventory-adjustment-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
||||||
@@ -507,6 +486,8 @@ const InventoryAdjustmentTable = () => {
|
|||||||
'productSort',
|
'productSort',
|
||||||
'warehouseSort',
|
'warehouseSort',
|
||||||
'stockSort',
|
'stockSort',
|
||||||
|
'productName',
|
||||||
|
'warehouseName',
|
||||||
]}
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -596,7 +577,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={productIdValue}
|
value={formik.values.product}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -620,7 +601,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Tipe Transaksi'
|
label='Tipe Transaksi'
|
||||||
placeholder='Pilih Tipe Transaksi'
|
placeholder='Pilih Tipe Transaksi'
|
||||||
options={transactionTypeOptions}
|
options={transactionTypeOptions}
|
||||||
value={transactionTypeValue}
|
value={formik.values.transaction_type}
|
||||||
onChange={handleFilterTransactionTypeChange}
|
onChange={handleFilterTransactionTypeChange}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -630,13 +611,9 @@ const InventoryAdjustmentTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<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='button'
|
type='reset'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import { string, object } from 'yup';
|
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const AdjustmentFilterSchema = object().shape({
|
export const AdjustmentFilterSchema = Yup.object().shape({
|
||||||
product_id: string().nullable(),
|
product: Yup.object({
|
||||||
warehouse_id: string().nullable(),
|
value: Yup.string().nullable(),
|
||||||
transaction_type: string().nullable(),
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
warehouse: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
transaction_type: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AdjustmentFilterType = {
|
export type AdjustmentFilterType = {
|
||||||
product_id: string | null;
|
product?: OptionType<string>;
|
||||||
transaction_type: string | null;
|
warehouse?: OptionType<string>;
|
||||||
warehouse: OptionType<number> | null;
|
transaction_type?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
import useSWR from 'swr';
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR, { mutate } from 'swr';
|
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
@@ -20,7 +13,6 @@ import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -108,20 +100,21 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MovementTable = () => {
|
const MovementTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
productFilter?: OptionType<string>;
|
||||||
|
warehouseFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productFilter: '',
|
productFilter: undefined,
|
||||||
warehouseFilter: '',
|
warehouseFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -129,6 +122,8 @@ const MovementTable = () => {
|
|||||||
productFilter: 'product_id',
|
productFilter: 'product_id',
|
||||||
warehouseFilter: 'warehouse_id',
|
warehouseFilter: 'warehouse_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'movement-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -137,19 +132,20 @@ const MovementTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<MovementFilterType>({
|
const formik = useFormik<MovementFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_id: null,
|
product: tableFilterState.productFilter,
|
||||||
warehouse_id: null,
|
warehouse: tableFilterState.warehouseFilter,
|
||||||
},
|
},
|
||||||
validationSchema: MovementFilterSchema,
|
validationSchema: MovementFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product_id || '');
|
updateFilter('productFilter', values.product || undefined, true);
|
||||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', '');
|
updateFilter('productFilter', undefined, true);
|
||||||
updateFilter('warehouseFilter', '');
|
updateFilter('warehouseFilter', undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,47 +176,23 @@ const MovementTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = useCallback(
|
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('product', val);
|
||||||
const product = val as OptionType | null;
|
};
|
||||||
const productId = product?.value ? String(product.value) : null;
|
|
||||||
formik.setFieldValue('product_id', productId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterWarehouseChange = useCallback(
|
const handleFilterWarehouseChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const warehouse = val as OptionType | null;
|
) => {
|
||||||
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
formik.setFieldValue('warehouse', val);
|
||||||
formik.setFieldValue('warehouse_id', warehouseId);
|
};
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const productIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.product_id) return null;
|
|
||||||
return (
|
|
||||||
productOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.product_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.product_id, productOptions]);
|
|
||||||
|
|
||||||
const warehouseIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.warehouse_id) return null;
|
|
||||||
return (
|
|
||||||
warehouseOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.warehouse_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.warehouse_id, warehouseOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
|
formik.setValues({
|
||||||
|
product: tableFilterState.productFilter ?? undefined,
|
||||||
|
warehouse: tableFilterState.warehouseFilter ?? undefined,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -255,17 +227,8 @@ const MovementTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('movement-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
||||||
@@ -464,7 +427,7 @@ const MovementTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={productIdValue}
|
value={formik.values.product}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -476,7 +439,7 @@ const MovementTable = () => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih Gudang'
|
placeholder='Pilih Gudang'
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
value={warehouseIdValue}
|
value={formik.values.warehouse}
|
||||||
onChange={handleFilterWarehouseChange}
|
onChange={handleFilterWarehouseChange}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
isLoading={isLoadingWarehouseOptions}
|
isLoading={isLoadingWarehouseOptions}
|
||||||
@@ -489,13 +452,9 @@ const MovementTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<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='button'
|
type='reset'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const MovementFilterSchema = object().shape({
|
export const MovementFilterSchema = Yup.object().shape({
|
||||||
product_id: string().nullable(),
|
product: Yup.object({
|
||||||
warehouse_id: string().nullable(),
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
warehouse: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MovementFilterType = {
|
export type MovementFilterType = {
|
||||||
product_id: string | null;
|
product?: OptionType<string>;
|
||||||
warehouse_id: string | null;
|
warehouse?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,17 +4,23 @@ import Button from '@/components/Button';
|
|||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
import { InventoryProductApi } from '@/services/api/inventory';
|
import { InventoryProductApi } from '@/services/api/inventory';
|
||||||
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||||
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton';
|
import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton';
|
||||||
@@ -71,25 +77,79 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InventoryProductTable = () => {
|
const InventoryProductTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
categoryFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
|
categoryFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
|
categoryFilter: 'product_category_id',
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'inventory-product-table',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<{ category?: OptionType<string> }>({
|
||||||
|
initialValues: { category: tableFilterState.categoryFilter },
|
||||||
|
validationSchema: Yup.object().shape({
|
||||||
|
category: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
}),
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('categoryFilter', values.category || undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('categoryFilter', undefined, true);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== CATEGORY OPTIONS =====
|
||||||
|
const {
|
||||||
|
setInputValue: setCategoryInputValue,
|
||||||
|
options: categoryOptions,
|
||||||
|
isLoadingOptions: isLoadingCategoryOptions,
|
||||||
|
loadMore: loadMoreCategories,
|
||||||
|
} = useSelect<ProductCategory>(
|
||||||
|
filterModal.open ? ProductCategoryApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
formik.setValues({
|
||||||
|
category: tableFilterState.categoryFilter ?? undefined,
|
||||||
|
});
|
||||||
|
filterModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterCategoryChange = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
formik.setFieldValue('category', val);
|
||||||
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const { data: inventoryProducts, isLoading } = useSWR(
|
const { data: inventoryProducts, isLoading } = useSWR(
|
||||||
@@ -97,17 +157,8 @@ const InventoryProductTable = () => {
|
|||||||
InventoryProductApi.getAllFetcher
|
InventoryProductApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('inventory-product-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
||||||
@@ -182,96 +233,163 @@ const InventoryProductTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<>
|
||||||
{/* Header Section */}
|
<div className='w-full'>
|
||||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
{/* Header Section */}
|
||||||
{/* Action Buttons */}
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
{/* Action Buttons */}
|
||||||
<RequirePermission permissions='lti.inventory.product_stock.create'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<Button
|
<RequirePermission permissions='lti.inventory.product_stock.create'>
|
||||||
href='/inventory/product/add'
|
<Button
|
||||||
color='primary'
|
href='/inventory/product/add'
|
||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
color='primary'
|
||||||
>
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
>
|
||||||
Add Product
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
</Button>
|
Add Product
|
||||||
</RequirePermission>
|
</Button>
|
||||||
</div>
|
</RequirePermission>
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Search'
|
|
||||||
value={tableFilterState.search ?? ''}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
startAdornment={
|
|
||||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
|
||||||
}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
|
||||||
inputWrapper: 'rounded-xl! shadow-button-soft',
|
|
||||||
input:
|
|
||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table Section */}
|
|
||||||
<div className='flex flex-col mb-4'>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
</div>
|
||||||
) : !isResponseSuccess(inventoryProducts) ||
|
|
||||||
inventoryProducts.data?.length === 0 ? (
|
{/* Search and Filter */}
|
||||||
<div className='p-3'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<InventoryProductTableSkeleton
|
<DebouncedTextInput
|
||||||
columns={columns}
|
name='search'
|
||||||
icon={
|
placeholder='Search'
|
||||||
|
value={tableFilterState.search ?? ''}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
startAdornment={
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:document-text'
|
icon='heroicons:magnifying-glass'
|
||||||
className='text-white'
|
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
|
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||||
|
input:
|
||||||
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
onClick={handleFilterModalOpen}
|
||||||
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<Table<InventoryProduct>
|
|
||||||
data={
|
{/* Table Section */}
|
||||||
isResponseSuccess(inventoryProducts)
|
<div className='flex flex-col mb-4'>
|
||||||
? inventoryProducts?.data
|
{isLoading ? (
|
||||||
: []
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
}
|
<span className='loading loading-spinner loading-xl' />
|
||||||
columns={columns}
|
</div>
|
||||||
pageSize={tableFilterState.pageSize}
|
) : !isResponseSuccess(inventoryProducts) ||
|
||||||
page={
|
inventoryProducts.data?.length === 0 ? (
|
||||||
isResponseSuccess(inventoryProducts)
|
<div className='p-3'>
|
||||||
? inventoryProducts?.meta?.page
|
<InventoryProductTableSkeleton
|
||||||
: 0
|
columns={columns}
|
||||||
}
|
icon={
|
||||||
totalItems={
|
<Icon
|
||||||
isResponseSuccess(inventoryProducts)
|
icon='heroicons:document-text'
|
||||||
? inventoryProducts?.meta?.total_results
|
className='text-white'
|
||||||
: 0
|
width={20}
|
||||||
}
|
height={20}
|
||||||
onPageChange={setPage}
|
/>
|
||||||
onPageSizeChange={setPageSize}
|
}
|
||||||
isLoading={isLoading}
|
/>
|
||||||
sorting={sorting}
|
</div>
|
||||||
setSorting={setSorting}
|
) : (
|
||||||
className={{
|
<Table<InventoryProduct>
|
||||||
containerClassName: cn('p-3 mb-0'),
|
data={
|
||||||
headerColumnClassName: 'text-nowrap',
|
isResponseSuccess(inventoryProducts)
|
||||||
}}
|
? inventoryProducts?.data
|
||||||
/>
|
: []
|
||||||
)}
|
}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(inventoryProducts)
|
||||||
|
? inventoryProducts?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(inventoryProducts)
|
||||||
|
? inventoryProducts?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn('p-3 mb-0'),
|
||||||
|
headerColumnClassName: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</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',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori Produk'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={categoryOptions}
|
||||||
|
value={formik.values.category}
|
||||||
|
onChange={handleFilterCategoryChange}
|
||||||
|
onInputChange={setCategoryInputValue}
|
||||||
|
isLoading={isLoadingCategoryOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreCategories}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<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={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import StockLogFilterModal from '@/components/pages/inventory/product/detail/StockLogFilterModal';
|
||||||
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
||||||
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
||||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@@ -11,17 +19,34 @@ const InventoryProductDetail = ({
|
|||||||
}: {
|
}: {
|
||||||
inventoryProduct?: InventoryProduct;
|
inventoryProduct?: InventoryProduct;
|
||||||
}) => {
|
}) => {
|
||||||
const stockLogs = useMemo(() => {
|
const filterModal = useModal();
|
||||||
return (
|
|
||||||
inventoryProduct?.product_warehouses?.flatMap((warehouse) =>
|
const { state: filterState, updateFilter } = useTableFilter<{
|
||||||
warehouse.stock_logs.map((log) => ({
|
warehouse_ids: OptionType<number>[];
|
||||||
...log,
|
}>({
|
||||||
warehouse_name: warehouse.warehouse_name,
|
initial: {
|
||||||
warehouse_id: warehouse.warehouse_id,
|
warehouse_ids: [],
|
||||||
}))
|
},
|
||||||
) || []
|
persist: true,
|
||||||
);
|
storeName: 'inventory-product-stock-log-filter',
|
||||||
}, [inventoryProduct]);
|
});
|
||||||
|
|
||||||
|
const filteredProductWarehouses = useMemo(() => {
|
||||||
|
const warehouses = inventoryProduct?.product_warehouses ?? [];
|
||||||
|
if (!filterState.warehouse_ids?.length) return warehouses;
|
||||||
|
const selectedIds = new Set(filterState.warehouse_ids.map((w) => w.value));
|
||||||
|
return warehouses.filter((pw) => selectedIds.has(pw.warehouse_id));
|
||||||
|
}, [inventoryProduct?.product_warehouses, filterState.warehouse_ids]);
|
||||||
|
|
||||||
|
const filterSubmitHandler = (values: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
}) => {
|
||||||
|
updateFilter('warehouse_ids', values.warehouse_ids, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterResetHandler = () => {
|
||||||
|
updateFilter('warehouse_ids', [], true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
@@ -114,7 +139,29 @@ const InventoryProductDetail = ({
|
|||||||
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StockLogTable stockLogs={stockLogs} />
|
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
||||||
|
<div className='flex justify-end'>
|
||||||
|
<ButtonFilter
|
||||||
|
values={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onClick={filterModal.openModal}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{filteredProductWarehouses.map((productWarehouse) => (
|
||||||
|
<StockLogTable
|
||||||
|
key={productWarehouse.id}
|
||||||
|
productWarehouse={productWarehouse}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<StockLogFilterModal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
productWarehouses={inventoryProduct?.product_warehouses ?? []}
|
||||||
|
initialValues={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onSubmit={filterSubmitHandler}
|
||||||
|
onReset={filterResetHandler}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { RefObject, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface StockLogFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
productWarehouses: ProductWarehouseStock[];
|
||||||
|
initialValues: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
};
|
||||||
|
onSubmit: (values: { warehouse_ids: OptionType<number>[] }) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockLogFilterModal = ({
|
||||||
|
ref,
|
||||||
|
productWarehouses,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: StockLogFilterModalProps) => {
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const warehouseOptions: OptionType<number>[] = productWarehouses.map(
|
||||||
|
(pw) => ({
|
||||||
|
label: pw.warehouse_name,
|
||||||
|
value: pw.warehouse_id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
|
onSubmit: (values) => {
|
||||||
|
onSubmit(values);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resetForm } = formik;
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({ values: { warehouse_ids: [] } });
|
||||||
|
onReset();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal ref={ref} className={{ modalBox: 'p-0 rounded-xl' }}>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formikResetHandler}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Stock Log</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Gudang'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih gudang'
|
||||||
|
options={warehouseOptions}
|
||||||
|
value={formik.values.warehouse_ids}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('warehouse_ids', val as OptionType<number>[])
|
||||||
|
}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 rounded-lg text-base-content/65'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockLogFilterModal;
|
||||||
@@ -1,95 +1,183 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
|
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
|
||||||
import { StockLog } from '@/types/api/inventory/product';
|
import { StockLogApi } from '@/services/api/inventory';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ProductWarehouseStock, StockLog } from '@/types/api/inventory/product';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { FileDown } from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
||||||
|
warehouseName
|
||||||
|
) => [
|
||||||
|
{
|
||||||
|
header: 'ID',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal',
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Gudang',
|
||||||
|
accessorKey: 'warehouse_name',
|
||||||
|
cell: warehouseName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Stock Akhir',
|
||||||
|
accessorKey: 'stock',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.stock);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Peningkatan',
|
||||||
|
accessorKey: 'increase',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.increase);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Penurunan',
|
||||||
|
accessorKey: 'decrease',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.decrease);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jenis Transaksi',
|
||||||
|
accessorKey: 'loggable_type',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.loggable_type
|
||||||
|
? formatTitleCase(props.row.original.loggable_type)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Catatan',
|
||||||
|
accessorKey: 'notes',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.notes ? props.row.original.notes : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Oleh',
|
||||||
|
accessorKey: 'created_user.name',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const StockLogTable = ({
|
const StockLogTable = ({
|
||||||
stockLogs,
|
productWarehouse,
|
||||||
}: {
|
}: {
|
||||||
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
|
productWarehouse: ProductWarehouseStock;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isExportLoading, setIsExportLoading] = useState(false);
|
||||||
|
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setHasBeenVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (containerRef.current) observer.observe(containerRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
product_warehouse_id: productWarehouse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleExportExcel = async () => {
|
||||||
|
setIsExportLoading(true);
|
||||||
|
try {
|
||||||
|
await StockLogApi.exportToExcel(
|
||||||
|
productWarehouse.warehouse_name,
|
||||||
|
getTableFilterQueryString()
|
||||||
|
);
|
||||||
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
|
} finally {
|
||||||
|
setIsExportLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
||||||
|
hasBeenVisible
|
||||||
|
? `${StockLogApi.basePath}${getTableFilterQueryString()}`
|
||||||
|
: null,
|
||||||
|
StockLogApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const stockLogs = isResponseSuccess(stockLogsResponse)
|
||||||
|
? stockLogsResponse.data
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div ref={containerRef}>
|
||||||
title='Informasi Stock Produk'
|
<Card
|
||||||
collapsible
|
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
||||||
variant='bordered'
|
collapsible
|
||||||
className={{
|
variant='bordered'
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<StockLog>
|
|
||||||
data={stockLogs}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'ID',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Tanggal',
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Gudang',
|
|
||||||
accessorKey: 'warehouse_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Stock Akhir',
|
|
||||||
accessorKey: 'stock',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.stock);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Peningkatan',
|
|
||||||
accessorKey: 'increase',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.increase);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penurunan',
|
|
||||||
accessorKey: 'decrease',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.decrease);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Jenis Transaksi',
|
|
||||||
accessorKey: 'loggable_type',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.loggable_type
|
|
||||||
? formatTitleCase(props.row.original.loggable_type)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Catatan',
|
|
||||||
accessorKey: 'notes',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.notes ? props.row.original.notes : '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Oleh',
|
|
||||||
accessorKey: 'created_user.name',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-6',
|
wrapper: 'w-full',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Card>
|
<div className='flex justify-end px-6 pt-4'>
|
||||||
|
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
||||||
|
<FileDown size={16} />
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table<StockLog>
|
||||||
|
data={stockLogs}
|
||||||
|
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
||||||
|
page={tableFilterState.page ?? 0}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoadingStockLogs}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(stockLogsResponse)
|
||||||
|
? stockLogsResponse.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mt-4 mb-0',
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,42 @@
|
|||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const stockProductWarehouseTableColumns: ColumnDef<ProductWarehouseStock>[] = [
|
||||||
|
{
|
||||||
|
header: 'Nama Gudang',
|
||||||
|
accessorKey: 'warehouse_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Lokasi',
|
||||||
|
accessorKey: 'location',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.location != null
|
||||||
|
? props.row.original.location.name
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Stok',
|
||||||
|
accessorFn(row) {
|
||||||
|
return row.current_stock;
|
||||||
|
},
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.current_stock);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const StockProductWarehouseTable = ({
|
const StockProductWarehouseTable = ({
|
||||||
productWarehouseStock,
|
productWarehouseStock,
|
||||||
}: {
|
}: {
|
||||||
productWarehouseStock?: ProductWarehouseStock[];
|
productWarehouseStock?: ProductWarehouseStock[];
|
||||||
}) => {
|
}) => {
|
||||||
|
const { state: tableFilterState, setPage, setPageSize } = useTableFilter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title='Informasi Gudang'
|
title='Informasi Gudang'
|
||||||
@@ -19,32 +48,14 @@ const StockProductWarehouseTable = ({
|
|||||||
>
|
>
|
||||||
<Table<ProductWarehouseStock>
|
<Table<ProductWarehouseStock>
|
||||||
data={productWarehouseStock ?? []}
|
data={productWarehouseStock ?? []}
|
||||||
columns={[
|
columns={stockProductWarehouseTableColumns}
|
||||||
{
|
pageSize={tableFilterState.pageSize}
|
||||||
header: 'Nama Gudang',
|
page={tableFilterState.page ?? 0}
|
||||||
accessorKey: 'warehouse_name',
|
totalItems={productWarehouseStock?.length ?? 0}
|
||||||
},
|
onPageChange={setPage}
|
||||||
{
|
onPageSizeChange={setPageSize}
|
||||||
header: 'Lokasi',
|
|
||||||
accessorKey: 'location',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.location != null
|
|
||||||
? props.row.original.location.name
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Stok',
|
|
||||||
accessorFn(row) {
|
|
||||||
return row.current_stock;
|
|
||||||
},
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.current_stock);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-6',
|
containerClassName: 'mt-6 mb-0',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
|||||||
@@ -849,7 +849,11 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||||
disabled={deliveryRejected}
|
disabled={deliveryRejected}
|
||||||
>
|
>
|
||||||
Approve
|
{marketing?.data?.latest_approval?.step_number === 1 &&
|
||||||
|
'Approve'}
|
||||||
|
|
||||||
|
{marketing?.data?.latest_approval?.step_number === 2 &&
|
||||||
|
'Deliver Item'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useMemo } from 'react';
|
import { RefObject, useCallback, useMemo } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -17,20 +17,31 @@ import {
|
|||||||
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
|
||||||
interface MarketingFilterModal {
|
interface MarketingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
onSubmit?: (values: MarketingFilter) => void;
|
onSubmit?: (values: MarketingFilter) => void;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
|
initialValues?: {
|
||||||
|
product_ids: OptionType<number>[];
|
||||||
|
status: OptionType<string> | null;
|
||||||
|
customer: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarketingFilterModal = ({
|
const MarketingFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
|
initialValues,
|
||||||
}: MarketingFilterModal) => {
|
}: MarketingFilterModal) => {
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
@@ -38,36 +49,13 @@ const MarketingFilterModal = ({
|
|||||||
|
|
||||||
// ===== OPTIONS =====
|
// ===== OPTIONS =====
|
||||||
const {
|
const {
|
||||||
rawData: productsRawData,
|
options: productsOptions,
|
||||||
isLoadingOptions: isLoadingProductsOptions,
|
isLoadingOptions: isLoadingProductsOptions,
|
||||||
setInputValue: setProductsInputValue,
|
setInputValue: setProductsInputValue,
|
||||||
loadMore: loadMoreProducts,
|
loadMore: loadMoreProducts,
|
||||||
} = useSelect<BaseMarketing>(
|
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
||||||
MarketingApi.basePath,
|
include_all: 'true',
|
||||||
'id',
|
});
|
||||||
'so_number',
|
|
||||||
'search'
|
|
||||||
);
|
|
||||||
|
|
||||||
const productsOptions = useMemo(() => {
|
|
||||||
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
|
|
||||||
|
|
||||||
const productsMap = new Map<number, { value: number; label: string }>();
|
|
||||||
|
|
||||||
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
|
|
||||||
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
|
|
||||||
const product = so.product_warehouse?.product;
|
|
||||||
if (product?.id && product?.name) {
|
|
||||||
productsMap.set(product.id, {
|
|
||||||
value: product.id,
|
|
||||||
label: product.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(productsMap.values());
|
|
||||||
}, [productsRawData]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: customersOptions,
|
options: customersOptions,
|
||||||
@@ -78,6 +66,19 @@ const MarketingFilterModal = ({
|
|||||||
has_marketing: 'true',
|
has_marketing: 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: projectFlockOptions,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
...MARKETING_APPROVAL_LINE.map((item) => ({
|
...MARKETING_APPROVAL_LINE.map((item) => ({
|
||||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||||
@@ -87,18 +88,29 @@ const MarketingFilterModal = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const formik = useFormik<MarketingFilterFormValues>({
|
const formik = useFormik<MarketingFilterFormValues>({
|
||||||
initialValues: {
|
initialValues: initialValues || {
|
||||||
product_ids: [],
|
product_ids: [],
|
||||||
status: null,
|
status: null,
|
||||||
customer: null,
|
customer: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
validationSchema: MarketingFilterSchema,
|
validationSchema: MarketingFilterSchema,
|
||||||
|
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues: MarketingFilter = {
|
const formattedValues: MarketingFilter = {
|
||||||
product_ids: values.product_ids.map((item) => Number(item.value)),
|
product_ids: values.product_ids.map((item) => Number(item.value)),
|
||||||
|
product_names: values.product_ids.map((item) => item.label),
|
||||||
status: values.status?.value.toString() || '',
|
status: values.status?.value.toString() || '',
|
||||||
|
status_name: values.status?.label || '-',
|
||||||
customer_id: Number(values.customer?.value),
|
customer_id: Number(values.customer?.value),
|
||||||
|
customer_name: values.customer?.label || '-',
|
||||||
|
project_flock_id: values.project_flock?.value || undefined,
|
||||||
|
project_flock_name: values.project_flock?.label,
|
||||||
|
project_flock_kandang_id:
|
||||||
|
Number(values.project_flock_kandang?.value) || undefined,
|
||||||
|
project_flock_kandang_name:
|
||||||
|
values.project_flock_kandang?.label || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues);
|
||||||
@@ -111,6 +123,22 @@ const MarketingFilterModal = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { resetForm } = formik;
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
product_ids: [],
|
||||||
|
status: null,
|
||||||
|
customer: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset, closeModalHandler]);
|
||||||
|
|
||||||
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('product_ids', val as OptionType[]);
|
formik.setFieldValue('product_ids', val as OptionType[]);
|
||||||
};
|
};
|
||||||
@@ -126,6 +154,27 @@ const MarketingFilterModal = ({
|
|||||||
formik.setFieldValue('status', val as OptionType);
|
formik.setFieldValue('status', val as OptionType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -135,7 +184,7 @@ const MarketingFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formikResetHandler}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -192,6 +241,37 @@ const MarketingFilterModal = ({
|
|||||||
onInputChange={setCustomersInputValue}
|
onInputChange={setCustomersInputValue}
|
||||||
onMenuScrollToBottom={loadMoreCustomers}
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
/>
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
options={projectFlockOptions}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock',
|
||||||
|
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
||||||
|
);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -2,26 +2,39 @@
|
|||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import {
|
||||||
|
getErrorMessage,
|
||||||
|
isResponseError,
|
||||||
|
isResponseSuccess,
|
||||||
|
} from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
MarketingApi,
|
MarketingApi,
|
||||||
SalesOrderApi,
|
SalesOrderApi,
|
||||||
} from '@/services/api/marketing/marketing';
|
} from '@/services/api/marketing/marketing';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
BaseSalesOrder,
|
BaseSalesOrder,
|
||||||
Marketing,
|
Marketing,
|
||||||
MarketingFilter,
|
MarketingFilter,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
import {
|
||||||
|
CellContext,
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
SortingState,
|
||||||
|
Updater,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useMemo, useState } from 'react';
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
@@ -154,12 +167,21 @@ const MarketingTable = () => {
|
|||||||
);
|
);
|
||||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
const [bulkDeliveryDate, setBulkDeliveryDate] = useState('');
|
||||||
|
const [bulkDeliveryNotes, setBulkDeliveryNotes] = useState('');
|
||||||
|
const [isSubmittingBulkDelivery, setIsSubmittingBulkDelivery] =
|
||||||
|
useState(false);
|
||||||
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
|
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
||||||
|
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const confirmationModal = useModal();
|
const confirmationModal = useModal();
|
||||||
const productsModal = useModal();
|
const productsModal = useModal();
|
||||||
const deliveryModal = useModal();
|
const deliveryModal = useModal();
|
||||||
|
const bulkDeliveryModal = useModal();
|
||||||
|
const exportProgressInputModal = useModal();
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -172,8 +194,17 @@ const MarketingTable = () => {
|
|||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
product_ids: '',
|
product_ids: '',
|
||||||
|
product_names: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
status_name: '',
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
|
customer_name: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
project_flock_name: '',
|
||||||
|
project_flock_kandang_id: '',
|
||||||
|
project_flock_kandang_name: '',
|
||||||
|
sort_by: '',
|
||||||
|
order_by: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -181,9 +212,43 @@ const MarketingTable = () => {
|
|||||||
product_ids: 'product_ids',
|
product_ids: 'product_ids',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
customer_id: 'customer_id',
|
customer_id: 'customer_id',
|
||||||
|
project_flock_id: 'project_flock_id',
|
||||||
|
project_flock_kandang_id: 'project_flock_kandang_id',
|
||||||
|
sort_by: 'sort_by',
|
||||||
|
order_by: 'sort_order',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: [
|
||||||
|
'product_names',
|
||||||
|
'status_name',
|
||||||
|
'customer_name',
|
||||||
|
'project_flock_name',
|
||||||
|
'project_flock_kandang_name',
|
||||||
|
],
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'marketing-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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
data: marketing,
|
data: marketing,
|
||||||
@@ -198,26 +263,64 @@ const MarketingTable = () => {
|
|||||||
const filterSubmitHandler = (values: MarketingFilter) => {
|
const filterSubmitHandler = (values: MarketingFilter) => {
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'product_ids',
|
'product_ids',
|
||||||
values.product_ids?.map((item) => item.toString()).join(',')
|
values.product_ids?.map((item) => item.toString()).join(','),
|
||||||
|
true
|
||||||
);
|
);
|
||||||
updateFilter('status', values.status ? values.status.toString() : '');
|
updateFilter('product_names', values.product_names?.join(','));
|
||||||
|
updateFilter('status', values.status ? values.status.toString() : '', true);
|
||||||
|
updateFilter('status_name', values.status_name, true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'customer_id',
|
'customer_id',
|
||||||
values.customer_id ? values.customer_id.toString() : ''
|
values.customer_id ? values.customer_id.toString() : '',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter('customer_name', values.customer_name, true);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_id',
|
||||||
|
values.project_flock_id ? values.project_flock_id.toString() : '',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter('project_flock_name', values.project_flock_name ?? '', true);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_kandang_id',
|
||||||
|
values.project_flock_kandang_id
|
||||||
|
? values.project_flock_kandang_id.toString()
|
||||||
|
: '',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_kandang_name',
|
||||||
|
values.project_flock_kandang_name ?? '',
|
||||||
|
true
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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', '');
|
updateFilter('product_ids', '', true);
|
||||||
updateFilter('status', '');
|
updateFilter('product_names', '', true);
|
||||||
updateFilter('customer_id', '');
|
updateFilter('status', '', true);
|
||||||
|
updateFilter('status_name', '', true);
|
||||||
|
updateFilter('customer_id', '', true);
|
||||||
|
updateFilter('customer_name', '', true);
|
||||||
|
updateFilter('project_flock_id', '', true);
|
||||||
|
updateFilter('project_flock_name', '', true);
|
||||||
|
updateFilter('project_flock_kandang_id', '', true);
|
||||||
|
updateFilter('project_flock_kandang_name', '', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
setApproveAction('APPROVED');
|
setApproveAction('APPROVED');
|
||||||
|
|
||||||
|
if (selectedApprovalStep === 2) {
|
||||||
|
bulkDeliveryModal.openModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,10 +329,13 @@ const MarketingTable = () => {
|
|||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const productsClickHandler = (item: Marketing) => {
|
const productsClickHandler = useCallback(
|
||||||
setSelectedItem(item);
|
(item: Marketing) => {
|
||||||
productsModal.openModal();
|
setSelectedItem(item);
|
||||||
};
|
productsModal.openModal();
|
||||||
|
},
|
||||||
|
[productsModal]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteMarketingHandler = async () => {
|
const deleteMarketingHandler = async () => {
|
||||||
const deleteMarketingRes = await MarketingApi.delete(
|
const deleteMarketingRes = await MarketingApi.delete(
|
||||||
@@ -251,75 +357,226 @@ const MarketingTable = () => {
|
|||||||
const selectedRowsData = allData.filter(
|
const selectedRowsData = allData.filter(
|
||||||
(row) => rowSelection[row.id.toString()]
|
(row) => rowSelection[row.id.toString()]
|
||||||
);
|
);
|
||||||
|
const selectedApprovalStep =
|
||||||
|
selectedRowsData.length > 0
|
||||||
|
? selectedRowsData[0].latest_approval.step_number
|
||||||
|
: null;
|
||||||
|
|
||||||
const hasApprovable = selectedRowsData.some(
|
const eligibleSelectedRows = selectedRowsData.filter((row) => {
|
||||||
(row) =>
|
const approval = row.latest_approval;
|
||||||
row.latest_approval.step_number === 1 &&
|
|
||||||
row.latest_approval.action !== 'REJECTED'
|
if (approval.action === 'REJECTED') {
|
||||||
);
|
return false;
|
||||||
const hasRejectable = selectedRowsData.some(
|
}
|
||||||
(row) =>
|
|
||||||
row.latest_approval.step_number === 1 &&
|
if (selectedApprovalStep === null) {
|
||||||
row.latest_approval.action !== 'REJECTED'
|
return approval.step_number === 1 || approval.step_number === 2;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return approval.step_number === selectedApprovalStep;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasApprovable = eligibleSelectedRows.length > 0;
|
||||||
|
const hasRejectable = eligibleSelectedRows.length > 0;
|
||||||
|
|
||||||
const disableApprove = !hasApprovable;
|
const disableApprove = !hasApprovable;
|
||||||
const disableReject = !hasRejectable;
|
const disableReject = !hasRejectable;
|
||||||
|
|
||||||
const idsToProcess =
|
const idsToProcess = eligibleSelectedRows.map((row) => row.id);
|
||||||
approveAction === 'APPROVED'
|
const nextApprovalStatus =
|
||||||
? selectedRowsData
|
selectedApprovalStep === 1
|
||||||
.filter((row) => row.latest_approval.step_number === 1)
|
? 'SALES_ORDER'
|
||||||
.map((row) => row.id)
|
: selectedApprovalStep === 2
|
||||||
: selectedRowsData
|
? 'DELIVERY_ORDER'
|
||||||
.filter((row) => row.latest_approval.step_number === 2)
|
: null;
|
||||||
.map((row) => row.id);
|
|
||||||
|
const productIds = tableFilterState.product_ids
|
||||||
|
? tableFilterState.product_ids
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const productLabels = tableFilterState.product_names
|
||||||
|
? tableFilterState.product_names
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const marketingFilterInitialValues = {
|
||||||
|
product_ids: productIds.map((value, idx) => ({
|
||||||
|
value: Number(value),
|
||||||
|
label: productLabels[idx] || '-',
|
||||||
|
})),
|
||||||
|
status: tableFilterState.status
|
||||||
|
? {
|
||||||
|
value: tableFilterState.status,
|
||||||
|
label: tableFilterState.status_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
customer: tableFilterState.customer_id
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.customer_id),
|
||||||
|
label: tableFilterState.customer_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
project_flock: tableFilterState.project_flock_id
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.project_flock_id),
|
||||||
|
label: tableFilterState.project_flock_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.project_flock_kandang_id),
|
||||||
|
label: tableFilterState.project_flock_kandang_name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
const approveMarketingHandler = async (notes: string) => {
|
const approveMarketingHandler = async (notes: string) => {
|
||||||
let idsToProcess: number[] = [];
|
|
||||||
|
|
||||||
idsToProcess = selectedRowsData
|
|
||||||
.filter((row) => row.latest_approval.step_number === 1)
|
|
||||||
.map((row) => row.id);
|
|
||||||
|
|
||||||
if (idsToProcess.length === 0) {
|
if (idsToProcess.length === 0) {
|
||||||
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
||||||
confirmationModal.closeModal();
|
confirmationModal.closeModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
|
if (approveAction === 'APPROVED' && selectedApprovalStep !== 1) {
|
||||||
idsToProcess,
|
toast.error('Approve tahap ini harus menggunakan tanggal pengiriman.');
|
||||||
approveAction,
|
confirmationModal.closeModal();
|
||||||
notes
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (isResponseSuccess(approveMarketingRes)) {
|
if (approveAction === 'APPROVED' && !nextApprovalStatus) {
|
||||||
|
toast.error('Status approval berikutnya tidak valid.');
|
||||||
confirmationModal.closeModal();
|
confirmationModal.closeModal();
|
||||||
toast.success(approveMarketingRes?.message as string);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(approveMarketingRes)) {
|
||||||
|
confirmationModal.closeModal();
|
||||||
|
toast.success(approveMarketingRes?.message as string);
|
||||||
|
setRowSelection({});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMarketing();
|
||||||
|
} finally {
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkDeliveryDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
||||||
|
e
|
||||||
|
) => {
|
||||||
|
setBulkDeliveryDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkDeliveryNotesChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLTextAreaElement
|
||||||
|
> = (e) => {
|
||||||
|
setBulkDeliveryNotes(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitBulkDeliveryApprovalHandler = async (
|
||||||
|
selectedIds: number[],
|
||||||
|
deliveryDate: string,
|
||||||
|
notes: string
|
||||||
|
) => {
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
toast.error('Tidak ada data yang valid untuk diproses.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deliveryDate) {
|
||||||
|
toast.error('Tanggal pengiriman wajib diisi.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmittingBulkDelivery(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bulkDeliveryApprovalRes = await MarketingApi.bulkApprovals(
|
||||||
|
selectedIds,
|
||||||
|
'DELIVERY_ORDER',
|
||||||
|
deliveryDate,
|
||||||
|
notes || `APPROVED delivery marketing ${selectedIds.join(', ')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(bulkDeliveryApprovalRes)) {
|
||||||
|
toast.error(bulkDeliveryApprovalRes?.message as string);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isResponseSuccess(bulkDeliveryApprovalRes)) {
|
||||||
|
toast.error('Gagal memproses bulk approve delivery.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(bulkDeliveryApprovalRes?.message as string);
|
||||||
|
bulkDeliveryModal.closeModal();
|
||||||
|
setBulkDeliveryDate('');
|
||||||
|
setBulkDeliveryNotes('');
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
|
refreshMarketing();
|
||||||
|
} finally {
|
||||||
|
setIsSubmittingBulkDelivery(false);
|
||||||
}
|
}
|
||||||
if (isResponseError(approveMarketingRes)) {
|
|
||||||
confirmationModal.closeModal();
|
|
||||||
toast.error(approveMarketingRes?.message as string);
|
|
||||||
}
|
|
||||||
refreshMarketing();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||||
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
setIsDeliveryLoading(true);
|
||||||
deliveryModal.closeModal();
|
try {
|
||||||
toast.success(res?.message as string);
|
const res = await SalesOrderApi.delivery(
|
||||||
refreshMarketing?.();
|
selectedItem?.id as number,
|
||||||
router.push(
|
notes
|
||||||
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
);
|
||||||
);
|
deliveryModal.closeModal();
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
refreshMarketing?.();
|
||||||
|
router.push(
|
||||||
|
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsDeliveryLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
const getRowCanSelect = useCallback(
|
||||||
const approval = row.original.latest_approval;
|
(row: Row<Marketing>): boolean => {
|
||||||
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
const approval = row.original.latest_approval;
|
||||||
};
|
const isSelectableStep =
|
||||||
|
approval?.step_number === 1 || approval?.step_number === 2;
|
||||||
|
|
||||||
|
if (!isSelectableStep || approval?.action === 'REJECTED') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedApprovalStep === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return approval?.step_number === selectedApprovalStep;
|
||||||
|
},
|
||||||
|
[selectedApprovalStep]
|
||||||
|
);
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
const exportToExcelHandler = async () => {
|
||||||
setIsLoadingExportingToExcel(true);
|
setIsLoadingExportingToExcel(true);
|
||||||
@@ -329,6 +586,53 @@ const MarketingTable = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetExportProgressForm = () => {
|
||||||
|
setExportProgressStartDate('');
|
||||||
|
setExportProgressEndDate('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportProgressStartDateChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLInputElement
|
||||||
|
> = (e) => {
|
||||||
|
setExportProgressStartDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportProgressEndDateChangeHandler: ChangeEventHandler<
|
||||||
|
HTMLInputElement
|
||||||
|
> = (e) => {
|
||||||
|
setExportProgressEndDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportProgressInputToExcelClickHandler = () => {
|
||||||
|
resetExportProgressForm();
|
||||||
|
exportProgressInputModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitExportProgressInputHandler = async () => {
|
||||||
|
if (!exportProgressStartDate || !exportProgressEndDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExportProgressLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MarketingApi.exportInputProgressToExcel(
|
||||||
|
exportProgressStartDate,
|
||||||
|
exportProgressEndDate
|
||||||
|
);
|
||||||
|
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
toast.success('Ekspor berhasil');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsExportProgressLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -336,7 +640,22 @@ const MarketingTable = () => {
|
|||||||
size: 1,
|
size: 1,
|
||||||
header: ({ table }) => {
|
header: ({ table }) => {
|
||||||
const allRows = table.getRowModel().rows;
|
const allRows = table.getRowModel().rows;
|
||||||
const selectableRows = allRows.filter(getRowCanSelect);
|
const stepForBulkSelection =
|
||||||
|
selectedApprovalStep ??
|
||||||
|
allRows.find(getRowCanSelect)?.original.latest_approval.step_number;
|
||||||
|
const selectableRows = allRows.filter((row) => {
|
||||||
|
if (!getRowCanSelect(row)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stepForBulkSelection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
row.original.latest_approval.step_number === stepForBulkSelection
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
selectableRows.length > 0 &&
|
selectableRows.length > 0 &&
|
||||||
@@ -378,7 +697,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'so_do_number',
|
accessorKey: 'so_number',
|
||||||
header: 'No. Order',
|
header: 'No. Order',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
return props.row.original.do_number
|
return props.row.original.do_number
|
||||||
@@ -394,7 +713,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'approval.step_name',
|
accessorKey: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -429,10 +748,12 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer.name',
|
accessorKey: 'customer',
|
||||||
header: 'Customer',
|
header: 'Customer',
|
||||||
|
cell: (props) => props.row.original.customer.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'grand_total',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.sales_order
|
row.sales_order
|
||||||
?.map((product) => product.total_price)
|
?.map((product) => product.total_price)
|
||||||
@@ -449,6 +770,7 @@ const MarketingTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'marketing_products.length',
|
accessorKey: 'marketing_products.length',
|
||||||
header: 'Product Details',
|
header: 'Product Details',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
if (props?.row?.original?.sales_order?.length) {
|
if (props?.row?.original?.sales_order?.length) {
|
||||||
if (props?.row?.original?.sales_order?.length > 1) {
|
if (props?.row?.original?.sales_order?.length > 1) {
|
||||||
@@ -470,6 +792,14 @@ const MarketingTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.created_at
|
||||||
|
? formatDate(props.row.original.created_at, 'DD MMM yyyy')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
maxSize: 80,
|
maxSize: 80,
|
||||||
@@ -504,7 +834,13 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, []);
|
}, [
|
||||||
|
deleteModal,
|
||||||
|
deliveryModal,
|
||||||
|
getRowCanSelect,
|
||||||
|
productsClickHandler,
|
||||||
|
selectedApprovalStep,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -527,7 +863,7 @@ const MarketingTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
{idsToProcess.length > 0 && (
|
{idsToProcess.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px'></div>
|
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px' />
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
@@ -541,7 +877,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Reject
|
Reject ({idsToProcess.length} Item)
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
@@ -557,7 +893,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Approve
|
Approve ({idsToProcess.length} Item)
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
@@ -566,7 +902,18 @@ const MarketingTable = () => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={['page', 'pageSize', 'search']}
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'product_names',
|
||||||
|
'status_name',
|
||||||
|
'customer_name',
|
||||||
|
'project_flock_name',
|
||||||
|
'project_flock_kandang_name',
|
||||||
|
'sort_by',
|
||||||
|
'order_by',
|
||||||
|
]}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
}}
|
}}
|
||||||
@@ -612,7 +959,17 @@ const MarketingTable = () => {
|
|||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Export to Excel
|
Ekspor ke Excel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportProgressInputToExcelClickHandler}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor Input Progress (Excel)
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -646,6 +1003,9 @@ const MarketingTable = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pageSize={tableFilterState.pageSize}
|
pageSize={tableFilterState.pageSize}
|
||||||
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
totalItems={
|
totalItems={
|
||||||
isResponseSuccess(marketing)
|
isResponseSuccess(marketing)
|
||||||
? marketing?.meta?.total_results
|
? marketing?.meta?.total_results
|
||||||
@@ -677,14 +1037,16 @@ const MarketingTable = () => {
|
|||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={confirmationModal.ref}
|
ref={confirmationModal.ref}
|
||||||
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
||||||
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -708,14 +1070,169 @@ 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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
ref={bulkDeliveryModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Bulk Approve Delivery
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
bulkDeliveryModal.closeModal();
|
||||||
|
setBulkDeliveryDate('');
|
||||||
|
setBulkDeliveryNotes('');
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<p className='text-sm text-base-content/70'>
|
||||||
|
Pilih tanggal pengiriman untuk approve {idsToProcess.length} data
|
||||||
|
penjualan tahap 2.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='bulk_delivery_date'
|
||||||
|
label='Tanggal Pengiriman'
|
||||||
|
value={bulkDeliveryDate}
|
||||||
|
onChange={bulkDeliveryDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
name='bulk_delivery_notes'
|
||||||
|
label='Catatan'
|
||||||
|
placeholder='Masukkan catatan approval...'
|
||||||
|
value={bulkDeliveryNotes}
|
||||||
|
onChange={bulkDeliveryNotesChangeHandler}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
disabled={isSubmittingBulkDelivery}
|
||||||
|
onClick={() => {
|
||||||
|
bulkDeliveryModal.closeModal();
|
||||||
|
setBulkDeliveryDate('');
|
||||||
|
setBulkDeliveryNotes('');
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
isLoading={isSubmittingBulkDelivery}
|
||||||
|
disabled={isSubmittingBulkDelivery}
|
||||||
|
onClick={() =>
|
||||||
|
submitBulkDeliveryApprovalHandler(
|
||||||
|
idsToProcess,
|
||||||
|
bulkDeliveryDate,
|
||||||
|
bulkDeliveryNotes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
ref={exportProgressInputModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Ekspor Input Progress
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_start_date'
|
||||||
|
label='Tanggal Mulai'
|
||||||
|
value={exportProgressStartDate}
|
||||||
|
onChange={exportProgressStartDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_end_date'
|
||||||
|
label='Tanggal Selesai'
|
||||||
|
value={exportProgressEndDate}
|
||||||
|
onChange={exportProgressEndDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
onClick={submitExportProgressInputHandler}
|
||||||
|
isLoading={isExportProgressLoading}
|
||||||
|
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
ref={productsModal.ref}
|
ref={productsModal.ref}
|
||||||
className={{
|
className={{
|
||||||
@@ -777,6 +1294,7 @@ const MarketingTable = () => {
|
|||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
|
initialValues={marketingFilterInitialValues}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ const SalesOrderFormModal = ({
|
|||||||
})
|
})
|
||||||
.filter((item) => Boolean(item)),
|
.filter((item) => Boolean(item)),
|
||||||
} as UpdateDeliveryOrderPayload);
|
} as UpdateDeliveryOrderPayload);
|
||||||
|
|
||||||
switch (modalAction) {
|
switch (modalAction) {
|
||||||
case 'add':
|
case 'add':
|
||||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||||
@@ -261,11 +262,7 @@ const SalesOrderFormModal = ({
|
|||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
||||||
useFormikErrorList(formik, {
|
useFormikErrorList(formik);
|
||||||
onAfterSubmit: () => {
|
|
||||||
router.push('/marketing');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ================== FORM REPEATER HANDLER ==================
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ export const MarketingFilterSchema = object({
|
|||||||
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
||||||
status: mixed<OptionType<string>>().nullable(),
|
status: mixed<OptionType<string>>().nullable(),
|
||||||
customer: mixed<OptionType<number>>().nullable(),
|
customer: mixed<OptionType<number>>().nullable(),
|
||||||
|
project_flock: mixed<OptionType<number>>().nullable(),
|
||||||
|
project_flock_kandang: mixed<OptionType<number>>().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MarketingFilterFormValues = {
|
export type MarketingFilterFormValues = {
|
||||||
product_ids: OptionType<number>[];
|
product_ids: OptionType<number>[];
|
||||||
status: OptionType<string> | null;
|
status: OptionType<string> | null;
|
||||||
customer: OptionType<number> | null;
|
customer: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
|||||||
.required('Pengiriman wajib diisi!')
|
.required('Pengiriman wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
'at-least-one-valid-row',
|
'at-least-one-valid-row',
|
||||||
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
|
'Seluruh data pengiriman harus diisi lengkap!',
|
||||||
function (items) {
|
function (items) {
|
||||||
if (!items || items.length === 0) return false;
|
if (!items || items.length === 0) return false;
|
||||||
|
|
||||||
// VALIDASI: minimal 1 item valid full
|
// VALIDASI: seluruh item harus valid full
|
||||||
const itemSchema = DeliveryOrderProductSchema;
|
const itemSchema = DeliveryOrderProductSchema;
|
||||||
|
|
||||||
const hasValidItem = items.some((item) => {
|
const hasValidItem = items.every((item) => {
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
return itemSchema.isValidSync(item, { abortEarly: true });
|
return itemSchema.isValidSync(item, { abortEarly: true });
|
||||||
});
|
});
|
||||||
@@ -123,8 +123,17 @@ export const SalesProductToFieldValues = (
|
|||||||
total_price: product.total_price,
|
total_price: product.total_price,
|
||||||
marketing_type: product.marketing_type
|
marketing_type: product.marketing_type
|
||||||
? {
|
? {
|
||||||
value: product.marketing_type,
|
value:
|
||||||
label: formatTitleCase(product.marketing_type),
|
product.marketing_type === 'AYAM' ||
|
||||||
|
product.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM,AYAM_PULLET'
|
||||||
|
: product.marketing_type,
|
||||||
|
label: formatTitleCase(
|
||||||
|
product.marketing_type === 'AYAM' ||
|
||||||
|
product.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM'
|
||||||
|
: product.marketing_type
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: product.convertion_unit
|
convertion_unit: product.convertion_unit
|
||||||
@@ -144,9 +153,11 @@ export const DeliveryProductToFieldValues = (
|
|||||||
delivery: BaseDeliveryOrder
|
delivery: BaseDeliveryOrder
|
||||||
): DeliveryOrderProductFormValues[] => {
|
): DeliveryOrderProductFormValues[] => {
|
||||||
const data = delivery.deliveries.map((item) => {
|
const data = delivery.deliveries.map((item) => {
|
||||||
const salesOrder = salesOrders.find(
|
const salesOrder =
|
||||||
(so) => so.product_warehouse.id === item.product_warehouse.id
|
salesOrders.find((so) => so.id === item.marketing_product_id) ??
|
||||||
);
|
salesOrders.find(
|
||||||
|
(so) => so.product_warehouse.id === item.product_warehouse.id
|
||||||
|
);
|
||||||
const warehouseOption = {
|
const warehouseOption = {
|
||||||
value: item.product_warehouse.warehouse.id,
|
value: item.product_warehouse.warehouse.id,
|
||||||
label: item.product_warehouse.warehouse.name,
|
label: item.product_warehouse.warehouse.name,
|
||||||
@@ -180,11 +191,20 @@ export const DeliveryProductToFieldValues = (
|
|||||||
vehicle_number: item.vehicle_number,
|
vehicle_number: item.vehicle_number,
|
||||||
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
||||||
do_number: delivery.do_number,
|
do_number: delivery.do_number,
|
||||||
marketing_product_id: salesOrder?.id,
|
marketing_product_id: item.marketing_product_id ?? salesOrder?.id,
|
||||||
marketing_type: salesOrder?.marketing_type
|
marketing_type: salesOrder?.marketing_type
|
||||||
? {
|
? {
|
||||||
value: salesOrder?.marketing_type,
|
value:
|
||||||
label: formatTitleCase(salesOrder?.marketing_type),
|
salesOrder?.marketing_type === 'AYAM' ||
|
||||||
|
salesOrder?.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM,AYAM_PULLET'
|
||||||
|
: salesOrder?.marketing_type,
|
||||||
|
label: formatTitleCase(
|
||||||
|
salesOrder?.marketing_type === 'AYAM' ||
|
||||||
|
salesOrder?.marketing_type === 'AYAM_PULLET'
|
||||||
|
? 'AYAM'
|
||||||
|
: salesOrder?.marketing_type
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: salesOrder?.convertion_unit
|
convertion_unit: salesOrder?.convertion_unit
|
||||||
@@ -194,7 +214,7 @@ export const DeliveryProductToFieldValues = (
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
marketing_product: {
|
marketing_product: {
|
||||||
id: salesOrder?.id,
|
id: item.marketing_product_id ?? salesOrder?.id,
|
||||||
vehicle_number: item.vehicle_number,
|
vehicle_number: item.vehicle_number,
|
||||||
warehouse_id: item.product_warehouse.warehouse.id,
|
warehouse_id: item.product_warehouse.warehouse.id,
|
||||||
warehouse: warehouseOption,
|
warehouse: warehouseOption,
|
||||||
|
|||||||
+23
-23
@@ -146,15 +146,6 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ============ Fetch Data ============
|
// ============ Fetch Data ============
|
||||||
const { data: productData } = useSWR(
|
|
||||||
selectedProduct?.value
|
|
||||||
? ProductApi.basePath + '/' + selectedProduct?.value
|
|
||||||
: null,
|
|
||||||
() =>
|
|
||||||
selectedProduct?.value
|
|
||||||
? ProductApi.getSingle(Number(selectedProduct?.value))
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// Options Week dari minggu 1 - 22
|
// Options Week dari minggu 1 - 22
|
||||||
// const optionsWeek = useMemo(() => {
|
// const optionsWeek = useMemo(() => {
|
||||||
@@ -190,12 +181,19 @@ const DeliveryOrderProductForm = ({
|
|||||||
const deliveryOrder = useMemo(() => {
|
const deliveryOrder = useMemo(() => {
|
||||||
if (!hasDeliveryOrder || !deliveryOrders) return null;
|
if (!hasDeliveryOrder || !deliveryOrders) return null;
|
||||||
|
|
||||||
|
const marketingProductId =
|
||||||
|
initialValues?.marketing_product_id ?? initialValues?.id;
|
||||||
|
|
||||||
for (const doItem of deliveryOrders) {
|
for (const doItem of deliveryOrders) {
|
||||||
const found = doItem.deliveries.find(
|
const found =
|
||||||
(d) =>
|
doItem.deliveries.find(
|
||||||
d.product_warehouse.id ===
|
(d) => d.marketing_product_id === marketingProductId
|
||||||
initialValues?.marketing_product?.product_warehouse_id
|
) ??
|
||||||
);
|
doItem.deliveries.find(
|
||||||
|
(d) =>
|
||||||
|
d.product_warehouse.id ===
|
||||||
|
initialValues?.marketing_product?.product_warehouse_id
|
||||||
|
);
|
||||||
if (found) {
|
if (found) {
|
||||||
return {
|
return {
|
||||||
...found,
|
...found,
|
||||||
@@ -403,7 +401,10 @@ const DeliveryOrderProductForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
if (!Boolean(initialValues.qty)) {
|
if (
|
||||||
|
!Boolean(initialValues.qty) &&
|
||||||
|
!Boolean(initialValues.marketing_product_id)
|
||||||
|
) {
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
} else {
|
} else {
|
||||||
setFormikValues({
|
setFormikValues({
|
||||||
@@ -413,7 +414,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
});
|
});
|
||||||
if (initialValues?.marketing_product_id) {
|
if (initialValues?.marketing_product_id) {
|
||||||
setSelectedProduct({
|
setSelectedProduct({
|
||||||
value: initialValues?.id,
|
value: initialValues?.marketing_product_id,
|
||||||
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
|
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
|
||||||
} as OptionType);
|
} as OptionType);
|
||||||
}
|
}
|
||||||
@@ -430,7 +431,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
handleBlurField(currentInput);
|
handleBlurField(currentInput);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'uom',
|
'uom',
|
||||||
isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
|
initialValues?.marketing_product?.product_warehouse_data?.product?.uom
|
||||||
|
?.name ?? ''
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -803,9 +805,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-gray-500'>
|
<span className='text-sm text-gray-500'>
|
||||||
{isResponseSuccess(productData)
|
{initialValues?.marketing_product?.product_warehouse_data
|
||||||
? productData?.data?.uom.name
|
?.product?.uom?.name ?? ''}
|
||||||
: ''}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -816,9 +817,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty +
|
)?.qty +
|
||||||
' ' +
|
' ' +
|
||||||
(isResponseSuccess(productData)
|
(initialValues?.marketing_product?.product_warehouse_data
|
||||||
? productData?.data?.uom.name
|
?.product?.uom?.name ?? '')
|
||||||
: '')
|
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -252,6 +252,11 @@ const SalesOrderProductForm = ({
|
|||||||
setSelectedProductWarehouse(productWarehouse || null);
|
setSelectedProductWarehouse(productWarehouse || null);
|
||||||
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
||||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||||
|
|
||||||
|
if (productWarehouse?.quantity) {
|
||||||
|
handleFieldChange('qty', productWarehouse?.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
||||||
if (
|
if (
|
||||||
productWarehouse?.week !== undefined &&
|
productWarehouse?.week !== undefined &&
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Qty</td>
|
<td className='text-sm px-4 py-3'>Qty</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.qty
|
{item.qty !== undefined && item.qty !== null && item.qty !== ''
|
||||||
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
@@ -273,7 +273,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Qty</td>
|
<td className='text-sm px-4 py-3'>Qty</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.qty
|
{item.qty !== undefined && item.qty !== null && item.qty !== ''
|
||||||
? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
|
? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Area } from '@/types/api/master-data/area';
|
|||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AreasTable = () => {
|
const AreasTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const AreasTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'areas-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -137,17 +134,8 @@ const AreasTable = () => {
|
|||||||
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
|
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('areas-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Bank } from '@/types/api/master-data/bank';
|
|||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BanksTable = () => {
|
const BanksTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const BanksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'banks-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -137,17 +134,8 @@ const BanksTable = () => {
|
|||||||
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
|
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('banks-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Customer } from '@/types/api/master-data/customer';
|
|||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CustomersTable = () => {
|
const CustomersTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const CustomersTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'customers-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -139,17 +136,8 @@ const CustomersTable = () => {
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('customers-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -201,6 +189,11 @@ const CustomersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'bank_name',
|
||||||
|
header: 'Nama Bank',
|
||||||
|
cell: (props) => props.row.original.bank_name || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Customer, unknown>) => {
|
cell: (props: CellContext<Customer, unknown>) => {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export const CustomerFormSchema = Yup.object({
|
|||||||
.email('Format email tidak valid!')
|
.email('Format email tidak valid!')
|
||||||
.required('Email wajib diisi!'),
|
.required('Email wajib diisi!'),
|
||||||
|
|
||||||
|
bank_name: Yup.string()
|
||||||
|
.min(3, 'Nama bank minimal 3 karakter!')
|
||||||
|
.required('Nama bank wajib diisi!'),
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ const CustomerForm = ({
|
|||||||
},
|
},
|
||||||
type: normalizeType(initialValues?.type),
|
type: normalizeType(initialValues?.type),
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
|
bank_name: initialValues?.bank_name ?? '',
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
@@ -164,6 +165,7 @@ const CustomerForm = ({
|
|||||||
pic_id: values.picId,
|
pic_id: values.picId,
|
||||||
type: (values.type as OptionType).value as string,
|
type: (values.type as OptionType).value as string,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
|
bank_name: values.bank_name,
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,6 +288,22 @@ const CustomerForm = ({
|
|||||||
errorMessage={formik.errors.phone}
|
errorMessage={formik.errors.phone}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Nama Bank'
|
||||||
|
name='bank_name'
|
||||||
|
placeholder='Masukkan nama bank customer'
|
||||||
|
value={formik.values.bank_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
||||||
|
}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.bank_name}
|
||||||
|
readOnly={formType === 'detail'}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Flock } from '@/types/api/master-data/flock';
|
|||||||
import { FlockApi } from '@/services/api/master-data';
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FlockTable = () => {
|
const FlockTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,12 +109,14 @@ const FlockTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'flock-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -139,17 +136,8 @@ const FlockTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('flocks-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -35,7 +28,6 @@ import { User } from '@/types/api/api-general';
|
|||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
KandangFilterSchema,
|
KandangFilterSchema,
|
||||||
KandangFilterType,
|
KandangFilterType,
|
||||||
@@ -122,20 +114,21 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const KandangsTable = () => {
|
const KandangsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
locationFilter?: OptionType<string>;
|
||||||
|
picFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
locationFilter: '',
|
locationFilter: undefined,
|
||||||
picFilter: '',
|
picFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -143,6 +136,8 @@ const KandangsTable = () => {
|
|||||||
locationFilter: 'location_id',
|
locationFilter: 'location_id',
|
||||||
picFilter: 'pic_id',
|
picFilter: 'pic_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'kandangs-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -151,22 +146,34 @@ const KandangsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<KandangFilterType>({
|
const formik = useFormik<KandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
location_id: null,
|
location: tableFilterState.locationFilter,
|
||||||
pic_id: null,
|
pic: tableFilterState.picFilter,
|
||||||
},
|
},
|
||||||
validationSchema: KandangFilterSchema,
|
validationSchema: KandangFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('locationFilter', values.location_id || '');
|
updateFilter('locationFilter', values.location || undefined, true);
|
||||||
updateFilter('picFilter', values.pic_id || '');
|
updateFilter('picFilter', values.pic || undefined, true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('locationFilter', '');
|
|
||||||
updateFilter('picFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('locationFilter', undefined, true);
|
||||||
|
updateFilter('picFilter', undefined, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
location: undefined,
|
||||||
|
pic: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setFieldValue } = formik;
|
||||||
|
|
||||||
// ===== LOCATION OPTIONS =====
|
// ===== LOCATION OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
@@ -194,43 +201,15 @@ const KandangsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterLocationChange = useCallback(
|
const handleFilterLocationChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const location = val as OptionType | null;
|
) => {
|
||||||
const locationId = location?.value ? String(location.value) : null;
|
setFieldValue('location', val);
|
||||||
|
};
|
||||||
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
const handleFilterPicChange = (val: OptionType | OptionType[] | null) => {
|
||||||
},
|
setFieldValue('pic', val);
|
||||||
[formik]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterPicChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const pic = val as OptionType | null;
|
|
||||||
const picId = pic?.value ? String(pic.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('pic_id', picId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const locationIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.location_id) return null;
|
|
||||||
return (
|
|
||||||
locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.location_id, locationOptions]);
|
|
||||||
|
|
||||||
const picIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.pic_id) return null;
|
|
||||||
return (
|
|
||||||
picOptions.find((opt) => String(opt.value) === formik.values.pic_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.pic_id, picOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -255,17 +234,8 @@ const KandangsTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('kandangs-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -475,13 +445,13 @@ const KandangsTable = () => {
|
|||||||
<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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationIdValue}
|
value={formik.values.location}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
@@ -494,7 +464,7 @@ const KandangsTable = () => {
|
|||||||
label='PIC'
|
label='PIC'
|
||||||
placeholder='Pilih PIC'
|
placeholder='Pilih PIC'
|
||||||
options={picOptions}
|
options={picOptions}
|
||||||
value={picIdValue}
|
value={formik.values.pic}
|
||||||
onChange={handleFilterPicChange}
|
onChange={handleFilterPicChange}
|
||||||
onInputChange={setPicInputValue}
|
onInputChange={setPicInputValue}
|
||||||
isLoading={isLoadingPicOptions}
|
isLoading={isLoadingPicOptions}
|
||||||
@@ -510,17 +480,14 @@ const KandangsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<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={!formik.isValid || formik.isSubmitting}
|
disabled={!formik.isValid}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const KandangFilterSchema = object().shape({
|
export const KandangFilterSchema = Yup.object().shape({
|
||||||
location_id: string().nullable(),
|
location: Yup.object({
|
||||||
pic_id: string().nullable(),
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
|
||||||
|
pic: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type KandangFilterType = {
|
export type KandangFilterType = {
|
||||||
location_id: string | null;
|
location?: OptionType<string>;
|
||||||
pic_id: string | null;
|
pic?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -32,7 +25,6 @@ import { Area } from '@/types/api/master-data/area';
|
|||||||
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
LocationFilterSchema,
|
LocationFilterSchema,
|
||||||
LocationFilterType,
|
LocationFilterType,
|
||||||
@@ -118,25 +110,27 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LocationsTable = () => {
|
const LocationsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
areaFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
areaFilter: '',
|
areaFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
areaFilter: 'area_id',
|
areaFilter: 'area_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'locations-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -145,19 +139,28 @@ const LocationsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<LocationFilterType>({
|
const formik = useFormik<LocationFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area: tableFilterState.areaFilter,
|
||||||
},
|
},
|
||||||
validationSchema: LocationFilterSchema,
|
validationSchema: LocationFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area || undefined, true);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('areaFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('areaFilter', undefined, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
area: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== AREA OPTIONS =====
|
// ===== AREA OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setAreaInputValue,
|
setInputValue: setAreaInputValue,
|
||||||
@@ -172,24 +175,9 @@ const LocationsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = useCallback(
|
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('area', val);
|
||||||
const area = val as OptionType | null;
|
};
|
||||||
const areaId = area?.value ? String(area.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('area_id', areaId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const areaIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.area_id) return null;
|
|
||||||
return (
|
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.area_id, areaOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -212,19 +200,10 @@ const LocationsTable = () => {
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('locations-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -425,13 +404,13 @@ const LocationsTable = () => {
|
|||||||
<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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={areaIdValue}
|
value={formik.values.area}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -447,10 +426,7 @@ const LocationsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const LocationFilterSchema = object().shape({
|
export const LocationFilterSchema = Yup.object().shape({
|
||||||
area_id: string().nullable(),
|
area: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LocationFilterType = {
|
export type LocationFilterType = {
|
||||||
area_id: string | null;
|
area?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
|
|||||||
import { NonstockApi } from '@/services/api/master-data';
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NonstocksTable = () => {
|
const NonstocksTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,22 +109,16 @@ const NonstocksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'nonstock-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('nonstocks-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -148,8 +137,7 @@ const NonstocksTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -21,7 +20,6 @@ import { ProductCategory } from '@/types/api/master-data/product-category';
|
|||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProductCategoryTable = () => {
|
const ProductCategoryTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -120,12 +115,10 @@ const ProductCategoryTable = () => {
|
|||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'product-category-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -144,8 +137,7 @@ const ProductCategoryTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -214,10 +206,6 @@ const ProductCategoryTable = () => {
|
|||||||
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('product-category-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -33,7 +26,6 @@ import { ProductApi, ProductCategoryApi } from '@/services/api/master-data';
|
|||||||
import { formatCurrency } from '@/lib/helper';
|
import { formatCurrency } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
ProductFilterSchema,
|
ProductFilterSchema,
|
||||||
ProductFilterType,
|
ProductFilterType,
|
||||||
@@ -119,25 +111,27 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProductsTable = () => {
|
const ProductsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
productCategoryFilter?: OptionType<string>;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productCategoryFilter: '',
|
productCategoryFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
productCategoryFilter: 'product_category_id',
|
productCategoryFilter: 'product_category_id',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'product-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -146,19 +140,32 @@ const ProductsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<ProductFilterType>({
|
const formik = useFormik<ProductFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_category_id: null,
|
product_category: tableFilterState.productCategoryFilter,
|
||||||
},
|
},
|
||||||
validationSchema: ProductFilterSchema,
|
validationSchema: ProductFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productCategoryFilter', values.product_category_id || '');
|
updateFilter(
|
||||||
|
'productCategoryFilter',
|
||||||
|
values.product_category || undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('productCategoryFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('productCategoryFilter', undefined, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
product_category: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== PRODUCT CATEGORY OPTIONS =====
|
// ===== PRODUCT CATEGORY OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setProductCategoryInputValue,
|
setInputValue: setProductCategoryInputValue,
|
||||||
@@ -173,25 +180,11 @@ const ProductsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductCategoryChange = useCallback(
|
const handleFilterProductCategoryChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const category = val as OptionType | null;
|
) => {
|
||||||
const categoryId = category?.value ? String(category.value) : null;
|
formik.setFieldValue('product_category', val);
|
||||||
|
};
|
||||||
formik.setFieldValue('product_category_id', categoryId);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const productCategoryIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.product_category_id) return null;
|
|
||||||
return (
|
|
||||||
productCategoryOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.product_category_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.product_category_id, productCategoryOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -199,10 +192,6 @@ const ProductsTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -220,13 +209,8 @@ const ProductsTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('product-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -477,13 +461,13 @@ const ProductsTable = () => {
|
|||||||
<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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Kategori Produk'
|
label='Kategori Produk'
|
||||||
placeholder='Pilih Kategori Produk'
|
placeholder='Pilih Kategori Produk'
|
||||||
options={productCategoryOptions}
|
options={productCategoryOptions}
|
||||||
value={productCategoryIdValue}
|
value={formik.values.product_category}
|
||||||
onChange={handleFilterProductCategoryChange}
|
onChange={handleFilterProductCategoryChange}
|
||||||
onInputChange={setProductCategoryInputValue}
|
onInputChange={setProductCategoryInputValue}
|
||||||
isLoading={isLoadingProductCategoryOptions}
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
@@ -499,10 +483,7 @@ const ProductsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const ProductFilterSchema = object().shape({
|
export const ProductFilterSchema = Yup.object().shape({
|
||||||
product_category_id: string().nullable(),
|
product_category: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProductFilterType = {
|
export type ProductFilterType = {
|
||||||
product_category_id: string | null;
|
product_category?: OptionType<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,27 +128,44 @@ const ProductionStandardTable = () => {
|
|||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
projectCategoryFilter: 'project_category',
|
projectCategoryFilter: 'project_category',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'production-standard-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FILTER INITIAL VALUES (derived from persisted state) =====
|
||||||
|
const filterInitialValues = useMemo<ProductionStandardFilterType>(
|
||||||
|
() => ({
|
||||||
|
project_category: tableFilterState.projectCategoryFilter || null,
|
||||||
|
}),
|
||||||
|
[tableFilterState.projectCategoryFilter]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<ProductionStandardFilterType>({
|
const formik = useFormik<ProductionStandardFilterType>({
|
||||||
initialValues: {
|
initialValues: filterInitialValues,
|
||||||
project_category: null,
|
|
||||||
},
|
|
||||||
validationSchema: ProductionStandardFilterSchema,
|
validationSchema: ProductionStandardFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('projectCategoryFilter', values.project_category || '');
|
updateFilter('projectCategoryFilter', values.project_category || '');
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('projectCategoryFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('projectCategoryFilter', '', true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
project_category: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
||||||
const projectCategoryOptions = useMemo(
|
const projectCategoryOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -381,7 +398,7 @@ const ProductionStandardTable = () => {
|
|||||||
<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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Kategori'
|
label='Kategori'
|
||||||
@@ -397,13 +414,9 @@ const ProductionStandardTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<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='button'
|
type='reset'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -30,7 +29,7 @@ import { Supplier } from '@/types/api/master-data/supplier';
|
|||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
SupplierFilterSchema,
|
SupplierFilterSchema,
|
||||||
SupplierFilterType,
|
SupplierFilterType,
|
||||||
@@ -117,20 +116,21 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SuppliersTable = () => {
|
const SuppliersTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
categoryFilter?: OptionType<string>;
|
||||||
|
flagFilter?: string;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
categoryFilter: '',
|
categoryFilter: undefined,
|
||||||
flagFilter: '',
|
flagFilter: undefined,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -138,6 +138,8 @@ const SuppliersTable = () => {
|
|||||||
categoryFilter: 'category_id',
|
categoryFilter: 'category_id',
|
||||||
flagFilter: 'flag',
|
flagFilter: 'flag',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'supplier-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -146,26 +148,33 @@ const SuppliersTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<SupplierFilterType>({
|
const formik = useFormik<SupplierFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
category_id: null,
|
category: tableFilterState.categoryFilter,
|
||||||
flag: false,
|
flag: tableFilterState.flagFilter === 'EKSPEDISI',
|
||||||
},
|
},
|
||||||
validationSchema: SupplierFilterSchema,
|
validationSchema: SupplierFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('categoryFilter', values.category_id || '');
|
updateFilter('categoryFilter', values.category || undefined, true);
|
||||||
updateFilter(
|
updateFilter('flagFilter', values.flag === true ? 'EKSPEDISI' : '', true);
|
||||||
'flagFilter',
|
|
||||||
values.flag === true ? 'EKSPEDISI' : values.flag === false ? '' : ''
|
|
||||||
);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('categoryFilter', '');
|
|
||||||
updateFilter('flagFilter', '');
|
|
||||||
formik.setFieldValue('flag', false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('categoryFilter', undefined, true);
|
||||||
|
updateFilter('flagFilter', '', true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
category: undefined,
|
||||||
|
flag: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
const { setFieldValue } = formik;
|
const { setFieldValue } = formik;
|
||||||
|
|
||||||
// ===== CATEGORY OPTIONS (SAPRONAK or BOP) =====
|
// ===== CATEGORY OPTIONS (SAPRONAK or BOP) =====
|
||||||
@@ -187,15 +196,11 @@ const SuppliersTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterCategoryChange = useCallback(
|
const handleFilterCategoryChange = (
|
||||||
(val: OptionType | OptionType[] | null) => {
|
val: OptionType | OptionType[] | null
|
||||||
const option = val as OptionType | null;
|
) => {
|
||||||
const categoryId = option?.value ? String(option.value) : null;
|
setFieldValue('category', val);
|
||||||
|
};
|
||||||
setFieldValue('category_id', categoryId);
|
|
||||||
},
|
|
||||||
[setFieldValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterFlagChange = useCallback(
|
const handleFilterFlagChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
@@ -213,13 +218,13 @@ const SuppliersTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
// ===== FILTER HELPERS =====
|
||||||
const categoryIdValue = useMemo(() => {
|
// const categoryIdValue = useMemo(() => {
|
||||||
if (!formik.values.category_id) return null;
|
// if (!formik.values.category_id) return null;
|
||||||
return (
|
// return (
|
||||||
categoryOptions.find((opt) => opt.value === formik.values.category_id) ||
|
// categoryOptions.find((opt) => opt.value === formik.values.category_id) ||
|
||||||
null
|
// null
|
||||||
);
|
// );
|
||||||
}, [formik.values.category_id, categoryOptions]);
|
// }, [formik.values.category_id, categoryOptions]);
|
||||||
|
|
||||||
const flagValue = useMemo(() => {
|
const flagValue = useMemo(() => {
|
||||||
if (formik.values.flag === null) return null;
|
if (formik.values.flag === null) return null;
|
||||||
@@ -243,14 +248,6 @@ const SuppliersTable = () => {
|
|||||||
}
|
}
|
||||||
}, [filterModal.open, tableFilterState.flagFilter, setFieldValue]);
|
}, [filterModal.open, tableFilterState.flagFilter, setFieldValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('suppliers-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -269,8 +266,7 @@ const SuppliersTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -330,6 +326,11 @@ const SuppliersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'bank_name',
|
||||||
|
header: 'Nama Bank',
|
||||||
|
cell: (props) => props.row.original.bank_name || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'address',
|
accessorKey: 'address',
|
||||||
header: 'Alamat',
|
header: 'Alamat',
|
||||||
@@ -491,13 +492,13 @@ const SuppliersTable = () => {
|
|||||||
<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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Kategori'
|
label='Kategori'
|
||||||
placeholder='Pilih Kategori'
|
placeholder='Pilih Kategori'
|
||||||
options={categoryOptions}
|
options={categoryOptions}
|
||||||
value={categoryIdValue}
|
value={formik.values.category}
|
||||||
onChange={handleFilterCategoryChange}
|
onChange={handleFilterCategoryChange}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -517,13 +518,9 @@ const SuppliersTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<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='button'
|
type='reset'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { string, boolean, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const SupplierFilterSchema = object().shape({
|
export const SupplierFilterSchema = Yup.object().shape({
|
||||||
category_id: string().nullable(),
|
category: Yup.object({
|
||||||
flag: boolean().nullable(),
|
value: Yup.string().required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
|
||||||
|
flag: Yup.boolean().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SupplierFilterType = {
|
export type SupplierFilterType = {
|
||||||
category_id: string | null;
|
category?: OptionType<string>;
|
||||||
flag: boolean | null;
|
flag: boolean | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export const SupplierFormSchema = Yup.object({
|
|||||||
npwp: Yup.string()
|
npwp: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
||||||
.required('Nomor NPWP wajib diisi!'),
|
.required('Nomor NPWP wajib diisi!'),
|
||||||
|
bank_name: Yup.string()
|
||||||
|
.min(3, 'Nama bank minimal 3 karakter!')
|
||||||
|
.required('Nama bank wajib diisi!'),
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const SupplierForm = ({
|
|||||||
email: initialValues?.email ?? '',
|
email: initialValues?.email ?? '',
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
npwp: initialValues?.npwp ?? '',
|
npwp: initialValues?.npwp ?? '',
|
||||||
|
bank_name: initialValues?.bank_name ?? '',
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
due_date: initialValues?.due_date ?? 1,
|
due_date: initialValues?.due_date ?? 1,
|
||||||
};
|
};
|
||||||
@@ -149,6 +150,7 @@ const SupplierForm = ({
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
npwp: values.npwp,
|
npwp: values.npwp,
|
||||||
|
bank_name: values.bank_name,
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
due_date: parseInt(values.due_date.toString()),
|
due_date: parseInt(values.due_date.toString()),
|
||||||
};
|
};
|
||||||
@@ -368,6 +370,22 @@ const SupplierForm = ({
|
|||||||
errorMessage={formik.errors.npwp}
|
errorMessage={formik.errors.npwp}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
required
|
||||||
|
label='Nama Bank'
|
||||||
|
name='bank_name'
|
||||||
|
placeholder='Masukkan nama bank supplier'
|
||||||
|
value={formik.values.bank_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
||||||
|
}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.bank_name}
|
||||||
|
readOnly={formType === 'detail'}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,8 +20,6 @@ import { Uom } from '@/types/api/master-data/uom';
|
|||||||
import { UomApi } from '@/services/api/master-data';
|
import { UomApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -103,9 +101,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UomsTable = () => {
|
const UomsTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -114,22 +109,16 @@ const UomsTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: searchValue,
|
search: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'uom-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('uoms-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -146,8 +135,7 @@ const UomsTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -31,7 +24,6 @@ import { Warehouse } from '@/types/api/master-data/warehouse';
|
|||||||
import { WarehouseApi, AreaApi } from '@/services/api/master-data';
|
import { WarehouseApi, AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import {
|
import {
|
||||||
WarehouseFilterSchema,
|
WarehouseFilterSchema,
|
||||||
WarehouseFilterType,
|
WarehouseFilterType,
|
||||||
@@ -120,9 +112,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WarehousesTable = () => {
|
const WarehousesTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -141,6 +130,8 @@ const WarehousesTable = () => {
|
|||||||
areaFilter: 'area_id',
|
areaFilter: 'area_id',
|
||||||
activeProjectFlockFilter: 'active_project_flock',
|
activeProjectFlockFilter: 'active_project_flock',
|
||||||
},
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'warehouses-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -149,27 +140,36 @@ const WarehousesTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<WarehouseFilterType>({
|
const formik = useFormik<WarehouseFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area_id: tableFilterState.areaFilter || null,
|
||||||
active_project_flock: false,
|
active_project_flock:
|
||||||
|
tableFilterState.activeProjectFlockFilter === 'true',
|
||||||
},
|
},
|
||||||
validationSchema: WarehouseFilterSchema,
|
validationSchema: WarehouseFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area_id || '', true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'activeProjectFlockFilter',
|
'activeProjectFlockFilter',
|
||||||
values.active_project_flock === true ? 'true' : ''
|
values.active_project_flock === true ? 'true' : '',
|
||||||
|
true
|
||||||
);
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('areaFilter', '');
|
|
||||||
updateFilter('activeProjectFlockFilter', '');
|
|
||||||
formik.setFieldValue('active_project_flock', false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setFieldValue } = formik;
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('areaFilter', '', true);
|
||||||
|
updateFilter('activeProjectFlockFilter', '', true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
area_id: null,
|
||||||
|
active_project_flock: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
// ===== AREA OPTIONS =====
|
// ===== AREA OPTIONS =====
|
||||||
const {
|
const {
|
||||||
@@ -243,26 +243,6 @@ const WarehousesTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (filterModal.open) {
|
|
||||||
const activeProjectFlockValue =
|
|
||||||
tableFilterState.activeProjectFlockFilter === 'true' ? true : false; // Default ke false (Semua Kandang)
|
|
||||||
setFieldValue('active_project_flock', activeProjectFlockValue);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
filterModal.open,
|
|
||||||
tableFilterState.activeProjectFlockFilter,
|
|
||||||
setFieldValue,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('warehouses-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -281,8 +261,7 @@ const WarehousesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -507,7 +486,7 @@ const WarehousesTable = () => {
|
|||||||
<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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
@@ -538,10 +517,7 @@ const WarehousesTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
onClick={formikResetHandler}
|
||||||
formik.resetForm();
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useModal } from '@/components/Modal';
|
|||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Dropdown from '@/components/Dropdown';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
@@ -23,7 +22,6 @@ import { Icon } from '@iconify/react';
|
|||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -45,6 +43,7 @@ import {
|
|||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -148,7 +147,6 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
||||||
@@ -174,6 +172,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
kandang_id: '',
|
kandang_id: '',
|
||||||
category: '',
|
category: '',
|
||||||
period: '',
|
period: '',
|
||||||
|
area_name: '',
|
||||||
|
location_name: '',
|
||||||
|
kandang_name: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -185,7 +186,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
category: 'category',
|
category: 'category',
|
||||||
period: 'period',
|
period: 'period',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: ['area_name', 'location_name', 'kandang_name'],
|
||||||
|
persist: true,
|
||||||
|
storeName: 'project-flock-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// ===== State =====
|
// ===== State =====
|
||||||
@@ -206,8 +211,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
|
||||||
useState(false);
|
|
||||||
const {
|
const {
|
||||||
isChickinApproveModalOpen,
|
isChickinApproveModalOpen,
|
||||||
isChickinApproveLoading,
|
isChickinApproveLoading,
|
||||||
@@ -257,6 +261,18 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
updateFilter('kandang_id', values.kandang_id || '');
|
updateFilter('kandang_id', values.kandang_id || '');
|
||||||
updateFilter('category', values.category || '');
|
updateFilter('category', values.category || '');
|
||||||
updateFilter('period', values.period || '');
|
updateFilter('period', values.period || '');
|
||||||
|
updateFilter(
|
||||||
|
'area_name',
|
||||||
|
areaValue?.label ? String(areaValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'location_name',
|
||||||
|
locationValue?.label ? String(locationValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'kandang_name',
|
||||||
|
kandangValue?.label ? String(kandangValue.label) : ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
@@ -266,6 +282,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
updateFilter('kandang_id', '');
|
updateFilter('kandang_id', '');
|
||||||
updateFilter('category', '');
|
updateFilter('category', '');
|
||||||
updateFilter('period', '');
|
updateFilter('period', '');
|
||||||
|
updateFilter('area_name', '');
|
||||||
|
updateFilter('location_name', '');
|
||||||
|
updateFilter('kandang_name', '');
|
||||||
setFilterAreaId(undefined);
|
setFilterAreaId(undefined);
|
||||||
setFilterLocationId(undefined);
|
setFilterLocationId(undefined);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
@@ -307,40 +326,55 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const periodOptions = useMemo(
|
|
||||||
() => [
|
|
||||||
{ value: '1', label: 'Periode 1' },
|
|
||||||
{ value: '2', label: 'Periode 2' },
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
// ===== FILTER HELPERS =====
|
||||||
const areaValue = useMemo(() => {
|
const areaValue = useMemo(() => {
|
||||||
if (!formik.values.area_id) return null;
|
if (!formik.values.area_id) return null;
|
||||||
return (
|
const found = areaOptions.find(
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
(opt) => String(opt.value) === formik.values.area_id
|
||||||
null
|
|
||||||
);
|
);
|
||||||
}, [formik.values.area_id, areaOptions]);
|
if (found) return found;
|
||||||
|
if (tableFilterState.area_name) {
|
||||||
|
return {
|
||||||
|
value: formik.values.area_id,
|
||||||
|
label: tableFilterState.area_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [formik.values.area_id, areaOptions, tableFilterState.area_name]);
|
||||||
|
|
||||||
const locationValue = useMemo(() => {
|
const locationValue = useMemo(() => {
|
||||||
if (!formik.values.location_id) return null;
|
if (!formik.values.location_id) return null;
|
||||||
return (
|
const found = locationOptions.find(
|
||||||
locationOptions.find(
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
);
|
||||||
}, [formik.values.location_id, locationOptions]);
|
if (found) return found;
|
||||||
|
if (tableFilterState.location_name) {
|
||||||
|
return {
|
||||||
|
value: formik.values.location_id,
|
||||||
|
label: tableFilterState.location_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [
|
||||||
|
formik.values.location_id,
|
||||||
|
locationOptions,
|
||||||
|
tableFilterState.location_name,
|
||||||
|
]);
|
||||||
|
|
||||||
const kandangValue = useMemo(() => {
|
const kandangValue = useMemo(() => {
|
||||||
if (!formik.values.kandang_id) return null;
|
if (!formik.values.kandang_id) return null;
|
||||||
return (
|
const found = kandangOptions.find(
|
||||||
kandangOptions.find(
|
(opt) => String(opt.value) === formik.values.kandang_id
|
||||||
(opt) => String(opt.value) === formik.values.kandang_id
|
|
||||||
) || null
|
|
||||||
);
|
);
|
||||||
}, [formik.values.kandang_id, kandangOptions]);
|
if (found) return found;
|
||||||
|
if (tableFilterState.kandang_name) {
|
||||||
|
return {
|
||||||
|
value: formik.values.kandang_id,
|
||||||
|
label: tableFilterState.kandang_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [formik.values.kandang_id, kandangOptions, tableFilterState.kandang_name]);
|
||||||
|
|
||||||
const categoryValue = useMemo(() => {
|
const categoryValue = useMemo(() => {
|
||||||
if (!formik.values.category) return null;
|
if (!formik.values.category) return null;
|
||||||
@@ -350,13 +384,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.category, categoryOptions]);
|
}, [formik.values.category, categoryOptions]);
|
||||||
|
|
||||||
const periodValue = useMemo(() => {
|
|
||||||
if (!formik.values.period) return null;
|
|
||||||
return (
|
|
||||||
periodOptions.find((opt) => opt.value === formik.values.period) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.period, periodOptions]);
|
|
||||||
|
|
||||||
// ===== FILTER DEPENDENCY HANDLERS =====
|
// ===== FILTER DEPENDENCY HANDLERS =====
|
||||||
const handleFilterAreaChange = (area: OptionType | null) => {
|
const handleFilterAreaChange = (area: OptionType | null) => {
|
||||||
const areaId = area?.value ? String(area.value) : undefined;
|
const areaId = area?.value ? String(area.value) : undefined;
|
||||||
@@ -425,18 +452,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('project-flock-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
setSearchValue(e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprovalHandler = async (
|
const confirmApprovalHandler = async (
|
||||||
notes: string,
|
notes: string,
|
||||||
approvalAction: 'APPROVED' | 'REJECTED'
|
approvalAction: 'APPROVED' | 'REJECTED'
|
||||||
@@ -554,6 +574,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
price: budget.price,
|
price: budget.price,
|
||||||
total_price: budget.qty * budget.price,
|
total_price: budget.qty * budget.price,
|
||||||
})) || [],
|
})) || [],
|
||||||
|
periode: createdProjectFlock.period ?? '-',
|
||||||
} as ProjectFlockFormValues;
|
} as ProjectFlockFormValues;
|
||||||
}, [createdProjectFlock]);
|
}, [createdProjectFlock]);
|
||||||
|
|
||||||
@@ -776,14 +797,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
|
||||||
setIsLoadingExportingToExcel(true);
|
|
||||||
|
|
||||||
toast.error('Not implemented yet!');
|
|
||||||
|
|
||||||
setIsLoadingExportingToExcel(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const bulkApproveClickHandler = () => {
|
const bulkApproveClickHandler = () => {
|
||||||
setApprovalAction('APPROVED');
|
setApprovalAction('APPROVED');
|
||||||
confirmModal.openModal();
|
confirmModal.openModal();
|
||||||
@@ -972,55 +985,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
|
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={['page', 'pageSize', 'search']}
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'area_name',
|
||||||
|
'location_name',
|
||||||
|
'kandang_name',
|
||||||
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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'
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={exportToExcelHandler}
|
|
||||||
isLoading={isLoadingExportingToExcel}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Export to Excel
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1349,18 +1324,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
isClearable={true}
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInputRadio
|
<NumberInput
|
||||||
label='Periode'
|
label='Periode'
|
||||||
placeholder='Pilih Periode'
|
name='period'
|
||||||
options={periodOptions}
|
placeholder='Masukkan Periode'
|
||||||
value={periodValue}
|
value={formik.values.period ?? ''}
|
||||||
onChange={(val) => {
|
onChange={formik.handleChange}
|
||||||
if (!Array.isArray(val)) {
|
onBlur={formik.handleBlur}
|
||||||
formik.setFieldValue('period', val?.value || null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isClearable
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type ProjectFlockFormSchemaType = {
|
|||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
location_id: number;
|
location_id: number;
|
||||||
|
periode: number | string;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
project_budgets: ProjectFlockBudgetsSchemaType[];
|
project_budgets: ProjectFlockBudgetsSchemaType[];
|
||||||
};
|
};
|
||||||
@@ -109,6 +110,12 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
|
|||||||
.min(1, 'Lokasi wajib diisi!')
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
.required('Lokasi wajib diisi!'),
|
.required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
|
// Period
|
||||||
|
periode: Yup.number()
|
||||||
|
.typeError('Periode harus berupa angka!')
|
||||||
|
.min(1, 'Periode minimal 1!')
|
||||||
|
.required('Periode wajib diisi!'),
|
||||||
|
|
||||||
kandang_ids: Yup.array()
|
kandang_ids: Yup.array()
|
||||||
.of(Yup.number().required('Kandang tidak valid!'))
|
.of(Yup.number().required('Kandang tidak valid!'))
|
||||||
.min(1, 'Minimal harus ada 1 kandang!')
|
.min(1, 'Minimal harus ada 1 kandang!')
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ export const ProjectFlockFormConfirmationTable = ({
|
|||||||
label: 'Standar Produksi',
|
label: 'Standar Produksi',
|
||||||
value: projectFlockForm?.production_standard?.label ?? '-',
|
value: projectFlockForm?.production_standard?.label ?? '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Periode',
|
||||||
|
value: projectFlockForm?.periode ?? '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Informasi Kandang',
|
label: 'Informasi Kandang',
|
||||||
value: '',
|
value: '',
|
||||||
@@ -261,7 +265,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingFlocks,
|
isLoadingOptions: isLoadingFlocks,
|
||||||
options: optionsFlock,
|
options: optionsFlock,
|
||||||
loadMore: loadMoreFlock,
|
loadMore: loadMoreFlock,
|
||||||
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
} = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
location_id: selectedLocation,
|
location_id: selectedLocation,
|
||||||
area_id: selectedArea,
|
area_id: selectedArea,
|
||||||
@@ -279,7 +283,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
setInputValue: setInputValueLocation,
|
setInputValue: setInputValueLocation,
|
||||||
loadMore: loadMoreLocation,
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
? selectedArea
|
? selectedArea
|
||||||
@@ -291,7 +295,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
setInputValue: setInputValueProductionStandard,
|
setInputValue: setInputValueProductionStandard,
|
||||||
loadMore: loadMoreProductionStandard,
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,7 +311,7 @@ const ProjectFlockForm = ({
|
|||||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||||
|
|
||||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||||
`${selectedFlock?.toString()}/periods`,
|
selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
|
||||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -529,6 +533,7 @@ const ProjectFlockForm = ({
|
|||||||
kandang_ids: initialValues?.kandangs?.map(
|
kandang_ids: initialValues?.kandangs?.map(
|
||||||
(k: Kandang) => k.id
|
(k: Kandang) => k.id
|
||||||
) as number[],
|
) as number[],
|
||||||
|
periode: initialValues?.period ?? '',
|
||||||
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
||||||
return {
|
return {
|
||||||
nonstock: {
|
nonstock: {
|
||||||
@@ -568,6 +573,7 @@ const ProjectFlockForm = ({
|
|||||||
category: values.category as string,
|
category: values.category as string,
|
||||||
production_standard_id: values.production_standard_id as number,
|
production_standard_id: values.production_standard_id as number,
|
||||||
location_id: values.location_id as number,
|
location_id: values.location_id as number,
|
||||||
|
periode: parseInt(values.periode as unknown as string),
|
||||||
kandang_ids: values.kandang_ids as number[],
|
kandang_ids: values.kandang_ids as number[],
|
||||||
project_budgets: values.project_budgets.flatMap((budget) => {
|
project_budgets: values.project_budgets.flatMap((budget) => {
|
||||||
return {
|
return {
|
||||||
@@ -793,6 +799,7 @@ const ProjectFlockForm = ({
|
|||||||
formik.values.kandang_ids?.includes(kandang.id)
|
formik.values.kandang_ids?.includes(kandang.id)
|
||||||
)?.period
|
)?.period
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const inputPeriod =
|
const inputPeriod =
|
||||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||||
|
|
||||||
@@ -1022,12 +1029,18 @@ const ProjectFlockForm = ({
|
|||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='period'
|
name='periode'
|
||||||
label='Periode'
|
label='Periode'
|
||||||
disabled
|
|
||||||
readOnly
|
|
||||||
placeholder='Periode Flock'
|
placeholder='Periode Flock'
|
||||||
value={selectedLocation ? inputPeriod : ''}
|
value={formik.values.periode}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
allowNegative={false}
|
||||||
|
decimalScale={0}
|
||||||
|
isError={
|
||||||
|
formik.touched.periode && Boolean(formik.errors.periode)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.periode as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, {
|
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||||
useCallback,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
} from 'react';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
@@ -18,6 +12,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -39,13 +34,11 @@ import Table from '@/components/Table';
|
|||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
import { getRecordingRestriction } from './recording-utils';
|
import { getRecordingRestriction } from './recording-utils';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
@@ -75,6 +68,26 @@ const getStatusBadgeColor = (status: string): Color => {
|
|||||||
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRecordingApproved = (recording: Recording): boolean => {
|
||||||
|
return (
|
||||||
|
recording.approval?.action === 'APPROVED' &&
|
||||||
|
recording.approval?.step_name === 'Disetujui'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const recordingApprovalStatusOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'CREATED', label: 'Pengajuan' },
|
||||||
|
{ value: 'UPDATED', label: 'Diperbarui' },
|
||||||
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectFlockCategoryOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'GROWING', label: 'Growing' },
|
||||||
|
{ value: 'LAYING', label: 'Laying' },
|
||||||
|
];
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
@@ -266,80 +279,111 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RecordingTable = () => {
|
const RecordingTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
areaFilter: OptionType<number> | null;
|
||||||
|
locationFilter: OptionType<number> | null;
|
||||||
|
projectFlockFilter: OptionType<number> | null;
|
||||||
|
kandangFilter: OptionType<number> | null;
|
||||||
|
projectFlockKandangFilter: number | null;
|
||||||
|
approvalStatusFilter: OptionType<string> | null;
|
||||||
|
projectFlockCategoryFilter: OptionType<string> | null;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
areaFilter: '',
|
areaFilter: null,
|
||||||
locationFilter: '',
|
locationFilter: null,
|
||||||
kandangFilter: '',
|
projectFlockFilter: null,
|
||||||
projectFlockKandangFilter: '',
|
kandangFilter: null,
|
||||||
|
projectFlockKandangFilter: null,
|
||||||
|
approvalStatusFilter: null,
|
||||||
|
projectFlockCategoryFilter: null,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
search: 'search',
|
search: 'search',
|
||||||
|
areaFilter: 'area_id',
|
||||||
|
locationFilter: 'location_id',
|
||||||
|
projectFlockFilter: 'project_flock_id',
|
||||||
kandangFilter: 'kandang_id',
|
kandangFilter: 'kandang_id',
|
||||||
projectFlockKandangFilter: 'project_flock_kandang_id',
|
projectFlockKandangFilter: 'project_flock_kandang_id',
|
||||||
|
approvalStatusFilter: 'approval_status',
|
||||||
|
projectFlockCategoryFilter: 'project_flock_category',
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
persist: true,
|
||||||
updateFilter('search', searchValue);
|
storeName: 'recording-table',
|
||||||
}, [searchValue, updateFilter]);
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
// ===== FILTER STATE =====
|
|
||||||
const [filterArea, setFilterArea] = useState<OptionType | null>(null);
|
|
||||||
const [filterLocation, setFilterLocation] = useState<OptionType | null>(null);
|
|
||||||
const [filterProjectFlock, setFilterProjectFlock] =
|
|
||||||
useState<OptionType | null>(null);
|
|
||||||
const [filterKandang, setFilterKandang] = useState<OptionType | null>(null);
|
|
||||||
const [, setFilterProjectFlockKandangId] = useState<number | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [filterLocationAreaId, setFilterLocationAreaId] = useState<string>('');
|
|
||||||
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
|
||||||
useState<string>('');
|
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<RecordingFilterType>({
|
const formik = useFormik<RecordingFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area_id: tableFilterState.areaFilter,
|
||||||
location_id: null,
|
location_id: tableFilterState.locationFilter,
|
||||||
kandang_id: null,
|
project_flock_id: tableFilterState.projectFlockFilter,
|
||||||
project_flock_kandang_id: null,
|
kandang_id: tableFilterState.kandangFilter,
|
||||||
|
project_flock_kandang_id: tableFilterState.projectFlockKandangFilter,
|
||||||
|
approval_status: tableFilterState.approvalStatusFilter,
|
||||||
|
project_flock_category: tableFilterState.projectFlockCategoryFilter,
|
||||||
},
|
},
|
||||||
validationSchema: RecordingFilterSchema,
|
validationSchema: RecordingFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area_id, true);
|
||||||
updateFilter('locationFilter', values.location_id || '');
|
updateFilter('locationFilter', values.location_id, true);
|
||||||
updateFilter('kandangFilter', values.kandang_id || '');
|
updateFilter('projectFlockFilter', values.project_flock_id, true);
|
||||||
|
updateFilter('kandangFilter', values.kandang_id, true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'projectFlockKandangFilter',
|
'projectFlockKandangFilter',
|
||||||
values.project_flock_kandang_id || ''
|
values.project_flock_kandang_id,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter('approvalStatusFilter', values.approval_status, true);
|
||||||
|
updateFilter(
|
||||||
|
'projectFlockCategoryFilter',
|
||||||
|
values.project_flock_category,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('areaFilter', '');
|
|
||||||
updateFilter('locationFilter', '');
|
|
||||||
updateFilter('kandangFilter', '');
|
|
||||||
updateFilter('projectFlockKandangFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('areaFilter', null, true);
|
||||||
|
updateFilter('locationFilter', null, true);
|
||||||
|
updateFilter('projectFlockFilter', null, true);
|
||||||
|
updateFilter('kandangFilter', null, true);
|
||||||
|
updateFilter('projectFlockKandangFilter', null, true);
|
||||||
|
updateFilter('approvalStatusFilter', null, true);
|
||||||
|
updateFilter('projectFlockCategoryFilter', null, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
area_id: null,
|
||||||
|
location_id: null,
|
||||||
|
project_flock_id: null,
|
||||||
|
kandang_id: null,
|
||||||
|
project_flock_kandang_id: null,
|
||||||
|
approval_status: null,
|
||||||
|
project_flock_category: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { project_flock_id, kandang_id } = formik.values;
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
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) =>
|
||||||
@@ -355,10 +399,14 @@ const RecordingTable = () => {
|
|||||||
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
|
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
||||||
|
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
||||||
|
|
||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const exportProgressInputModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: recordings,
|
data: recordings,
|
||||||
@@ -370,13 +418,6 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
||||||
const locationParams = useMemo(() => {
|
|
||||||
if (filterLocationAreaId) {
|
|
||||||
return { area_id: filterLocationAreaId };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [filterLocationAreaId]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
@@ -387,7 +428,9 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'search',
|
'search',
|
||||||
locationParams
|
{
|
||||||
|
area_id: String(formik.values.area_id?.value),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -402,13 +445,6 @@ const RecordingTable = () => {
|
|||||||
'search'
|
'search'
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectFlockParams = useMemo(() => {
|
|
||||||
if (filterProjectFlockLocationId) {
|
|
||||||
return { location_id: filterProjectFlockLocationId };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [filterProjectFlockLocationId]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setProjectFlockInputValue,
|
setInputValue: setProjectFlockInputValue,
|
||||||
options: projectFlockOptions,
|
options: projectFlockOptions,
|
||||||
@@ -420,34 +456,41 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'flock_name',
|
'flock_name',
|
||||||
'search',
|
'search',
|
||||||
projectFlockParams
|
{
|
||||||
|
location_id: String(formik.values.location_id?.value),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const kandangOptions = useMemo(() => {
|
const kandangOptions = useMemo(() => {
|
||||||
if (!filterProjectFlock || !projectFlocksRawData) return [];
|
if (!project_flock_id || !projectFlocksRawData) return [];
|
||||||
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
||||||
|
|
||||||
const data = projectFlocksRawData.data as ProjectFlock[];
|
const data = projectFlocksRawData.data as ProjectFlock[];
|
||||||
const selectedProjectFlockData = data.find(
|
const selectedProjectFlockData = data.find((pf) =>
|
||||||
(pf) => pf.id === filterProjectFlock.value
|
pf.id === formik.values.project_flock_id?.value
|
||||||
|
? Number(formik.values.project_flock_id.value)
|
||||||
|
: 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedProjectFlockData?.kandangs) return [];
|
if (!selectedProjectFlockData?.kandangs) return [];
|
||||||
|
|
||||||
return selectedProjectFlockData.kandangs.map((k) => ({
|
return selectedProjectFlockData.kandangs.map((k) => ({
|
||||||
value: k.id,
|
value: k.id,
|
||||||
label: k.name || '',
|
label: k.name || '',
|
||||||
}));
|
}));
|
||||||
}, [filterProjectFlock, projectFlocksRawData]);
|
}, [project_flock_id, projectFlocksRawData]);
|
||||||
|
|
||||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
if (!filterProjectFlock || !filterKandang) return null;
|
if (!project_flock_id?.value || !kandang_id?.value) return null;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
project_flock_id: filterProjectFlock.value.toString(),
|
project_flock_id: project_flock_id.value.toString(),
|
||||||
kandang_id: filterKandang.value.toString(),
|
kandang_id: kandang_id.value.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||||
}, [filterProjectFlock, filterKandang]);
|
}, [project_flock_id, kandang_id]);
|
||||||
|
|
||||||
const { data: projectFlockKandangLookupData } = useSWR(
|
const { data: projectFlockKandangLookupData } = useSWR(
|
||||||
projectFlockKandangLookupUrl,
|
projectFlockKandangLookupUrl,
|
||||||
@@ -469,118 +512,45 @@ const RecordingTable = () => {
|
|||||||
? projectFlockKandangLookupData.data
|
? projectFlockKandangLookupData.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const formikRef = useRef(formik);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
formikRef.current = formik;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectFlockKandangLookup?.id) {
|
if (projectFlockKandangLookup?.id) {
|
||||||
const pfkId = String(projectFlockKandangLookup.id);
|
const pfkId = String(projectFlockKandangLookup.id);
|
||||||
setFilterProjectFlockKandangId(projectFlockKandangLookup.id);
|
formik.setFieldValue('project_flock_kandang_id', pfkId);
|
||||||
formikRef.current.setFieldValue('project_flock_kandang_id', pfkId);
|
|
||||||
} else {
|
} else {
|
||||||
setFilterProjectFlockKandangId(undefined);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
formikRef.current.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
}
|
}
|
||||||
}, [projectFlockKandangLookup]);
|
}, [projectFlockKandangLookup]);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = useCallback(
|
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('area_id', val);
|
||||||
const area = val as OptionType | null;
|
formik.setFieldValue('location_id', null);
|
||||||
const areaId = area?.value ? String(area.value) : null;
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
formik.setFieldValue('kandang_id', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
};
|
||||||
|
|
||||||
formik.setFieldValue('area_id', areaId);
|
const handleFilterLocationChange = (
|
||||||
formik.setFieldValue('location_id', null);
|
val: OptionType | OptionType[] | null
|
||||||
formik.setFieldValue('kandang_id', null);
|
) => {
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('location_id', val);
|
||||||
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
formik.setFieldValue('kandang_id', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
};
|
||||||
|
|
||||||
setFilterArea(area);
|
const handleFilterProjectFlockChange = (
|
||||||
setFilterLocation(null);
|
val: OptionType | OptionType[] | null
|
||||||
setFilterProjectFlock(null);
|
) => {
|
||||||
setFilterKandang(null);
|
formik.setFieldValue('project_flock_id', val);
|
||||||
setFilterLocationAreaId(areaId || '');
|
formik.setFieldValue('kandang_id', null);
|
||||||
setFilterProjectFlockLocationId('');
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
},
|
};
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterLocationChange = useCallback(
|
const handleFilterKandangChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('kandang_id', val);
|
||||||
const location = val as OptionType | null;
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
const locationId = location?.value ? String(location.value) : null;
|
};
|
||||||
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterLocation(location);
|
|
||||||
setFilterProjectFlock(null);
|
|
||||||
setFilterKandang(null);
|
|
||||||
setFilterProjectFlockLocationId(locationId || '');
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterProjectFlockChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const projectFlock = val as OptionType | null;
|
|
||||||
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterProjectFlock(projectFlock);
|
|
||||||
setFilterKandang(null);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterKandangChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const kandang = val as OptionType | null;
|
|
||||||
const kandangId = kandang?.value ? String(kandang.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('kandang_id', kandangId);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterKandang(kandang);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const areaIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.area_id) return null;
|
|
||||||
return (
|
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.area_id, areaOptions]);
|
|
||||||
|
|
||||||
const locationIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.location_id) return null;
|
|
||||||
return (
|
|
||||||
locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.location_id, locationOptions]);
|
|
||||||
|
|
||||||
const projectFlockIdValue = useMemo(() => {
|
|
||||||
if (!filterProjectFlock) return null;
|
|
||||||
return filterProjectFlock;
|
|
||||||
}, [filterProjectFlock]);
|
|
||||||
|
|
||||||
const kandangIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.kandang_id) return null;
|
|
||||||
return (
|
|
||||||
kandangOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.kandang_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.kandang_id, kandangOptions]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -588,25 +558,9 @@ const RecordingTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecordingApproved = useCallback((recording: Recording): boolean => {
|
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
return (
|
updateFilter('search', e.target.value, true);
|
||||||
recording.approval?.action === 'APPROVED' &&
|
};
|
||||||
recording.approval?.step_name === 'Disetujui'
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('recording-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
setSearchValue(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
},
|
|
||||||
[updateFilter, setSearchValue, setPage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const singleDeleteHandler = async () => {
|
const singleDeleteHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
@@ -698,6 +652,60 @@ const RecordingTable = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetExportProgressForm = useCallback(() => {
|
||||||
|
setExportProgressStartDate('');
|
||||||
|
setExportProgressEndDate('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exportProgressStartDateChangeHandler = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setExportProgressStartDate(e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportProgressEndDateChangeHandler = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setExportProgressEndDate(e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportProgressInputToExcelClickHandler = useCallback(() => {
|
||||||
|
resetExportProgressForm();
|
||||||
|
exportProgressInputModal.openModal();
|
||||||
|
}, [exportProgressInputModal, resetExportProgressForm]);
|
||||||
|
|
||||||
|
const submitExportProgressInputHandler = useCallback(async () => {
|
||||||
|
if (!exportProgressStartDate || !exportProgressEndDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExportProgressLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await RecordingApi.exportInputProgressToExcel(
|
||||||
|
exportProgressStartDate,
|
||||||
|
exportProgressEndDate
|
||||||
|
);
|
||||||
|
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
toast.success('Ekspor berhasil');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsExportProgressLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
exportProgressEndDate,
|
||||||
|
exportProgressInputModal,
|
||||||
|
exportProgressStartDate,
|
||||||
|
resetExportProgressForm,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(recordings) && recordings.data) {
|
if (isResponseSuccess(recordings) && recordings.data) {
|
||||||
const newSelection: Record<string, boolean> = {};
|
const newSelection: Record<string, boolean> = {};
|
||||||
@@ -858,7 +866,8 @@ const RecordingTable = () => {
|
|||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
{props.row.original.day} (Minggu ke-
|
{props.row.original.day} (Minggu ke-
|
||||||
{props.row.original.project_flock.production_standart.week})
|
{props.row.original.week} hari ke-
|
||||||
|
{props.row.original.excess_days})
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1104,7 +1113,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)}%`
|
? `${value.toFixed(2)} butir`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1120,7 +1129,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center text-gray-600'>
|
<div className='text-center text-gray-600'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)}%`
|
? `${value.toFixed(2)} btr`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1368,6 +1377,16 @@ const RecordingTable = () => {
|
|||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Export to Excel
|
Export to Excel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportProgressInputToExcelClickHandler}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor Input Progress (Excel)
|
||||||
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1446,13 +1465,13 @@ const RecordingTable = () => {
|
|||||||
<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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={areaIdValue}
|
value={formik.values.area_id}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -1465,13 +1484,13 @@ const RecordingTable = () => {
|
|||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationIdValue}
|
value={formik.values.location_id}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isDisabled={!filterArea}
|
isDisabled={!formik.values.area_id?.value}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1479,13 +1498,13 @@ const RecordingTable = () => {
|
|||||||
label='Project Flock'
|
label='Project Flock'
|
||||||
placeholder='Pilih Project Flock'
|
placeholder='Pilih Project Flock'
|
||||||
options={projectFlockOptions}
|
options={projectFlockOptions}
|
||||||
value={projectFlockIdValue}
|
value={formik.values.project_flock_id}
|
||||||
onChange={handleFilterProjectFlockChange}
|
onChange={handleFilterProjectFlockChange}
|
||||||
onInputChange={setProjectFlockInputValue}
|
onInputChange={setProjectFlockInputValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!filterLocation}
|
isDisabled={!formik.values.location_id?.value}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1493,11 +1512,35 @@ const RecordingTable = () => {
|
|||||||
label='Kandang'
|
label='Kandang'
|
||||||
placeholder='Pilih Kandang'
|
placeholder='Pilih Kandang'
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
value={kandangIdValue}
|
value={formik.values.kandang_id}
|
||||||
onChange={handleFilterKandangChange}
|
onChange={handleFilterKandangChange}
|
||||||
isLoading={!filterProjectFlock}
|
isLoading={!formik.values.project_flock_id?.value}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock_id?.value}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={projectFlockCategoryOptions}
|
||||||
|
value={formik.values.project_flock_category}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('project_flock_category', val);
|
||||||
|
}}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status Approval'
|
||||||
|
placeholder='Pilih Status Approval'
|
||||||
|
options={recordingApprovalStatusOptions}
|
||||||
|
value={formik.values.approval_status}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('approval_status', val);
|
||||||
|
}}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!filterProjectFlock}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1505,30 +1548,16 @@ const RecordingTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<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='button'
|
type='reset'
|
||||||
variant='soft'
|
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'
|
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'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
setFilterArea(null);
|
|
||||||
setFilterLocation(null);
|
|
||||||
setFilterProjectFlock(null);
|
|
||||||
setFilterKandang(null);
|
|
||||||
setFilterLocationAreaId('');
|
|
||||||
setFilterProjectFlockLocationId('');
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<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={
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
!formik.isValid ||
|
|
||||||
formik.isSubmitting ||
|
|
||||||
!formik.values.kandang_id
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1551,6 +1580,76 @@ const RecordingTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
ref={exportProgressInputModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Ekspor Input Progress
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_start_date'
|
||||||
|
label='Tanggal Mulai'
|
||||||
|
value={exportProgressStartDate}
|
||||||
|
onChange={exportProgressStartDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_end_date'
|
||||||
|
label='Tanggal Selesai'
|
||||||
|
value={exportProgressEndDate}
|
||||||
|
onChange={exportProgressEndDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
onClick={submitExportProgressInputHandler}
|
||||||
|
isLoading={isExportProgressLoading}
|
||||||
|
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const RecordingFilterSchema = object().shape({
|
export const RecordingFilterSchema = Yup.object().shape({
|
||||||
area_id: string().nullable(),
|
area_id: Yup.object({
|
||||||
location_id: string().nullable(),
|
value: Yup.number().nullable(),
|
||||||
kandang_id: string().nullable(),
|
label: Yup.string().nullable(),
|
||||||
project_flock_kandang_id: string().nullable(),
|
}).nullable(),
|
||||||
|
location_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
kandang_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_kandang_id: Yup.number().nullable(),
|
||||||
|
approval_status: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_category: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RecordingFilterType = {
|
export type RecordingFilterType = {
|
||||||
area_id: string | null;
|
area_id: OptionType<number> | null;
|
||||||
location_id: string | null;
|
location_id: OptionType<number> | null;
|
||||||
kandang_id: string | null;
|
project_flock_id: OptionType<number> | null;
|
||||||
project_flock_kandang_id: string | null;
|
kandang_id: OptionType<number> | null;
|
||||||
|
project_flock_kandang_id: number | null;
|
||||||
|
approval_status: OptionType<string> | null;
|
||||||
|
project_flock_category: OptionType<string> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {
|
|||||||
CreateGrowingRecordingPayload,
|
CreateGrowingRecordingPayload,
|
||||||
CreateLayingRecordingPayload,
|
CreateLayingRecordingPayload,
|
||||||
CreateEggPayload,
|
CreateEggPayload,
|
||||||
|
RecordingStock,
|
||||||
} from '@/types/api/production/recording';
|
} from '@/types/api/production/recording';
|
||||||
|
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
||||||
|
|
||||||
type RecordingGrowingFormSchemaType = {
|
type RecordingGrowingFormSchemaType = {
|
||||||
record_date: string;
|
record_date: string;
|
||||||
@@ -29,11 +31,19 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
} | null;
|
} | null;
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
stocks: {
|
stocks: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id:
|
||||||
|
| {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
depletions: {
|
depletions: {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
source_product_warehouse_id?: number;
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
@@ -41,34 +51,48 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
|
|
||||||
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||||
eggs: {
|
eggs: {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StockSchema = {
|
export type StockSchema = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DepletionSchema = {
|
export type DepletionSchema = {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
source_product_warehouse_id?: number;
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EggSchema = {
|
export type EggSchema = {
|
||||||
product_warehouse_id?: number;
|
product_warehouse_id?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
})
|
||||||
.required('Produk wajib diisi!')
|
.required('Produk wajib diisi!')
|
||||||
.min(1, 'Produk wajib diisi!')
|
.typeError('Produk wajib diisi!'),
|
||||||
.typeError('Produk harus berupa angka!'),
|
|
||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.required('Jumlah penggunaan wajib diisi!')
|
.required('Jumlah penggunaan wajib diisi!')
|
||||||
.moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
|
.moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
|
||||||
@@ -76,9 +100,12 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Depletions harus berupa angka!'),
|
.nullable(),
|
||||||
source_product_warehouse_id: Yup.number()
|
source_product_warehouse_id: Yup.number()
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Gudang sumber harus berupa angka!'),
|
.typeError('Gudang sumber harus berupa angka!'),
|
||||||
@@ -88,9 +115,12 @@ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Kondisi telur harus berupa angka!'),
|
.nullable(),
|
||||||
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
||||||
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||||
});
|
});
|
||||||
@@ -248,14 +278,18 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
initialValues?.project_flock?.project_flock_kandang_id ??
|
initialValues?.project_flock?.project_flock_kandang_id ??
|
||||||
0,
|
0,
|
||||||
stocks: initialValues?.stocks?.map((stock) => ({
|
stocks: initialValues?.stocks?.map((stock) => ({
|
||||||
product_warehouse_id: stock.product_warehouse_id,
|
product_warehouse_id: {
|
||||||
|
value: stock.product_warehouse_id,
|
||||||
|
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
||||||
|
},
|
||||||
qty:
|
qty:
|
||||||
(stock as { qty?: number; usage_amount?: number }).qty ||
|
(stock as RecordingStock).qty ||
|
||||||
(stock as { qty?: number; usage_amount?: number }).usage_amount ||
|
((stock as RecordingStock).usage_amount || 0) +
|
||||||
|
((stock as RecordingStock).pending_qty || 0) ||
|
||||||
'',
|
'',
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: undefined,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -263,13 +297,16 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
(
|
(
|
||||||
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
||||||
) => ({
|
) => ({
|
||||||
product_warehouse_id: depletion.product_warehouse_id,
|
product_warehouse_id: {
|
||||||
|
value: Number(depletion.product_warehouse_id ?? 0),
|
||||||
|
label: getProductWarehouseOptionLabel(depletion.product_warehouse),
|
||||||
|
},
|
||||||
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
||||||
qty: depletion.qty,
|
qty: depletion.qty,
|
||||||
})
|
})
|
||||||
) ?? [
|
) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: undefined,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -281,12 +318,15 @@ export const getRecordingLayingFormInitialValues = (
|
|||||||
...getRecordingGrowingFormInitialValues(initialValues),
|
...getRecordingGrowingFormInitialValues(initialValues),
|
||||||
|
|
||||||
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
||||||
product_warehouse_id: egg.product_warehouse_id,
|
product_warehouse_id: {
|
||||||
|
value: Number(egg.product_warehouse_id ?? 0),
|
||||||
|
label: getProductWarehouseOptionLabel(egg.product_warehouse),
|
||||||
|
},
|
||||||
qty: egg.qty,
|
qty: egg.qty,
|
||||||
weight: egg.weight,
|
weight: egg.weight,
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -31,12 +31,14 @@ import {
|
|||||||
RecordingApi,
|
RecordingApi,
|
||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
} from '@/services/api/production';
|
} from '@/services/api/production';
|
||||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
import { ProductionStandardApi, ProductApi } from '@/services/api/master-data';
|
||||||
import {
|
import {
|
||||||
ProductionStandard,
|
ProductionStandard,
|
||||||
StandardDetails,
|
StandardDetails,
|
||||||
} from '@/types/api/master-data/production-standard';
|
} from '@/types/api/master-data/production-standard';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
|
import { SystemSettingsApi } from '@/services/api/system-settings';
|
||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
|
|
||||||
@@ -499,6 +501,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
type,
|
type,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ===== MIGRATION MODE =====
|
||||||
|
const { data: systemSettingsResponse } = useSWR(
|
||||||
|
SystemSettingsApi.basePath,
|
||||||
|
SystemSettingsApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const isMigrationMode = useMemo(() => {
|
||||||
|
if (!isResponseSuccess(systemSettingsResponse)) return false;
|
||||||
|
const setting = systemSettingsResponse.data.find(
|
||||||
|
(s) => s.key === 'allow_negative_pakan_ovk'
|
||||||
|
);
|
||||||
|
return setting?.value === 'true';
|
||||||
|
}, [systemSettingsResponse]);
|
||||||
|
|
||||||
// ===== PAYLOAD CREATION HELPERS =====
|
// ===== PAYLOAD CREATION HELPERS =====
|
||||||
const createGrowingPayload = useCallback(
|
const createGrowingPayload = useCallback(
|
||||||
(values: RecordingGrowingFormValues) => {
|
(values: RecordingGrowingFormValues) => {
|
||||||
@@ -506,7 +522,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
? values.depletions
|
? values.depletions
|
||||||
?.filter((d) => d.product_warehouse_id && d.qty)
|
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||||
.map((depletion) => ({
|
.map((depletion) => ({
|
||||||
product_warehouse_id: depletion.product_warehouse_id!,
|
product_warehouse_id: depletion.product_warehouse_id?.value ?? 0,
|
||||||
...(depletion.source_product_warehouse_id && {
|
...(depletion.source_product_warehouse_id && {
|
||||||
source_product_warehouse_id:
|
source_product_warehouse_id:
|
||||||
depletion.source_product_warehouse_id,
|
depletion.source_product_warehouse_id,
|
||||||
@@ -517,9 +533,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const stocks = recordingRestriction.canEditStock
|
const stocks = recordingRestriction.canEditStock
|
||||||
? (values.stocks ?? [])
|
? (values.stocks ?? [])
|
||||||
.filter((s) => s.product_warehouse_id && s.qty)
|
.filter((s) => s.product_warehouse_id?.value && s.qty)
|
||||||
.map((stock) => ({
|
.map((stock) => ({
|
||||||
product_warehouse_id: stock.product_warehouse_id,
|
// In migration mode, product_warehouse_id field holds product.id;
|
||||||
|
// send it as product_id so the backend auto-creates the warehouse entry.
|
||||||
|
...(isMigrationMode
|
||||||
|
? { product_id: stock.product_warehouse_id?.value }
|
||||||
|
: { product_warehouse_id: stock.product_warehouse_id?.value }),
|
||||||
qty: Number(stock.qty) || 0,
|
qty: Number(stock.qty) || 0,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
@@ -531,15 +551,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
...(depletions.length > 0 && { depletions }),
|
...(depletions.length > 0 && { depletions }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[recordingRestriction.canEditStock, recordingRestriction.canEditDepletion]
|
[
|
||||||
|
isMigrationMode,
|
||||||
|
recordingRestriction.canEditStock,
|
||||||
|
recordingRestriction.canEditDepletion,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createLayingPayload = useCallback(
|
const createLayingPayload = useCallback(
|
||||||
(values: RecordingLayingFormValues) => {
|
(values: RecordingLayingFormValues) => {
|
||||||
const depletions = values.depletions
|
const depletions = values.depletions
|
||||||
?.filter((d) => d.product_warehouse_id && d.qty)
|
?.filter((d) => d.product_warehouse_id?.value && d.qty)
|
||||||
.map((depletion) => ({
|
.map((depletion) => ({
|
||||||
product_warehouse_id: depletion.product_warehouse_id!,
|
product_warehouse_id: depletion.product_warehouse_id?.value ?? 0,
|
||||||
...(depletion.source_product_warehouse_id && {
|
...(depletion.source_product_warehouse_id && {
|
||||||
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
||||||
}),
|
}),
|
||||||
@@ -549,7 +573,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const eggs = values.eggs
|
const eggs = values.eggs
|
||||||
?.filter((e) => e.product_warehouse_id && e.qty && e.weight)
|
?.filter((e) => e.product_warehouse_id && e.qty && e.weight)
|
||||||
.map((egg) => ({
|
.map((egg) => ({
|
||||||
product_warehouse_id: egg.product_warehouse_id!,
|
product_warehouse_id: egg.product_warehouse_id?.value ?? 0,
|
||||||
qty: Number(egg.qty) || 0,
|
qty: Number(egg.qty) || 0,
|
||||||
weight:
|
weight:
|
||||||
typeof egg.weight === 'number'
|
typeof egg.weight === 'number'
|
||||||
@@ -559,9 +583,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const stocks = recordingRestriction.canEditStock
|
const stocks = recordingRestriction.canEditStock
|
||||||
? values.stocks
|
? values.stocks
|
||||||
.filter((s) => s.product_warehouse_id && s.qty)
|
.filter((s) => s.product_warehouse_id?.value && s.qty)
|
||||||
.map((stock) => ({
|
.map((stock) => ({
|
||||||
product_warehouse_id: stock.product_warehouse_id,
|
...(isMigrationMode
|
||||||
|
? { product_id: stock.product_warehouse_id?.value }
|
||||||
|
: { product_warehouse_id: stock.product_warehouse_id?.value }),
|
||||||
qty: Number(stock.qty) || 0,
|
qty: Number(stock.qty) || 0,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
@@ -574,7 +600,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
...(eggs && eggs.length > 0 && { eggs }),
|
...(eggs && eggs.length > 0 && { eggs }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[recordingRestriction.canEditStock]
|
[isMigrationMode, recordingRestriction.canEditStock]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRecordingEditable = useCallback((recording?: Recording) => {
|
const isRecordingEditable = useCallback((recording?: Recording) => {
|
||||||
@@ -603,11 +629,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return true;
|
return true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// When migration mode ON: fetch all master PAKAN/OVK products (no warehouse entry needed).
|
||||||
|
// When migration mode OFF: fetch from product-warehouses as usual.
|
||||||
const {
|
const {
|
||||||
setInputValue: setStockProductInputValue,
|
setInputValue: setStockProductInputValue,
|
||||||
rawData: stockProducts,
|
rawData: stockProductsPW,
|
||||||
isLoadingOptions: isLoadingStockProducts,
|
isLoadingOptions: isLoadingStockProductsPW,
|
||||||
loadMore: loadMoreStockProducts,
|
loadMore: loadMoreStockProductsPW,
|
||||||
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
||||||
flags: 'PAKAN,OVK',
|
flags: 'PAKAN,OVK',
|
||||||
limit: '100',
|
limit: '100',
|
||||||
@@ -616,6 +644,29 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
|
...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setStockMasterInputValue,
|
||||||
|
rawData: stockProductsMaster,
|
||||||
|
isLoadingOptions: isLoadingStockProductsMaster,
|
||||||
|
loadMore: loadMoreStockProductsMaster,
|
||||||
|
} = useSelect(
|
||||||
|
isMigrationMode ? ProductApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search',
|
||||||
|
{ flags: 'PAKAN,OVK', limit: '100' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoadingStockProducts = isMigrationMode
|
||||||
|
? isLoadingStockProductsMaster
|
||||||
|
: isLoadingStockProductsPW;
|
||||||
|
const loadMoreStockProducts = isMigrationMode
|
||||||
|
? loadMoreStockProductsMaster
|
||||||
|
: loadMoreStockProductsPW;
|
||||||
|
const setStockInputValue = isMigrationMode
|
||||||
|
? setStockMasterInputValue
|
||||||
|
: setStockProductInputValue;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rawData: depletionProductsData,
|
rawData: depletionProductsData,
|
||||||
isLoadingOptions: isLoadingDepletionProducts,
|
isLoadingOptions: isLoadingDepletionProducts,
|
||||||
@@ -999,9 +1050,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const items: Array<ProductWarehouse | null | undefined> = [];
|
const items: Array<ProductWarehouse | null | undefined> = [];
|
||||||
|
|
||||||
if (isResponseSuccess(stockProducts)) {
|
if (!isMigrationMode && isResponseSuccess(stockProductsPW)) {
|
||||||
items.push(
|
items.push(
|
||||||
...((stockProducts.data as unknown as ProductWarehouse[]) ?? [])
|
...((stockProductsPW.data as unknown as ProductWarehouse[]) ?? [])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,7 +1086,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
mergeKnownProductWarehouses(items);
|
mergeKnownProductWarehouses(items);
|
||||||
}, [
|
}, [
|
||||||
stockProducts,
|
isMigrationMode,
|
||||||
|
stockProductsPW,
|
||||||
depletionProductsData,
|
depletionProductsData,
|
||||||
eggProductsData,
|
eggProductsData,
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -1066,9 +1118,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const unifiedStockProducts = useMemo(() => {
|
const unifiedStockProducts = useMemo(() => {
|
||||||
const options = isResponseSuccess(stockProducts)
|
if (isMigrationMode) {
|
||||||
|
// In migration mode, show all master PAKAN/OVK products (no warehouse context).
|
||||||
|
// value = product.id; submission will send product_id to the backend.
|
||||||
|
const options: OptionType[] = isResponseSuccess(stockProductsMaster)
|
||||||
|
? (stockProductsMaster.data as unknown as Product[])
|
||||||
|
.map((p) => ({ value: p.id, label: p.name }))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
: [];
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = isResponseSuccess(stockProductsPW)
|
||||||
? buildProductWarehouseOptions(
|
? buildProductWarehouseOptions(
|
||||||
stockProducts.data as unknown as ProductWarehouse[]
|
stockProductsPW.data as unknown as ProductWarehouse[]
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -1085,7 +1148,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [
|
}, [
|
||||||
stockProducts,
|
isMigrationMode,
|
||||||
|
stockProductsMaster,
|
||||||
|
stockProductsPW,
|
||||||
buildProductWarehouseOptions,
|
buildProductWarehouseOptions,
|
||||||
initialValues,
|
initialValues,
|
||||||
type,
|
type,
|
||||||
@@ -1204,6 +1269,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In migration mode (edit), the dropdown options use product.id as their value,
|
||||||
|
// but the API returns product_warehouse_id (PW entity ID). Remap so the dropdown
|
||||||
|
// can match the correct option. The product ID is available on the nested
|
||||||
|
// product_warehouse object returned by the API.
|
||||||
|
if (isMigrationMode && type === 'edit' && initialValues?.stocks?.length) {
|
||||||
|
baseValues.stocks = initialValues.stocks.map((stock) => ({
|
||||||
|
product_warehouse_id: {
|
||||||
|
value: Number(
|
||||||
|
stock.product_warehouse?.product_id ?? stock.product_warehouse_id
|
||||||
|
),
|
||||||
|
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
||||||
|
},
|
||||||
|
qty: stock.usage_amount ?? '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (!recordingRestriction.canEditStock) {
|
if (!recordingRestriction.canEditStock) {
|
||||||
baseValues.stocks = [];
|
baseValues.stocks = [];
|
||||||
}
|
}
|
||||||
@@ -1224,6 +1305,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
selectedKandang,
|
selectedKandang,
|
||||||
recordingRestriction.canEditStock,
|
recordingRestriction.canEditStock,
|
||||||
recordingRestriction.canEditDepletion,
|
recordingRestriction.canEditDepletion,
|
||||||
|
isMigrationMode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const formik = useFormik<
|
const formik = useFormik<
|
||||||
@@ -1335,6 +1417,35 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SWR timing fix: formik initializes before system-settings load, so isMigrationMode
|
||||||
|
// starts false. When it flips true, formikInitialValues recomputes but enableReinitialize
|
||||||
|
// is false, so formik won't pick it up. Push the corrected stock values once, and only
|
||||||
|
// once — the ref prevents re-firing if something causes isMigrationMode to re-evaluate.
|
||||||
|
const migrationEditMappingApplied = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
type !== 'edit' ||
|
||||||
|
!isMigrationMode ||
|
||||||
|
!initialValues?.stocks?.length ||
|
||||||
|
migrationEditMappingApplied.current
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
migrationEditMappingApplied.current = true;
|
||||||
|
formik.setFieldValue(
|
||||||
|
'stocks',
|
||||||
|
initialValues.stocks.map((stock) => ({
|
||||||
|
product_warehouse_id: {
|
||||||
|
value: Number(
|
||||||
|
stock.product_warehouse?.product_id ?? stock.product_warehouse_id
|
||||||
|
),
|
||||||
|
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
||||||
|
},
|
||||||
|
qty: stock.usage_amount ?? '',
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isMigrationMode]);
|
||||||
|
|
||||||
// ===== HELPER FUNCTIONS =====
|
// ===== HELPER FUNCTIONS =====
|
||||||
const { setFieldValue } = formik;
|
const { setFieldValue } = formik;
|
||||||
|
|
||||||
@@ -1351,7 +1462,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
(stockIdx: number) => {
|
(stockIdx: number) => {
|
||||||
if ((type as 'add' | 'edit' | 'detail') === 'detail') return null;
|
if ((type as 'add' | 'edit' | 'detail') === 'detail') return null;
|
||||||
const stock = formik.values.stocks?.[stockIdx];
|
const stock = formik.values.stocks?.[stockIdx];
|
||||||
if (!stock || !stock.product_warehouse_id) return null;
|
if (!stock || !stock.product_warehouse_id?.value) return null;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[formik.values.stocks, type]
|
[formik.values.stocks, type]
|
||||||
@@ -1361,7 +1472,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
(productWarehouseId: number) => {
|
(productWarehouseId: number) => {
|
||||||
if ((type === 'edit' || type === 'detail') && initialValues?.stocks) {
|
if ((type === 'edit' || type === 'detail') && initialValues?.stocks) {
|
||||||
const existingStock = initialValues.stocks.find(
|
const existingStock = initialValues.stocks.find(
|
||||||
(s) => s.product_warehouse_id === productWarehouseId
|
(s) => Number(s.product_warehouse_id) === Number(productWarehouseId)
|
||||||
) as RecordingStock | undefined;
|
) as RecordingStock | undefined;
|
||||||
if (existingStock) {
|
if (existingStock) {
|
||||||
return {
|
return {
|
||||||
@@ -1381,21 +1492,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const getStockUsageAdornment = useCallback(
|
const getStockUsageAdornment = useCallback(
|
||||||
(stockIdx: number) => {
|
(stockIdx: number) => {
|
||||||
const stock = formik.values.stocks?.[stockIdx];
|
const stock = formik.values.stocks?.[stockIdx];
|
||||||
if (!stock || !stock.product_warehouse_id) return null;
|
if (!stock || !stock.product_warehouse_id?.value) return null;
|
||||||
|
|
||||||
const isDetail = (type as 'add' | 'edit' | 'detail') === 'detail';
|
const isDetail = (type as 'add' | 'edit' | 'detail') === 'detail';
|
||||||
const availableStock = getAvailableStock(stock.product_warehouse_id);
|
const availableStock = getAvailableStock(
|
||||||
|
stock.product_warehouse_id.value
|
||||||
|
);
|
||||||
const requestedUsage = Number(stock.qty) || 0;
|
const requestedUsage = Number(stock.qty) || 0;
|
||||||
const remainingStock = availableStock - requestedUsage;
|
const remainingStock = availableStock - requestedUsage;
|
||||||
const { pendingQty } = getStockPendingInfo(stock.product_warehouse_id);
|
const { pendingQty } = getStockPendingInfo(
|
||||||
|
stock.product_warehouse_id.value
|
||||||
|
);
|
||||||
|
|
||||||
if (isDetail) {
|
if (isDetail) {
|
||||||
if (pendingQty > 0) {
|
if (pendingQty > 0) {
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||||
(tersedia: {formatNumber(requestedUsage)} | pending:{' '}
|
(tersedia: {formatNumber(availableStock)} | pending:{' '}
|
||||||
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
||||||
pakai: {formatNumber(requestedUsage + pendingQty)})
|
pakai: {formatNumber(requestedUsage)})
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1494,10 +1609,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return (
|
return (
|
||||||
idx !== currentIdx &&
|
idx !== currentIdx &&
|
||||||
s.product_warehouse_id &&
|
s.product_warehouse_id &&
|
||||||
s.product_warehouse_id !== 0
|
s.product_warehouse_id.value !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((s) => s.product_warehouse_id) || [];
|
.map((s) => s.product_warehouse_id?.value) || [];
|
||||||
|
|
||||||
return unifiedStockProducts.filter(
|
return unifiedStockProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1514,10 +1629,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return (
|
return (
|
||||||
idx !== currentIdx &&
|
idx !== currentIdx &&
|
||||||
d.product_warehouse_id &&
|
d.product_warehouse_id &&
|
||||||
d.product_warehouse_id !== 0
|
d.product_warehouse_id.value !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((d) => d.product_warehouse_id) || [];
|
.map((d) => d.product_warehouse_id?.value) || [];
|
||||||
|
|
||||||
return depletionProducts.filter(
|
return depletionProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1534,10 +1649,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return (
|
return (
|
||||||
idx !== currentIdx &&
|
idx !== currentIdx &&
|
||||||
e.product_warehouse_id &&
|
e.product_warehouse_id &&
|
||||||
e.product_warehouse_id !== 0
|
e.product_warehouse_id.value !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((e) => e.product_warehouse_id) || [];
|
.map((e) => e.product_warehouse_id?.value) || [];
|
||||||
|
|
||||||
return eggProducts.filter(
|
return eggProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1583,7 +1698,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
isError: touchedField && Boolean(errorField?.[column]),
|
isError: touchedField && Boolean(errorField?.[column]),
|
||||||
errorMessage:
|
errorMessage:
|
||||||
touchedField && errorField?.[column]
|
touchedField && errorField?.[column]
|
||||||
? (errorField[column] as string)
|
? errorField[column] instanceof Object
|
||||||
|
? (errorField[column] as OptionType)?.label
|
||||||
|
: (errorField[column] as string)
|
||||||
: '',
|
: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1614,14 +1731,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1629,7 +1746,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1678,14 +1795,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1693,7 +1810,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1731,14 +1848,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1746,7 +1863,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1959,7 +2076,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newStocks = [
|
const newStocks = [
|
||||||
...(formik.values.stocks || []),
|
...(formik.values.stocks || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -1991,7 +2108,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newDepletions = [
|
const newDepletions = [
|
||||||
...(formik.values.depletions || []),
|
...(formik.values.depletions || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2025,7 +2142,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newEggs = [
|
const newEggs = [
|
||||||
...((formik.values as RecordingLayingFormValues).eggs || []),
|
...((formik.values as RecordingLayingFormValues).eggs || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2068,7 +2185,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
|
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
|
||||||
const layingValues = formik.values as RecordingLayingFormValues;
|
const layingValues = formik.values as RecordingLayingFormValues;
|
||||||
if (!layingValues.eggs || layingValues.eggs.length === 0) {
|
if (!layingValues.eggs || layingValues.eggs.length === 0) {
|
||||||
setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
|
setFieldValue('eggs', [{ product_warehouse_id: null, qty: '' }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
||||||
@@ -2790,20 +2907,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
key={`stock-product-${idx}-${stock.product_warehouse_id}`}
|
key={`stock-product-${idx}-${stock.product_warehouse_id?.value}`}
|
||||||
value={
|
value={stock.product_warehouse_id}
|
||||||
unifiedStockProducts.find(
|
onInputChange={setStockInputValue}
|
||||||
(product) =>
|
|
||||||
product.value === stock.product_warehouse_id
|
|
||||||
) || null
|
|
||||||
}
|
|
||||||
onInputChange={setStockProductInputValue}
|
|
||||||
onChange={(selectedOption) => {
|
onChange={(selectedOption) => {
|
||||||
const option =
|
const option =
|
||||||
selectedOption as OptionType | null;
|
selectedOption as OptionType | null;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`stocks.${idx}.product_warehouse_id`,
|
`stocks.${idx}.product_warehouse_id`,
|
||||||
option?.value || 0
|
option
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableStockProductOptions(idx)}
|
options={getAvailableStockProductOptions(idx)}
|
||||||
@@ -2839,9 +2951,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
}
|
}
|
||||||
isClearable={type !== 'detail'}
|
isClearable={type !== 'detail'}
|
||||||
inputPrefix={
|
inputPrefix={
|
||||||
stock.product_warehouse_id
|
stock.product_warehouse_id?.value
|
||||||
? getProductFlagBadgeAdornment(
|
? getProductFlagBadgeAdornment(
|
||||||
stock.product_warehouse_id
|
stock.product_warehouse_id.value
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -2877,7 +2989,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
inputSuffix={
|
inputSuffix={
|
||||||
stock.product_warehouse_id
|
stock.product_warehouse_id
|
||||||
? getProductUomSuffix(
|
? getProductUomSuffix(
|
||||||
stock.product_warehouse_id,
|
stock.product_warehouse_id.value,
|
||||||
'stock'
|
'stock'
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
@@ -3070,19 +3182,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
value={
|
value={depletion.product_warehouse_id}
|
||||||
depletionProducts.find(
|
|
||||||
(product) =>
|
|
||||||
product.value ===
|
|
||||||
depletion.product_warehouse_id
|
|
||||||
) || null
|
|
||||||
}
|
|
||||||
onChange={(selectedOption) => {
|
onChange={(selectedOption) => {
|
||||||
const option =
|
const option =
|
||||||
selectedOption as OptionType | null;
|
selectedOption as OptionType | null;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`depletions.${idx}.product_warehouse_id`,
|
`depletions.${idx}.product_warehouse_id`,
|
||||||
option?.value || 0
|
option
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableDepletionProductOptions(idx)}
|
options={getAvailableDepletionProductOptions(idx)}
|
||||||
@@ -3145,7 +3251,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
inputSuffix={
|
inputSuffix={
|
||||||
depletion.product_warehouse_id
|
depletion.product_warehouse_id
|
||||||
? getProductUomSuffix(
|
? getProductUomSuffix(
|
||||||
depletion.product_warehouse_id,
|
depletion.product_warehouse_id.value,
|
||||||
'depletion'
|
'depletion'
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
@@ -3323,18 +3429,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
value={
|
value={egg.product_warehouse_id}
|
||||||
eggProducts.find(
|
|
||||||
(product) =>
|
|
||||||
product.value === egg.product_warehouse_id
|
|
||||||
) || null
|
|
||||||
}
|
|
||||||
onChange={(selectedOption) => {
|
onChange={(selectedOption) => {
|
||||||
const option =
|
const option =
|
||||||
selectedOption as OptionType | null;
|
selectedOption as OptionType | null;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`eggs.${idx}.product_warehouse_id`,
|
`eggs.${idx}.product_warehouse_id`,
|
||||||
option?.value || 0
|
option
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableEggProductOptions(idx)}
|
options={getAvailableEggProductOptions(idx)}
|
||||||
|
|||||||
+35
-23
@@ -40,6 +40,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
? transferToLayingResponse.data
|
? transferToLayingResponse.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isTransferToLayingApproved =
|
||||||
|
transferToLaying?.approval.step_number === 2;
|
||||||
|
|
||||||
const { data: transferToLayingApprovalResponse } = useSWR(
|
const { data: transferToLayingApprovalResponse } = useSWR(
|
||||||
transferToLayingId
|
transferToLayingId
|
||||||
? ['approval-transfer-to-laying', transferToLayingId]
|
? ['approval-transfer-to-laying', transferToLayingId]
|
||||||
@@ -55,9 +58,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
const detailModal = useModal();
|
const detailModal = useModal();
|
||||||
|
|
||||||
const totalEnteredChickenForTransfer =
|
const maxSourceQuantity =
|
||||||
transferToLaying?.sources.reduce(
|
transferToLaying?.sources.reduce(
|
||||||
(acc, item) => acc + Number(item.qty),
|
(acc, item) => acc + Number(item.product_warehouse.quantity),
|
||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
@@ -67,8 +70,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
|
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
|
||||||
const totalAvailableChickenForTransfer =
|
const totalAvailableChickenForTransfer =
|
||||||
totalEnteredChickenForTransfer - totalTransferedChicken;
|
maxSourceQuantity - totalTransferedChicken;
|
||||||
|
|
||||||
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
||||||
if (shouldPushToRoute) {
|
if (shouldPushToRoute) {
|
||||||
@@ -161,11 +165,34 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-full py-2 text-xs font-semibold'>
|
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
||||||
Kandang Asal{' '}
|
<span className='text-nowrap'>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
Kandang Asal{' '}
|
||||||
<span className='text-error'> *</span>
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{!isTransferToLayingApproved && (
|
||||||
|
<>
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa ayam: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{transferToLaying?.sources.length === 0 && (
|
{transferToLaying?.sources.length === 0 && (
|
||||||
@@ -225,21 +252,6 @@ const TransferToLayingDetailModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='w-px h-5 bg-base-content/10' />
|
|
||||||
|
|
||||||
<StatusBadge
|
|
||||||
color={
|
|
||||||
totalAvailableChickenForTransfer < 0 ? 'error' : 'neutral'
|
|
||||||
}
|
|
||||||
text={`Sisa transfer: ${formatNumber(
|
|
||||||
totalAvailableChickenForTransfer,
|
|
||||||
'en-US'
|
|
||||||
)} ekor`}
|
|
||||||
className={{
|
|
||||||
badge: 'text-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{transferToLaying?.targets.length === 0 && (
|
{transferToLaying?.targets.length === 0 && (
|
||||||
@@ -304,7 +316,7 @@ const TransferToLayingDetailModal = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
errorMessage={
|
errorMessage={
|
||||||
totalAvailableChickenForTransfer < 0
|
totalAvailableChickenForTransfer < 0
|
||||||
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
|
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-21
@@ -13,7 +13,6 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Flock } from '@/types/api/master-data/flock';
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying';
|
|
||||||
import {
|
import {
|
||||||
TransferToLayingFilterSchema,
|
TransferToLayingFilterSchema,
|
||||||
TransferToLayingFilterValues,
|
TransferToLayingFilterValues,
|
||||||
@@ -21,12 +20,14 @@ import {
|
|||||||
|
|
||||||
interface TransferToLayingFilterModal {
|
interface TransferToLayingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
onSubmit?: (values: TransferToLayingFilter) => void;
|
initialValues?: Partial<TransferToLayingFilterValues>;
|
||||||
|
onSubmit?: (values: TransferToLayingFilterValues) => void;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransferToLayingFilterModal = ({
|
const TransferToLayingFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
|
initialValues: initialValuesProp,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: TransferToLayingFilterModal) => {
|
}: TransferToLayingFilterModal) => {
|
||||||
@@ -86,28 +87,16 @@ const TransferToLayingFilterModal = ({
|
|||||||
|
|
||||||
const formik = useFormik<TransferToLayingFilterValues>({
|
const formik = useFormik<TransferToLayingFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: '',
|
startDate: initialValuesProp?.startDate ?? '',
|
||||||
endDate: '',
|
endDate: initialValuesProp?.endDate ?? '',
|
||||||
flockSource: [],
|
flockSource: initialValuesProp?.flockSource ?? [],
|
||||||
flockDestination: [],
|
flockDestination: initialValuesProp?.flockDestination ?? [],
|
||||||
status: [],
|
status: initialValuesProp?.status ?? [],
|
||||||
},
|
},
|
||||||
|
enableReinitialize: true,
|
||||||
validationSchema: TransferToLayingFilterSchema,
|
validationSchema: TransferToLayingFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues = {
|
onSubmit?.(values);
|
||||||
...values,
|
|
||||||
flockSource: values.flockSource
|
|
||||||
? (values.flockSource as OptionType[]).map((item) => item.value)
|
|
||||||
: [],
|
|
||||||
flockDestination: values.flockDestination
|
|
||||||
? (values.flockDestination as OptionType[]).map((item) => item.value)
|
|
||||||
: [],
|
|
||||||
status: values.status
|
|
||||||
? (values.status as OptionType[]).map((item) => item.value)
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
onSubmit?.(formattedValues as TransferToLayingFilter);
|
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
|
|||||||
@@ -223,6 +223,8 @@ const TransferToLayingFormModal = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { flockSource: formikFlockSource } = formik.values;
|
||||||
|
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
|
|
||||||
const [selectedFlockSourceRawData, setSelectedFlockSourceRawData] = useState<
|
const [selectedFlockSourceRawData, setSelectedFlockSourceRawData] = useState<
|
||||||
@@ -455,13 +457,13 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(flockSourceRawData)) {
|
if (isResponseSuccess(flockSourceRawData)) {
|
||||||
const selectedFlockSourceRawData = flockSourceRawData.data.find(
|
const currentSelectedFlockSourceRawData = flockSourceRawData.data.find(
|
||||||
(item) => item.id === formik.values.flockSource?.value
|
(item) => item.id === formik.values.flockSource?.value
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedFlockSourceRawData(selectedFlockSourceRawData);
|
setSelectedFlockSourceRawData(currentSelectedFlockSourceRawData);
|
||||||
}
|
}
|
||||||
}, [flockSourceRawData]);
|
}, [flockSourceRawData, formikFlockSource]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
||||||
@@ -625,6 +627,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-3'>
|
<div className='flex flex-row items-center gap-3'>
|
||||||
<input
|
<input
|
||||||
|
id={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
||||||
type='radio'
|
type='radio'
|
||||||
name='flockSourceKandang'
|
name='flockSourceKandang'
|
||||||
value={item.project_flock_kandang_id}
|
value={item.project_flock_kandang_id}
|
||||||
@@ -637,13 +640,14 @@ const TransferToLayingFormModal = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
|
htmlFor={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
||||||
className={cn('text-sm text-base-content/50', {
|
className={cn('text-sm text-base-content/50', {
|
||||||
'cursor-pointer': isAvailable,
|
'cursor-pointer': isAvailable,
|
||||||
'cursor-not-allowed opacity-50': !isAvailable,
|
'cursor-not-allowed opacity-50': !isAvailable,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item.kandang_name}{' '}
|
{item.kandang_name}{' '}
|
||||||
<span className='text-base-content/20'>{`(Max: ${item.available_qty})`}</span>
|
<span className='text-base-content/20'>{`(Max: ${item.available_qty ?? '-'})`}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -818,11 +822,33 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-full py-2 text-xs font-semibold'>
|
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
||||||
Kandang Asal{' '}
|
<span className='text-nowrap'>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
Kandang Asal{' '}
|
||||||
<span className='text-error'> *</span>
|
<span
|
||||||
|
className='tooltip tooltip-error'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa ayam: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{formik.values.flockSourceKandangs.length === 0 && (
|
{formik.values.flockSourceKandangs.length === 0 && (
|
||||||
@@ -902,23 +928,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='w-px h-5 bg-base-content/10' />
|
|
||||||
|
|
||||||
<StatusBadge
|
|
||||||
color={
|
|
||||||
totalAvailableChickenForTransfer < 0
|
|
||||||
? 'error'
|
|
||||||
: 'neutral'
|
|
||||||
}
|
|
||||||
text={`Sisa transfer: ${formatNumber(
|
|
||||||
totalAvailableChickenForTransfer,
|
|
||||||
'en-US'
|
|
||||||
)} ekor`}
|
|
||||||
className={{
|
|
||||||
badge: 'text-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{formik.values.flockDestinationKandangs.length === 0 && (
|
{formik.values.flockDestinationKandangs.length === 0 && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -26,10 +26,9 @@ import TransferToLayingFilterModal from '@/components/pages/production/transfer-
|
|||||||
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
|
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
|
||||||
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
|
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
|
||||||
|
|
||||||
import {
|
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
||||||
TransferToLaying,
|
import { TransferToLayingFilterValues } from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter';
|
||||||
TransferToLayingFilter,
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
} from '@/types/api/production/transfer-to-laying';
|
|
||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -142,6 +141,8 @@ const TransferToLayingsTable = () => {
|
|||||||
status: '',
|
status: '',
|
||||||
filter_by: '',
|
filter_by: '',
|
||||||
sort_by: '',
|
sort_by: '',
|
||||||
|
flockSourceNames: '',
|
||||||
|
flockDestinationNames: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -154,6 +155,9 @@ const TransferToLayingsTable = () => {
|
|||||||
filter_by: 'filter_by',
|
filter_by: 'filter_by',
|
||||||
sort_by: 'sort_by',
|
sort_by: 'sort_by',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: ['flockSourceNames', 'flockDestinationNames'],
|
||||||
|
persist: true,
|
||||||
|
storeName: 'transfer-to-laying-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -431,12 +435,84 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterSubmitHandler = (values: TransferToLayingFilter) => {
|
const STATUS_FILTER_OPTIONS = [
|
||||||
updateFilter('startDate', values.startDate);
|
{ value: 'PENDING', label: 'Pengajuan' },
|
||||||
updateFilter('endDate', values.endDate);
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
updateFilter('flockSource', values.flockSource.join(','));
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
updateFilter('flockDestination', values.flockDestination.join(','));
|
];
|
||||||
updateFilter('status', values.status.join(','));
|
|
||||||
|
const filterModalInitialValues = useMemo(() => {
|
||||||
|
const flockSourceIds = tableFilterState.flockSource
|
||||||
|
? tableFilterState.flockSource.split(',')
|
||||||
|
: [];
|
||||||
|
const flockSourceNameList = tableFilterState.flockSourceNames
|
||||||
|
? tableFilterState.flockSourceNames.split(',')
|
||||||
|
: [];
|
||||||
|
const flockSourceOptions = flockSourceIds.filter(Boolean).map((id, i) => ({
|
||||||
|
value: parseInt(id),
|
||||||
|
label: flockSourceNameList[i] || id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const flockDestIds = tableFilterState.flockDestination
|
||||||
|
? tableFilterState.flockDestination.split(',')
|
||||||
|
: [];
|
||||||
|
const flockDestNameList = tableFilterState.flockDestinationNames
|
||||||
|
? tableFilterState.flockDestinationNames.split(',')
|
||||||
|
: [];
|
||||||
|
const flockDestOptions = flockDestIds.filter(Boolean).map((id, i) => ({
|
||||||
|
value: parseInt(id),
|
||||||
|
label: flockDestNameList[i] || id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const statusIds = tableFilterState.status
|
||||||
|
? tableFilterState.status.split(',')
|
||||||
|
: [];
|
||||||
|
const statusOptions = statusIds.filter(Boolean).map((id) => {
|
||||||
|
const found = STATUS_FILTER_OPTIONS.find((opt) => opt.value === id);
|
||||||
|
return found || { value: id, label: id };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate: tableFilterState.startDate || '',
|
||||||
|
endDate: tableFilterState.endDate || '',
|
||||||
|
flockSource: flockSourceOptions,
|
||||||
|
flockDestination: flockDestOptions,
|
||||||
|
status: statusOptions,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
tableFilterState.startDate,
|
||||||
|
tableFilterState.endDate,
|
||||||
|
tableFilterState.flockSource,
|
||||||
|
tableFilterState.flockDestination,
|
||||||
|
tableFilterState.status,
|
||||||
|
tableFilterState.flockSourceNames,
|
||||||
|
tableFilterState.flockDestinationNames,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filterSubmitHandler = (values: TransferToLayingFilterValues) => {
|
||||||
|
const flockSourceOpts = (values.flockSource as OptionType[]) || [];
|
||||||
|
const flockDestOpts = (values.flockDestination as OptionType[]) || [];
|
||||||
|
const statusOpts = (values.status as OptionType[]) || [];
|
||||||
|
|
||||||
|
updateFilter('startDate', values.startDate || '');
|
||||||
|
updateFilter('endDate', values.endDate || '');
|
||||||
|
updateFilter(
|
||||||
|
'flockSource',
|
||||||
|
flockSourceOpts.map((o) => String(o.value)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'flockDestination',
|
||||||
|
flockDestOpts.map((o) => String(o.value)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
|
||||||
|
updateFilter(
|
||||||
|
'flockSourceNames',
|
||||||
|
flockSourceOpts.map((o) => String(o.label)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'flockDestinationNames',
|
||||||
|
flockDestOpts.map((o) => String(o.label)).join(',')
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
@@ -445,6 +521,8 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('flockSource', '');
|
updateFilter('flockSource', '');
|
||||||
updateFilter('flockDestination', '');
|
updateFilter('flockDestination', '');
|
||||||
updateFilter('status', '');
|
updateFilter('status', '');
|
||||||
|
updateFilter('flockSourceNames', '');
|
||||||
|
updateFilter('flockDestinationNames', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
const exportToExcelHandler = async () => {
|
||||||
@@ -558,6 +636,8 @@ const TransferToLayingsTable = () => {
|
|||||||
'search',
|
'search',
|
||||||
'filter_by',
|
'filter_by',
|
||||||
'sort_by',
|
'sort_by',
|
||||||
|
'flockSourceNames',
|
||||||
|
'flockDestinationNames',
|
||||||
]}
|
]}
|
||||||
fieldGroups={[['startDate', 'endDate']]}
|
fieldGroups={[['startDate', 'endDate']]}
|
||||||
onClick={filterModal.openModal}
|
onClick={filterModal.openModal}
|
||||||
@@ -670,6 +750,7 @@ const TransferToLayingsTable = () => {
|
|||||||
|
|
||||||
<TransferToLayingFilterModal
|
<TransferToLayingFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
|
initialValues={filterModalInitialValues}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useState, useEffect } from 'react';
|
import { RefObject, useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -9,31 +9,49 @@ import Modal from '@/components/Modal';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
|
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { PurchaseFilter } from '@/types/api/purchase/purchase';
|
import { PurchaseFilter } from '@/types/api/purchase/purchase';
|
||||||
|
import { AreaApi, LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
|
import { Area } from '@/types/api/master-data/area';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
interface PurchaseFilterModalProps {
|
interface PurchaseFilterModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
initialValues?: {
|
||||||
|
poDate: string;
|
||||||
|
category: OptionType<number>[];
|
||||||
|
status: OptionType<string>[];
|
||||||
|
supplier: OptionType<number> | null;
|
||||||
|
area: OptionType<number> | null;
|
||||||
|
location: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
|
};
|
||||||
onSubmit?: (values: PurchaseFilter) => void;
|
onSubmit?: (values: PurchaseFilter) => void;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseFilterModal = ({
|
const PurchaseFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
|
initialValues,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: PurchaseFilterModalProps) => {
|
}: PurchaseFilterModalProps) => {
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = useCallback(() => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
};
|
}, [ref]);
|
||||||
|
|
||||||
// ===== DATE ERROR STATE =====
|
// ===== DATE ERROR STATE =====
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
|
||||||
|
|
||||||
// ===== CLEANUP TOAST ON UNMOUNT =====
|
// ===== CLEANUP TOAST ON UNMOUNT =====
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,32 +91,134 @@ const PurchaseFilterModal = ({
|
|||||||
'search'
|
'search'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [selectedAreaId, setSelectedAreaId] = useState(
|
||||||
|
initialValues?.area?.value ? String(initialValues.area.value) : ''
|
||||||
|
);
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState(
|
||||||
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setSupplierInputValue,
|
||||||
|
options: supplierOptions,
|
||||||
|
isLoadingOptions: isLoadingSupplierOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setAreaInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
|
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
|
area_id: selectedAreaId || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationId || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<{
|
const formik = useFormik<{
|
||||||
poDate: string;
|
poDate: string;
|
||||||
category: { label: string; value: number }[];
|
category: { label: string; value: number }[];
|
||||||
status: { label: string; value: string }[];
|
status: { label: string; value: string }[];
|
||||||
|
supplier: OptionType<number> | null;
|
||||||
|
area: OptionType<number> | null;
|
||||||
|
location: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
}>({
|
}>({
|
||||||
initialValues: {
|
// enableReinitialize: true,
|
||||||
|
initialValues: initialValues || {
|
||||||
poDate: '',
|
poDate: '',
|
||||||
category: [],
|
category: [],
|
||||||
status: [],
|
status: [],
|
||||||
|
supplier: null,
|
||||||
|
area: null,
|
||||||
|
location: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues = {
|
const formattedValues = {
|
||||||
...values,
|
...values,
|
||||||
category: values.category.map((item) => String(item.value)),
|
category: values.category.map((item) => String(item.value)),
|
||||||
|
category_labels: values.category,
|
||||||
status: values.status.map((item) => String(item.value)),
|
status: values.status.map((item) => String(item.value)),
|
||||||
|
supplier_id: values.supplier?.value,
|
||||||
|
supplier_label: values.supplier?.label,
|
||||||
|
area_id: values.area?.value,
|
||||||
|
area_label: values.area?.label,
|
||||||
|
location_id: values.location?.value,
|
||||||
|
location_label: values.location?.label,
|
||||||
|
project_flock_id: values.project_flock?.value,
|
||||||
|
project_flock_label: values.project_flock?.label,
|
||||||
|
project_flock_kandang_id: values.project_flock_kandang?.value,
|
||||||
|
project_flock_kandang_label: values.project_flock_kandang?.label,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
|
setSelectedAreaId('');
|
||||||
|
setSelectedLocationId('');
|
||||||
onReset?.();
|
onReset?.();
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { resetForm, submitForm } = formik;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedAreaId(
|
||||||
|
initialValues?.area?.value ? String(initialValues.area.value) : ''
|
||||||
|
);
|
||||||
|
setSelectedLocationId(
|
||||||
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
|
);
|
||||||
|
}, [initialValues?.area, initialValues?.location]);
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
const productCategoryChangeHandler = (
|
const productCategoryChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
) => {
|
) => {
|
||||||
@@ -109,6 +229,29 @@ const PurchaseFilterModal = ({
|
|||||||
formik.setFieldValue('status', val);
|
formik.setFieldValue('status', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({
|
||||||
|
values: {
|
||||||
|
poDate: '',
|
||||||
|
category: [],
|
||||||
|
status: [],
|
||||||
|
supplier: null,
|
||||||
|
area: null,
|
||||||
|
location: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSelectedAreaId('');
|
||||||
|
setSelectedLocationId('');
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset, closeModalHandler]);
|
||||||
|
|
||||||
|
const formikSubmitHandler = useCallback(async () => {
|
||||||
|
await submitForm();
|
||||||
|
}, [submitForm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -118,7 +261,7 @@ const PurchaseFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formikResetHandler}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -132,7 +275,9 @@ const PurchaseFilterModal = ({
|
|||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={closeModalHandler}
|
onClick={() => {
|
||||||
|
closeModalHandler();
|
||||||
|
}}
|
||||||
className='p-0 text-base-content/50 hover:text-base-content'
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
@@ -172,6 +317,108 @@ const PurchaseFilterModal = ({
|
|||||||
value: item.step_name,
|
value: item.step_name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Vendor'
|
||||||
|
placeholder='Pilih Vendor'
|
||||||
|
value={formik.values.supplier}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'supplier',
|
||||||
|
!Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options={supplierOptions}
|
||||||
|
isLoading={isLoadingSupplierOptions}
|
||||||
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
value={formik.values.area}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('area', nextValue);
|
||||||
|
formik.setFieldValue('location', null);
|
||||||
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedAreaId(
|
||||||
|
nextValue?.value ? String(nextValue.value) : ''
|
||||||
|
);
|
||||||
|
setSelectedLocationId('');
|
||||||
|
}}
|
||||||
|
options={areaOptions}
|
||||||
|
isLoading={isLoadingAreaOptions}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
value={formik.values.location}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('location', nextValue);
|
||||||
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedLocationId(
|
||||||
|
nextValue?.value ? String(nextValue.value) : ''
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={locationOptions}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.area}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('project_flock', nextValue);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
options={projectFlockOptions}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.location}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
!Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -187,7 +434,8 @@ const PurchaseFilterModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='button'
|
||||||
|
onClick={formikSubmitHandler}
|
||||||
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import useSWRInfinite from 'swr/infinite';
|
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';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -28,17 +25,37 @@ import StatusBadge from '@/components/helper/StatusBadge';
|
|||||||
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
|
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
|
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
|
||||||
|
import Dropdown from '@/components/dropdown/Dropdown';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
|
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
|
||||||
import { PurchaseApi } from '@/services/api/purchase';
|
import { PurchaseApi } from '@/services/api/purchase';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
|
||||||
import { Expense } from '@/types/api/expense';
|
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
|
||||||
|
type PurchaseTableFilters = {
|
||||||
|
search: string;
|
||||||
|
sort_by: string;
|
||||||
|
order_by: string;
|
||||||
|
po_date: string;
|
||||||
|
approval_status: string;
|
||||||
|
product_category_id: string;
|
||||||
|
product_category_name: string;
|
||||||
|
supplier_id: string;
|
||||||
|
supplier_name: string;
|
||||||
|
area_id: string;
|
||||||
|
area_name: string;
|
||||||
|
location_id: string;
|
||||||
|
location_name: string;
|
||||||
|
project_flock_id: string;
|
||||||
|
project_flock_name: string;
|
||||||
|
project_flock_kandang_id: string;
|
||||||
|
project_flock_kandang_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
// ===== STATUS BADGE UTILITIES =====
|
// ===== STATUS BADGE UTILITIES =====
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
@@ -147,42 +164,94 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PurchaseTable = () => {
|
const PurchaseTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
// ===== STATE MANAGEMENT =====
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
|
||||||
|
|
||||||
// ===== TABLE FILTER STATE =====
|
// ===== TABLE FILTER STATE =====
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
|
setFilters,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<PurchaseTableFilters>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
|
sort_by: '',
|
||||||
|
order_by: '',
|
||||||
po_date: '',
|
po_date: '',
|
||||||
approval_status: '',
|
approval_status: '',
|
||||||
product_category_id: '',
|
product_category_id: '',
|
||||||
|
product_category_name: '',
|
||||||
|
supplier_id: '',
|
||||||
|
supplier_name: '',
|
||||||
|
area_id: '',
|
||||||
|
area_name: '',
|
||||||
|
location_id: '',
|
||||||
|
location_name: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
project_flock_name: '',
|
||||||
|
project_flock_kandang_id: '',
|
||||||
|
project_flock_kandang_name: '',
|
||||||
},
|
},
|
||||||
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',
|
||||||
|
supplier_id: 'supplier_id',
|
||||||
|
area_id: 'area_id',
|
||||||
|
location_id: 'location_id',
|
||||||
|
project_flock_id: 'project_flock_id',
|
||||||
|
project_flock_kandang_id: 'project_flock_kandang_id',
|
||||||
},
|
},
|
||||||
|
excludeKeysFromUrl: [
|
||||||
|
'product_category_name',
|
||||||
|
'supplier_name',
|
||||||
|
'area_name',
|
||||||
|
'location_name',
|
||||||
|
'project_flock_name',
|
||||||
|
'project_flock_kandang_name',
|
||||||
|
],
|
||||||
|
persist: true,
|
||||||
|
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();
|
||||||
|
const exportProgressInputModal = useModal();
|
||||||
|
|
||||||
// ===== API DATA FETCHING =====
|
// ===== API DATA FETCHING =====
|
||||||
const {
|
const {
|
||||||
@@ -194,36 +263,10 @@ const PurchaseTable = () => {
|
|||||||
PurchaseApi.getAllFetcher
|
PurchaseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const getKey = (
|
|
||||||
pageIndex: number,
|
|
||||||
previousPageData: BaseApiResponse<Expense>[] | null
|
|
||||||
) => {
|
|
||||||
if (pageIndex > 0 && !previousPageData) return null;
|
|
||||||
return `${ExpenseApi.basePath}?page=${pageIndex + 1}&limit=100`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: expensesPages } = useSWRInfinite(
|
|
||||||
getKey,
|
|
||||||
ExpenseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const expenseMap = useMemo(() => {
|
|
||||||
const map = new Map<string, number>();
|
|
||||||
if (!expensesPages) return map;
|
|
||||||
|
|
||||||
expensesPages.forEach((page) => {
|
|
||||||
if (isResponseSuccess(page)) {
|
|
||||||
page.data.forEach((expense: Expense) => {
|
|
||||||
map.set(expense.reference_number, expense.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
}, [expensesPages]);
|
|
||||||
|
|
||||||
// ===== 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;
|
||||||
@@ -239,20 +282,16 @@ const PurchaseTable = () => {
|
|||||||
return (
|
return (
|
||||||
<ul className='list-disc pl-4'>
|
<ul className='list-disc pl-4'>
|
||||||
{poExpedition.map((exp, index) => {
|
{poExpedition.map((exp, index) => {
|
||||||
const expenseId = expenseMap.get(exp.refrence);
|
return (
|
||||||
if (expenseId) {
|
<li key={index}>
|
||||||
return (
|
<Link
|
||||||
<li key={index}>
|
href={`/expense/detail/?expenseId=${exp.id}`}
|
||||||
<Link
|
className='p-0 h-auto text-primary underline'
|
||||||
href={`/expense/detail/?expenseId=${expenseId}`}
|
>
|
||||||
className='p-0 h-auto text-primary underline'
|
{exp.refrence}
|
||||||
>
|
</Link>
|
||||||
{exp.refrence}
|
</li>
|
||||||
</Link>
|
);
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <li key={index}>{exp.refrence}</li>;
|
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
@@ -269,7 +308,7 @@ const PurchaseTable = () => {
|
|||||||
cell: (props) => props.row.original.requester_name || '-',
|
cell: (props) => props.row.original.requester_name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'products.name',
|
accessorKey: 'products',
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const products = props.row.original.products;
|
const products = props.row.original.products;
|
||||||
@@ -284,7 +323,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'location.name',
|
accessorKey: 'location',
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
cell: (props) => props.row.original.location?.name || '-',
|
cell: (props) => props.row.original.location?.name || '-',
|
||||||
},
|
},
|
||||||
@@ -296,6 +335,14 @@ const PurchaseTable = () => {
|
|||||||
? formatDate(props.row.original.po_date, 'DD MMM YYYY')
|
? formatDate(props.row.original.po_date, 'DD MMM YYYY')
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'received_date',
|
||||||
|
header: 'Tgl. Terima',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.received_date
|
||||||
|
? formatDate(props.row.original.received_date, 'DD MMM YYYY')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'due_date',
|
accessorKey: 'due_date',
|
||||||
header: 'Jatuh Tempo',
|
header: 'Jatuh Tempo',
|
||||||
@@ -306,6 +353,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aging',
|
header: 'Aging',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const purchase = props.row.original;
|
const purchase = props.row.original;
|
||||||
if (!purchase.po_date) return '-';
|
if (!purchase.po_date) return '-';
|
||||||
@@ -317,6 +365,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
header: 'Status Approval',
|
header: 'Status Approval',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -361,6 +410,14 @@ const PurchaseTable = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Tanggal Dibuat',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.created_at
|
||||||
|
? formatDate(props.row.original.created_at, 'DD MMM YYYY')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -392,10 +449,17 @@ const PurchaseTable = () => {
|
|||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await PurchaseApi.delete(selectedPurchase?.id as number);
|
const deleteResponse = await PurchaseApi.delete(
|
||||||
refreshPurchaseRequests();
|
selectedPurchase?.id as number
|
||||||
deleteModal.closeModal();
|
);
|
||||||
toast.success('Berhasil menghapus data permintaan pembelian!');
|
|
||||||
|
if (isResponseSuccess(deleteResponse)) {
|
||||||
|
refreshPurchaseRequests();
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Berhasil menghapus data permintaan pembelian!');
|
||||||
|
} else {
|
||||||
|
toast.error(deleteResponse?.message ?? 'Gagal menghapus data!');
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal menghapus data permintaan pembelian!');
|
toast.error('Gagal menghapus data permintaan pembelian!');
|
||||||
}
|
}
|
||||||
@@ -403,34 +467,191 @@ const PurchaseTable = () => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
|
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateFilter('search', searchValue);
|
|
||||||
}, [searchValue, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('purchase-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
setSearchValue(e.target.value);
|
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
},
|
},
|
||||||
[updateFilter, setSearchValue]
|
[updateFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterSubmitHandler = (values: PurchaseFilter) => {
|
const filterSubmitHandler = (values: PurchaseFilter) => {
|
||||||
updateFilter('po_date', values.poDate);
|
setFilters({
|
||||||
updateFilter('product_category_id', values.category.join(','));
|
po_date: values.poDate,
|
||||||
updateFilter('approval_status', values.status.join(','));
|
product_category_id: values.category.join(','),
|
||||||
|
product_category_name:
|
||||||
|
values.category_labels?.map((item) => item.label).join(',') || '',
|
||||||
|
approval_status: values.status.join(','),
|
||||||
|
supplier_id: values.supplier_id ? String(values.supplier_id) : '',
|
||||||
|
supplier_name: values.supplier_label || '',
|
||||||
|
area_id: values.area_id ? String(values.area_id) : '',
|
||||||
|
area_name: values.area_label || '',
|
||||||
|
location_id: values.location_id ? String(values.location_id) : '',
|
||||||
|
location_name: values.location_label || '',
|
||||||
|
project_flock_id: values.project_flock_id
|
||||||
|
? String(values.project_flock_id)
|
||||||
|
: '',
|
||||||
|
project_flock_name: values.project_flock_label || '',
|
||||||
|
project_flock_kandang_id: values.project_flock_kandang_id
|
||||||
|
? String(values.project_flock_kandang_id)
|
||||||
|
: '',
|
||||||
|
project_flock_kandang_name: values.project_flock_kandang_label || '',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
updateFilter('po_date', '');
|
setFilters({
|
||||||
updateFilter('product_category_id', '');
|
po_date: '',
|
||||||
updateFilter('approval_status', '');
|
product_category_id: '',
|
||||||
|
product_category_name: '',
|
||||||
|
approval_status: '',
|
||||||
|
supplier_id: '',
|
||||||
|
supplier_name: '',
|
||||||
|
area_id: '',
|
||||||
|
area_name: '',
|
||||||
|
location_id: '',
|
||||||
|
location_name: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
project_flock_name: '',
|
||||||
|
project_flock_kandang_id: '',
|
||||||
|
project_flock_kandang_name: '',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const purchaseFilterInitialValues = useMemo(() => {
|
||||||
|
const categoryIds = tableFilterState.product_category_id
|
||||||
|
? tableFilterState.product_category_id
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const categoryLabels = tableFilterState.product_category_name
|
||||||
|
? tableFilterState.product_category_name
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const approvalStatuses = tableFilterState.approval_status
|
||||||
|
? tableFilterState.approval_status
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
poDate: tableFilterState.po_date,
|
||||||
|
category: categoryIds.map((value, index) => ({
|
||||||
|
value: Number(value),
|
||||||
|
label: categoryLabels[index] || value,
|
||||||
|
})),
|
||||||
|
status: approvalStatuses.map((value) => ({
|
||||||
|
value,
|
||||||
|
label:
|
||||||
|
PURCHASE_ORDER_APPROVAL_LINE.find((item) => item.step_name === value)
|
||||||
|
?.step_name || value,
|
||||||
|
})),
|
||||||
|
supplier: tableFilterState.supplier_id
|
||||||
|
? ({
|
||||||
|
value: Number(tableFilterState.supplier_id),
|
||||||
|
label:
|
||||||
|
tableFilterState.supplier_name || tableFilterState.supplier_id,
|
||||||
|
} as OptionType<number>)
|
||||||
|
: null,
|
||||||
|
area: tableFilterState.area_id
|
||||||
|
? ({
|
||||||
|
value: Number(tableFilterState.area_id),
|
||||||
|
label: tableFilterState.area_name || tableFilterState.area_id,
|
||||||
|
} as OptionType<number>)
|
||||||
|
: null,
|
||||||
|
location: tableFilterState.location_id
|
||||||
|
? ({
|
||||||
|
value: Number(tableFilterState.location_id),
|
||||||
|
label:
|
||||||
|
tableFilterState.location_name || tableFilterState.location_id,
|
||||||
|
} as OptionType<number>)
|
||||||
|
: null,
|
||||||
|
project_flock: tableFilterState.project_flock_id
|
||||||
|
? ({
|
||||||
|
value: Number(tableFilterState.project_flock_id),
|
||||||
|
label:
|
||||||
|
tableFilterState.project_flock_name ||
|
||||||
|
tableFilterState.project_flock_id,
|
||||||
|
} as OptionType<number>)
|
||||||
|
: null,
|
||||||
|
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
||||||
|
? ({
|
||||||
|
value: Number(tableFilterState.project_flock_kandang_id),
|
||||||
|
label:
|
||||||
|
tableFilterState.project_flock_kandang_name ||
|
||||||
|
tableFilterState.project_flock_kandang_id,
|
||||||
|
} as OptionType<number>)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}, [tableFilterState]);
|
||||||
|
|
||||||
|
const exportToExcel = useCallback(async () => {
|
||||||
|
setIsLoadingExportingToExcel(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PurchaseApi.exportToExcel(getTableFilterQueryString());
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor data pembelian')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingExportingToExcel(false);
|
||||||
|
}
|
||||||
|
}, [getTableFilterQueryString]);
|
||||||
|
|
||||||
|
const resetExportProgressForm = useCallback(() => {
|
||||||
|
setExportProgressStartDate('');
|
||||||
|
setExportProgressEndDate('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exportProgressStartDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
||||||
|
useCallback((e) => {
|
||||||
|
setExportProgressStartDate(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exportProgressEndDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
||||||
|
useCallback((e) => {
|
||||||
|
setExportProgressEndDate(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exportProgressInputToExcelClickHandler = useCallback(() => {
|
||||||
|
resetExportProgressForm();
|
||||||
|
exportProgressInputModal.openModal();
|
||||||
|
}, [exportProgressInputModal, resetExportProgressForm]);
|
||||||
|
|
||||||
|
const submitExportProgressInputHandler = useCallback(async () => {
|
||||||
|
if (!exportProgressStartDate || !exportProgressEndDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExportProgressLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PurchaseApi.exportInputProgressToExcel(
|
||||||
|
exportProgressStartDate,
|
||||||
|
exportProgressEndDate
|
||||||
|
);
|
||||||
|
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
toast.success('Ekspor berhasil');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsExportProgressLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
exportProgressEndDate,
|
||||||
|
exportProgressInputModal,
|
||||||
|
exportProgressStartDate,
|
||||||
|
resetExportProgressForm,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -477,11 +698,73 @@ const PurchaseTable = () => {
|
|||||||
'search',
|
'search',
|
||||||
'filter_by',
|
'filter_by',
|
||||||
'sort_by',
|
'sort_by',
|
||||||
|
'order_by',
|
||||||
|
'product_category_name',
|
||||||
|
'supplier_name',
|
||||||
|
'area_name',
|
||||||
|
'location_name',
|
||||||
|
'project_flock_name',
|
||||||
|
'project_flock_kandang_name',
|
||||||
]}
|
]}
|
||||||
fieldGroups={[['startDate', 'endDate']]}
|
fieldGroups={[['startDate', 'endDate']]}
|
||||||
onClick={filterModal.openModal}
|
onClick={filterModal.openModal}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<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'
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportToExcel}
|
||||||
|
isLoading={isLoadingExportingToExcel}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor ke Excel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportProgressInputToExcelClickHandler}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor Input Progress (Excel)
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -529,7 +812,8 @@ const PurchaseTable = () => {
|
|||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={setSorting}
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
headerColumnClassName: 'text-nowrap',
|
||||||
@@ -543,6 +827,7 @@ const PurchaseTable = () => {
|
|||||||
|
|
||||||
<PurchaseFilterModal
|
<PurchaseFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
|
initialValues={purchaseFilterInitialValues}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
/>
|
/>
|
||||||
@@ -562,6 +847,76 @@ const PurchaseTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
ref={exportProgressInputModal.ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
closeOnBackdrop
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Ekspor Input Progress
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_start_date'
|
||||||
|
label='Tanggal Mulai'
|
||||||
|
value={exportProgressStartDate}
|
||||||
|
onChange={exportProgressStartDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='export_progress_end_date'
|
||||||
|
label='Tanggal Selesai'
|
||||||
|
value={exportProgressEndDate}
|
||||||
|
onChange={exportProgressEndDateChangeHandler}
|
||||||
|
isNestedModal
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
exportProgressInputModal.closeModal();
|
||||||
|
resetExportProgressForm();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
onClick={submitExportProgressInputHandler}
|
||||||
|
isLoading={isExportProgressLoading}
|
||||||
|
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ const PurchaseRequestForm = ({
|
|||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const [, setLocationSelectInputValue] = useState('');
|
|
||||||
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
|
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -163,6 +162,7 @@ const PurchaseRequestForm = ({
|
|||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
loadMore: loadMoreLocations,
|
loadMore: loadMoreLocations,
|
||||||
|
setInputValue: setLocationSelectInputValue,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import PurchaseOrderAcceptApprovalForm from '@/components/pages/purchase/form/or
|
|||||||
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
|
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
import {
|
import {
|
||||||
CreateAcceptApprovalRequestPayload,
|
CreateAcceptApprovalRequestPayload,
|
||||||
CreateManagerApprovalRequestPayload,
|
CreateManagerApprovalRequestPayload,
|
||||||
@@ -96,6 +98,7 @@ const PurchaseOrderDetail = ({
|
|||||||
const acceptRejectionModal = useModal();
|
const acceptRejectionModal = useModal();
|
||||||
const managerRejectionModal = useModal();
|
const managerRejectionModal = useModal();
|
||||||
const editModal = useModal();
|
const editModal = useModal();
|
||||||
|
const editPoDateModal = useModal();
|
||||||
const penerimaanBarangModal = useModal();
|
const penerimaanBarangModal = useModal();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
|
|
||||||
@@ -105,6 +108,9 @@ const PurchaseOrderDetail = ({
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
const [managerApprovalNotes, setManagerApprovalNotes] = useState('');
|
||||||
|
const [managerApprovalPoDate, setManagerApprovalPoDate] = useState('');
|
||||||
|
const [editPoDate, setEditPoDate] = useState('');
|
||||||
|
|
||||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
parseInt(item)
|
parseInt(item)
|
||||||
@@ -212,6 +218,8 @@ const PurchaseOrderDetail = ({
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
|
setManagerApprovalNotes('');
|
||||||
|
setManagerApprovalPoDate('');
|
||||||
confirmationModalWithNotes.openModal();
|
confirmationModalWithNotes.openModal();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
@@ -414,17 +422,50 @@ const PurchaseOrderDetail = ({
|
|||||||
deleteModal,
|
deleteModal,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const updatePoDateHandler = useCallback(async () => {
|
||||||
|
const purchaseRequestId = searchParams.get('purchaseId')
|
||||||
|
? parseInt(searchParams.get('purchaseId')!)
|
||||||
|
: initialValues?.id || 1;
|
||||||
|
|
||||||
|
if (!purchaseRequestId) {
|
||||||
|
toast.error('Purchase Request ID is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await PurchaseApi.updatePoDate(purchaseRequestId, {
|
||||||
|
po_date: editPoDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseError(res)) {
|
||||||
|
toast.error(res.message || 'Gagal mengubah tanggal PO');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Tanggal PO berhasil diubah');
|
||||||
|
setEditPoDate('');
|
||||||
|
editPoDateModal.closeModal();
|
||||||
|
refetchData?.();
|
||||||
|
}, [
|
||||||
|
initialValues?.id,
|
||||||
|
searchParams,
|
||||||
|
editPoDate,
|
||||||
|
editPoDateModal,
|
||||||
|
refetchData,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== APPROVAL/REJECTION HANDLERS =====
|
// ===== APPROVAL/REJECTION HANDLERS =====
|
||||||
const managerApprovalHandler = async (notes: string) => {
|
const managerApprovalHandler = async () => {
|
||||||
const payload: CreateManagerApprovalRequestPayload = {
|
const payload: CreateManagerApprovalRequestPayload = {
|
||||||
action: 'APPROVED',
|
action: 'APPROVED',
|
||||||
notes: notes || null,
|
notes: managerApprovalNotes || null,
|
||||||
|
po_date: managerApprovalPoDate || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
await createManagerApprovalHandler(payload);
|
await createManagerApprovalHandler(payload);
|
||||||
await refreshApprovals();
|
await refreshApprovals();
|
||||||
await refetchData?.();
|
await refetchData?.();
|
||||||
setApprovalNotes('');
|
setManagerApprovalNotes('');
|
||||||
|
setManagerApprovalPoDate('');
|
||||||
confirmationModalWithNotes.closeModal();
|
confirmationModalWithNotes.closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -829,6 +870,41 @@ const PurchaseOrderDetail = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{purchaseData.po_date &&
|
||||||
|
!purchaseData.po_date.startsWith('0001') && (
|
||||||
|
<div className='group'>
|
||||||
|
<div className='flex items-start'>
|
||||||
|
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||||
|
Tanggal PO
|
||||||
|
</span>
|
||||||
|
<div className='ml-3 flex items-center gap-1'>
|
||||||
|
<span className='text-gray-900'>
|
||||||
|
: {formatDate(purchaseData.po_date, 'DD MMM YYYY')}
|
||||||
|
</span>
|
||||||
|
<RequirePermission permissions='lti.purchase.update'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='p-1 min-h-0 h-auto'
|
||||||
|
onClick={() => {
|
||||||
|
setEditPoDate(
|
||||||
|
formatDate(purchaseData.po_date, 'YYYY-MM-DD')
|
||||||
|
);
|
||||||
|
editPoDateModal.openModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:edit-outline'
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1016,27 +1092,79 @@ const PurchaseOrderDetail = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Confirmation Modal with Notes */}
|
{/* Manager Approval Modal */}
|
||||||
<ConfirmationModalWithNotes
|
<Modal
|
||||||
ref={confirmationModalWithNotes.ref}
|
ref={confirmationModalWithNotes.ref}
|
||||||
type='success'
|
|
||||||
text='Apakah Anda yakin ingin melanjutkan approval ini?'
|
|
||||||
placeholder='(Opsional) Tambahkan catatan untuk approval ini...'
|
|
||||||
rows={4}
|
|
||||||
closeOnBackdrop
|
closeOnBackdrop
|
||||||
primaryButton={{
|
className={{
|
||||||
text: 'Ya, Lanjutkan',
|
modalBox: 'max-w-lg rounded-lg p-0',
|
||||||
color: 'success',
|
|
||||||
onClick: managerApprovalHandler,
|
|
||||||
}}
|
}}
|
||||||
secondaryButton={{
|
>
|
||||||
text: 'Batal',
|
<div className='flex flex-col'>
|
||||||
onClick: () => {
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
setApprovalNotes('');
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
confirmationModalWithNotes.closeModal();
|
Konfirmasi Approval Manager
|
||||||
},
|
</h4>
|
||||||
}}
|
<Button
|
||||||
/>
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
setManagerApprovalNotes('');
|
||||||
|
setManagerApprovalPoDate('');
|
||||||
|
confirmationModalWithNotes.closeModal();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<p className='text-sm text-base-content/70'>
|
||||||
|
Apakah Anda yakin ingin melanjutkan approval ini?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='manager_approval_po_date'
|
||||||
|
label='Tanggal PO'
|
||||||
|
value={managerApprovalPoDate}
|
||||||
|
onChange={(e) => setManagerApprovalPoDate(e.target.value)}
|
||||||
|
isNestedModal
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
name='manager_approval_notes'
|
||||||
|
label='Catatan (Opsional)'
|
||||||
|
placeholder='Tambahkan catatan untuk approval ini...'
|
||||||
|
value={managerApprovalNotes}
|
||||||
|
onChange={(e) => setManagerApprovalNotes(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
setManagerApprovalNotes('');
|
||||||
|
setManagerApprovalPoDate('');
|
||||||
|
confirmationModalWithNotes.closeModal();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='success'
|
||||||
|
onClick={managerApprovalHandler}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Ya, Lanjutkan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Staff Approval Modal */}
|
{/* Staff Approval Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -1112,6 +1240,66 @@ const PurchaseOrderDetail = ({
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Edit PO Date Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={editPoDateModal.ref}
|
||||||
|
closeOnBackdrop
|
||||||
|
className={{
|
||||||
|
modalBox: 'max-w-sm rounded-lg p-0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
||||||
|
<h4 className='text-sm font-semibold text-base-content'>
|
||||||
|
Edit Tanggal PO
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
setEditPoDate('');
|
||||||
|
editPoDateModal.closeModal();
|
||||||
|
}}
|
||||||
|
className='p-1'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
|
<DateInput
|
||||||
|
name='edit_po_date'
|
||||||
|
label='Tanggal PO'
|
||||||
|
value={editPoDate}
|
||||||
|
onChange={(e) => setEditPoDate(e.target.value)}
|
||||||
|
isNestedModal
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
setEditPoDate('');
|
||||||
|
editPoDateModal.closeModal();
|
||||||
|
}}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={updatePoDateHandler}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
disabled={!editPoDate}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Staff Rejection Modal */}
|
{/* Staff Rejection Modal */}
|
||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={staffRejectionModal.ref}
|
ref={staffRejectionModal.ref}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useState } from 'react';
|
|||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import ReportExpenseTab from './tab/ReportExpenseTab';
|
import ReportExpenseTab from '@/components/pages/report/expense/tab/ReportExpenseTab';
|
||||||
|
import ReportDepreciationTab from '@/components/pages/report/expense/tab/ReportDepreciationTab';
|
||||||
|
|
||||||
const ReportExpenseTabs = () => {
|
const ReportExpenseTabs = () => {
|
||||||
const [activeTabId, setActiveTabId] = useState<string>('1');
|
const [activeTabId, setActiveTabId] = useState<string>('1');
|
||||||
@@ -16,6 +17,11 @@ const ReportExpenseTabs = () => {
|
|||||||
label: 'Laporan Biaya Operasional',
|
label: 'Laporan Biaya Operasional',
|
||||||
content: <ReportExpenseTab tabId={'1'} />,
|
content: <ReportExpenseTab tabId={'1'} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
label: 'Laporan Depresiasi',
|
||||||
|
content: <ReportDepreciationTab tabId={'2'} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
type ReportExpenseColumn =
|
type ReportSkeletonColumn<TData extends object> =
|
||||||
| ColumnDef<ReportExpense>
|
| ColumnDef<TData>
|
||||||
| {
|
| {
|
||||||
header: string;
|
header: string;
|
||||||
columns: Array<{
|
columns: Array<{
|
||||||
header: string;
|
header: string;
|
||||||
accessorKey?: string;
|
accessorKey?: string;
|
||||||
cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode;
|
cell?: (props: { row: { original: TData } }) => React.ReactNode;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReportExpenseSkeleton = ({
|
const ReportExpenseSkeleton = <TData extends object>({
|
||||||
columns,
|
columns,
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
}: {
|
}: {
|
||||||
columns: ReportExpenseColumn[];
|
columns: ReportSkeletonColumn<TData>[];
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefObject, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
import { AreaApi, LocationApi } from '@/services/api/master-data';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { Area } from '@/types/api/master-data/area';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
|
||||||
|
export type ReportDepreciationFilterValues = {
|
||||||
|
area_id: string | null;
|
||||||
|
location_id: string | null;
|
||||||
|
project_flock_id: string | null;
|
||||||
|
period: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReportDepreciationFilterSchema = yup.object({
|
||||||
|
area_id: yup.string().nullable(),
|
||||||
|
location_id: yup.string().nullable(),
|
||||||
|
project_flock_id: yup.string().nullable(),
|
||||||
|
period: yup.string().nullable().required('Periode wajib dipilih'),
|
||||||
|
}) as yup.ObjectSchema<ReportDepreciationFilterValues>;
|
||||||
|
|
||||||
|
interface ReportDepreciationFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
initialValues?: ReportDepreciationFilterValues;
|
||||||
|
onSubmit?: (values: Partial<ReportDepreciationFilterValues>) => void;
|
||||||
|
onReset?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultInitialValues: ReportDepreciationFilterValues = {
|
||||||
|
area_id: null,
|
||||||
|
location_id: null,
|
||||||
|
project_flock_id: null,
|
||||||
|
period: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReportDepreciationFilterModal = ({
|
||||||
|
ref,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: ReportDepreciationFilterModalProps) => {
|
||||||
|
const [selectedAreaId, setSelectedAreaId] = useState<string | undefined>(
|
||||||
|
initialValues?.area_id || undefined
|
||||||
|
);
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(initialValues?.location_id || undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedAreaId(initialValues?.area_id || undefined);
|
||||||
|
setSelectedLocationId(initialValues?.location_id || undefined);
|
||||||
|
}, [initialValues?.area_id, initialValues?.location_id]);
|
||||||
|
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setAreaInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
|
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
|
area_id: selectedAreaId || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationId || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik<ReportDepreciationFilterValues>({
|
||||||
|
initialValues: initialValues || defaultInitialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
|
validationSchema: ReportDepreciationFilterSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
onSubmit?.(values);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
onReset: (_) => {
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const areaValue = useMemo(() => {
|
||||||
|
if (!formik.values.area_id) return null;
|
||||||
|
return (
|
||||||
|
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.area_id, areaOptions]);
|
||||||
|
|
||||||
|
const locationValue = useMemo(() => {
|
||||||
|
if (!formik.values.location_id) return null;
|
||||||
|
return (
|
||||||
|
locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.location_id, locationOptions]);
|
||||||
|
|
||||||
|
const projectFlockValue = useMemo(() => {
|
||||||
|
if (!formik.values.project_flock_id) return null;
|
||||||
|
return (
|
||||||
|
projectFlockOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.project_flock_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock_id, projectFlockOptions]);
|
||||||
|
|
||||||
|
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const areaId = val && !Array.isArray(val) ? String(val.value) : null;
|
||||||
|
|
||||||
|
setSelectedAreaId(areaId || undefined);
|
||||||
|
formik.setFieldValue('area_id', areaId);
|
||||||
|
formik.setFieldValue('location_id', null);
|
||||||
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
setSelectedLocationId(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const locationId = val && !Array.isArray(val) ? String(val.value) : null;
|
||||||
|
|
||||||
|
setSelectedLocationId(locationId || undefined);
|
||||||
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const projectFlockId =
|
||||||
|
val && !Array.isArray(val) ? String(val.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('project_flock_id', projectFlockId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'p-0 rounded-xl xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formik.handleReset}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-base-content/10'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
options={areaOptions}
|
||||||
|
value={areaValue}
|
||||||
|
onChange={areaChangeHandler}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
|
isLoading={isLoadingAreaOptions}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
value={locationValue}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
options={projectFlockOptions}
|
||||||
|
value={projectFlockValue}
|
||||||
|
onChange={projectFlockChangeHandler}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
label='Periode'
|
||||||
|
name='period'
|
||||||
|
placeholder='Pilih Periode'
|
||||||
|
value={formik.values.period || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={formik.touched.period && !!formik.errors.period}
|
||||||
|
errorMessage={formik.errors.period}
|
||||||
|
required
|
||||||
|
isNestedModal
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex justify-between gap-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={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportDepreciationFilterModal;
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Pagination from '@/components/Pagination';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import ReportDepreciationFilterModal from '@/components/pages/report/expense/tab/ReportDepreciationFilterModal';
|
||||||
|
|
||||||
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
|
import { ReportDepreciation } from '@/types/api/report/report-expense';
|
||||||
|
import { DepreciationReportApi } from '@/services/api/report/expense-report';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
|
||||||
|
interface ReportDepreciationTabProps {
|
||||||
|
tabId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
reset: resetFilter,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
area_id: '',
|
||||||
|
location_id: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
period: formatDate(Date.now(), 'YYYY-MM-DD'),
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
pageSize: 'limit',
|
||||||
|
area_id: 'area_id',
|
||||||
|
location_id: 'location_id',
|
||||||
|
project_flock_id: 'project_flock_id',
|
||||||
|
period: 'period',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: depreciationsResponse, isLoading: isLoadingDepreciations } =
|
||||||
|
useSWR(
|
||||||
|
`${DepreciationReportApi.basePath}${getTableFilterQueryString()}`,
|
||||||
|
DepreciationReportApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const depreciations = isResponseSuccess(depreciationsResponse)
|
||||||
|
? depreciationsResponse.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const filterModal = useModal();
|
||||||
|
const { ref: filterModalRef } = filterModal;
|
||||||
|
|
||||||
|
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||||
|
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
||||||
|
|
||||||
|
const depreciationKandangColumns: ColumnDef<
|
||||||
|
ReportDepreciation['components']['kandang'][0]
|
||||||
|
>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'kandang_name',
|
||||||
|
header: 'Kandang',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'house_type',
|
||||||
|
header: 'Tipe Kandang',
|
||||||
|
cell: ({ row }) => row.original.house_type.toUpperCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'depreciation_percent',
|
||||||
|
header: 'Persentase Depresiasi',
|
||||||
|
cell: ({ row }) => row.original.depreciation_percent + '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'depreciation_value',
|
||||||
|
header: 'Nilai Depresiasi',
|
||||||
|
cell: ({ row }) => formatCurrency(row.original.depreciation_value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'depreciation_source',
|
||||||
|
header: 'Asal Depresiasi',
|
||||||
|
cell: ({ row }) => row.original.depreciation_source.toUpperCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'cutover_date',
|
||||||
|
header: 'Tanggal Cutover',
|
||||||
|
cell: ({ row }) => formatDate(row.original.cutover_date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'origin_date',
|
||||||
|
header: 'Tanggal Origin',
|
||||||
|
cell: ({ row }) => formatDate(row.original.origin_date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabActionsElement = useMemo(
|
||||||
|
() => (
|
||||||
|
<div className='flex flex-row gap-3'>
|
||||||
|
<ButtonFilter
|
||||||
|
values={tableFilterState}
|
||||||
|
excludeFields={['page', 'pageSize']}
|
||||||
|
onClick={() => filterModal.openModal()}
|
||||||
|
variant='outline'
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[tableFilterState]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTabActions(tabId, tabActionsElement);
|
||||||
|
}, [setTabActions, tabActionsElement, tabId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearTabActions(tabId);
|
||||||
|
};
|
||||||
|
}, [clearTabActions, tabId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
||||||
|
{isLoadingDepreciations && (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingDepreciations && depreciations.length === 0 && (
|
||||||
|
<ReportExpenseSkeleton
|
||||||
|
columns={depreciationKandangColumns}
|
||||||
|
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.'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingDepreciations && depreciations.length > 0 && (
|
||||||
|
<>
|
||||||
|
{depreciations.map((depreciationItem, idx) => (
|
||||||
|
<Card
|
||||||
|
key={idx}
|
||||||
|
title={depreciationItem.farm_name}
|
||||||
|
subtitle={`Period: ${formatDate(depreciationItem.period, 'DD MMM YYYY')} | Depresiasi Efektif: ${formatNumber(depreciationItem.depreciation_percent_effective, 'en-US', 0, 10)}% | Nilai Depresiasi: ${formatCurrency(depreciationItem.depreciation_value)} | Total Pullet Cost: ${formatCurrency(depreciationItem.pullet_cost_day_n_total, 'IDR', 'id-ID', 0, 10)}`}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full rounded-lg border-none',
|
||||||
|
body: 'p-0',
|
||||||
|
title:
|
||||||
|
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
||||||
|
subtitle:
|
||||||
|
'px-2 pb-1.5 bg-primary text-white text-xs font-normal',
|
||||||
|
collapsible: 'rounded-lg',
|
||||||
|
}}
|
||||||
|
variant='bordered'
|
||||||
|
collapsible={true}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
data={depreciationItem.components.kandang}
|
||||||
|
columns={depreciationKandangColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(depreciationsResponse)
|
||||||
|
? depreciationsResponse?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(depreciationsResponse)
|
||||||
|
? depreciationsResponse?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoadingDepreciations}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'w-full mb-0!',
|
||||||
|
tableWrapperClassName:
|
||||||
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
tableFooterClassName:
|
||||||
|
'bg-gray-100 font-semibold border border-gray-200',
|
||||||
|
footerRowClassName: 'border-t-2 border-gray-300',
|
||||||
|
footerColumnClassName:
|
||||||
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(depreciationsResponse)
|
||||||
|
? (depreciationsResponse?.meta?.total_results ?? 0)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
itemsPerPage={tableFilterState.pageSize}
|
||||||
|
currentPage={
|
||||||
|
isResponseSuccess(depreciationsResponse)
|
||||||
|
? (depreciationsResponse?.meta?.page ?? 0)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPrevPage={() => setPage(tableFilterState.page - 1)}
|
||||||
|
onNextPage={() => setPage(tableFilterState.page + 1)}
|
||||||
|
onPageChange={setPage}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
onRowChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ReportDepreciationFilterModal
|
||||||
|
ref={filterModalRef}
|
||||||
|
initialValues={tableFilterState}
|
||||||
|
onReset={resetFilter}
|
||||||
|
onSubmit={(values) => {
|
||||||
|
updateFilter('area_id', values.area_id ?? '');
|
||||||
|
updateFilter('location_id', values.location_id ?? '');
|
||||||
|
updateFilter('project_flock_id', values.project_flock_id ?? '');
|
||||||
|
updateFilter(
|
||||||
|
'period',
|
||||||
|
values.period ? formatDate(values.period, 'YYYY-MM-DD') : ''
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportDepreciationTab;
|
||||||
@@ -23,8 +23,8 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||||
import { ReportExpenseApi } from '@/services/api/report';
|
import { ReportExpenseApi } from '@/services/api/report/expense-report';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef, SortingState, Updater } from '@tanstack/react-table';
|
||||||
import { httpClient } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
@@ -73,6 +73,25 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
|
// ===== SORTING STATE =====
|
||||||
|
const [sortBy, setSortBy] = useState('');
|
||||||
|
const [orderBy, setOrderBy] = useState('');
|
||||||
|
|
||||||
|
const sorting: SortingState = sortBy
|
||||||
|
? [{ id: sortBy, desc: orderBy === 'desc' }]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleSortingChange = (updater: Updater<SortingState>) => {
|
||||||
|
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
||||||
|
if (next.length > 0) {
|
||||||
|
setSortBy(next[0].id);
|
||||||
|
setOrderBy(next[0].desc ? 'desc' : 'asc');
|
||||||
|
} else {
|
||||||
|
setSortBy('');
|
||||||
|
setOrderBy('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilterModalOpenRef = useRef(() => {});
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
@@ -126,8 +145,49 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
|
const restoredLocation = filterParams.location_id
|
||||||
|
? locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.location_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.location_id,
|
||||||
|
label: filterParams.location_id,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const restoredSupplier = filterParams.supplier_id
|
||||||
|
? supplierOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.supplier_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.supplier_id,
|
||||||
|
label: filterParams.supplier_id,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const restoredKandang = filterParams.kandang_id
|
||||||
|
? projectFlockKandangOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.kandang_id
|
||||||
|
) || { value: filterParams.kandang_id, label: filterParams.kandang_id }
|
||||||
|
: null;
|
||||||
|
const restoredNonstock = filterParams.nonstock_id
|
||||||
|
? nonstockOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.nonstock_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.nonstock_id,
|
||||||
|
label: filterParams.nonstock_id,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const restoredCategory = filterParams.category
|
||||||
|
? categoryOptions.find((opt) => opt.value === filterParams.category) ||
|
||||||
|
null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
formik.setValues({
|
||||||
|
location_id: restoredLocation,
|
||||||
|
supplier_id: restoredSupplier,
|
||||||
|
kandang_id: restoredKandang,
|
||||||
|
nonstock_id: restoredNonstock,
|
||||||
|
realization_date: filterParams.realization_date || null,
|
||||||
|
category: restoredCategory,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== OPTIONS =====
|
// ===== OPTIONS =====
|
||||||
@@ -189,26 +249,49 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
[formik.values.category]
|
[formik.values.category]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const buildReportExpenseQueryString = useCallback(
|
||||||
|
(extraParams?: Record<string, string>) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (filterParams.location_id) {
|
||||||
|
params.append('location_id', filterParams.location_id);
|
||||||
|
}
|
||||||
|
if (filterParams.supplier_id) {
|
||||||
|
params.append('supplier_id', filterParams.supplier_id);
|
||||||
|
}
|
||||||
|
if (filterParams.kandang_id) {
|
||||||
|
params.append('project_flock_kandang_id', filterParams.kandang_id);
|
||||||
|
}
|
||||||
|
if (filterParams.nonstock_id) {
|
||||||
|
params.append('nonstock_id', filterParams.nonstock_id);
|
||||||
|
}
|
||||||
|
if (filterParams.realization_date) {
|
||||||
|
params.append('realization_date', filterParams.realization_date);
|
||||||
|
}
|
||||||
|
if (filterParams.category) {
|
||||||
|
params.append('category', filterParams.category);
|
||||||
|
}
|
||||||
|
if (sortBy) params.append('sort_by', sortBy);
|
||||||
|
if (orderBy) params.append('sort_order', orderBy);
|
||||||
|
|
||||||
|
Object.entries(extraParams ?? {}).forEach(([key, value]) => {
|
||||||
|
params.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
},
|
||||||
|
[filterParams, sortBy, orderBy]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: reportExpenseResponse, isLoading } = useSWR(
|
const { data: reportExpenseResponse, isLoading } = useSWR(
|
||||||
() => {
|
() => {
|
||||||
const params = new URLSearchParams();
|
const queryString = buildReportExpenseQueryString({
|
||||||
if (filterParams.location_id)
|
page: String(page),
|
||||||
params.append('location_id', filterParams.location_id);
|
limit: String(pageSize),
|
||||||
if (filterParams.supplier_id)
|
});
|
||||||
params.append('supplier_id', filterParams.supplier_id);
|
|
||||||
if (filterParams.kandang_id)
|
|
||||||
params.append('project_flock_kandang_id', filterParams.kandang_id);
|
|
||||||
if (filterParams.nonstock_id)
|
|
||||||
params.append('nonstock_id', filterParams.nonstock_id);
|
|
||||||
if (filterParams.realization_date)
|
|
||||||
params.append('realization_date', filterParams.realization_date);
|
|
||||||
if (filterParams.category)
|
|
||||||
params.append('category', filterParams.category);
|
|
||||||
params.append('page', String(page));
|
|
||||||
params.append('limit', String(pageSize));
|
|
||||||
|
|
||||||
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
|
return [`${ReportExpenseApi.basePath}?${queryString}`];
|
||||||
},
|
},
|
||||||
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
||||||
);
|
);
|
||||||
@@ -233,47 +316,31 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const reportExpenseExport = useCallback(async (): Promise<
|
const reportExpenseExport = useCallback(async (): Promise<
|
||||||
ReportExpense[] | null
|
ReportExpense[] | null
|
||||||
> => {
|
> => {
|
||||||
const params = new URLSearchParams();
|
const queryString = buildReportExpenseQueryString({
|
||||||
if (filterParams.location_id)
|
page: '1',
|
||||||
params.append('location_id', filterParams.location_id);
|
limit: '100',
|
||||||
if (filterParams.supplier_id)
|
});
|
||||||
params.append('supplier_id', filterParams.supplier_id);
|
|
||||||
if (filterParams.kandang_id)
|
|
||||||
params.append('kandang_id', filterParams.kandang_id);
|
|
||||||
if (filterParams.nonstock_id)
|
|
||||||
params.append('nonstock_id', filterParams.nonstock_id);
|
|
||||||
if (filterParams.realization_date)
|
|
||||||
params.append('realization_date', filterParams.realization_date);
|
|
||||||
if (filterParams.category) params.append('category', filterParams.category);
|
|
||||||
params.append('limit', '100');
|
|
||||||
params.append('page', '1');
|
|
||||||
|
|
||||||
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
||||||
`${ReportExpenseApi.basePath}?${params.toString()}`
|
`${ReportExpenseApi.basePath}?${queryString}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
}, [filterParams]);
|
}, [buildReportExpenseQueryString]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const allDataForExport = await reportExpenseExport();
|
await ReportExpenseApi.exportToExcel(buildReportExpenseQueryString());
|
||||||
|
} catch (error) {
|
||||||
if (!allDataForExport || allDataForExport.length === 0) {
|
toast.error(
|
||||||
toast.error('Tidak ada data untuk diekspor.');
|
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
await generateReportExpenseExcel(allDataForExport);
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [reportExpenseExport]);
|
}, [buildReportExpenseQueryString]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -397,19 +464,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: 'No',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. PO',
|
header: 'No. PO',
|
||||||
accessorKey: 'po_number',
|
accessorKey: 'po_number',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. Referensi',
|
header: 'No. Referensi',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Realisasi',
|
header: 'Tanggal Realisasi',
|
||||||
accessorKey: 'realization_date',
|
accessorKey: 'realization_date',
|
||||||
|
enableSorting: true,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -417,6 +488,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Tanggal Transaksi',
|
header: 'Tanggal Transaksi',
|
||||||
accessorKey: 'transaction_date',
|
accessorKey: 'transaction_date',
|
||||||
|
enableSorting: true,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -424,21 +496,30 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
|
enableSorting: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
|
accessorKey: 'product',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Supplier',
|
header: 'Supplier',
|
||||||
|
accessorKey: 'supplier',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.supplier?.name,
|
accessorFn: (row) => row.supplier?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
|
accessorKey: 'location',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.kandang?.location?.name,
|
accessorFn: (row) => row.kandang?.location?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Kandang',
|
header: 'Kandang',
|
||||||
|
accessorKey: 'kandang',
|
||||||
|
enableSorting: true,
|
||||||
accessorFn: (row) => row.kandang?.name,
|
accessorFn: (row) => row.kandang?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -446,23 +527,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
id: 'qty_pengajuan',
|
accessorKey: 'qty_pengajuan',
|
||||||
accessorFn: (row) => row.pengajuan?.qty,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
id: 'harga_pengajuan',
|
accessorKey: 'price_pengajuan',
|
||||||
accessorFn: (row) => row.pengajuan?.price,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.pengajuan?.price || 0),
|
formatCurrency(row.original.pengajuan?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
id: 'total_pengajuan',
|
accessorKey: 'total_pengajuan',
|
||||||
accessorFn: (row) =>
|
|
||||||
(row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0),
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.pengajuan?.qty || 0) *
|
(row.original.pengajuan?.qty || 0) *
|
||||||
@@ -477,23 +554,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
id: 'qty_realisasi',
|
accessorKey: 'qty_realisasi',
|
||||||
accessorFn: (row) => row.realisasi?.qty,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
id: 'harga_realisasi',
|
accessorKey: 'price_realisasi',
|
||||||
accessorFn: (row) => row.realisasi?.price,
|
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.realisasi?.price || 0),
|
formatCurrency(row.original.realisasi?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
id: 'total_realisasi',
|
accessorKey: 'total_realisasi',
|
||||||
accessorFn: (row) =>
|
|
||||||
(row.realisasi?.qty || 0) * (row.realisasi?.price || 0),
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.realisasi?.qty || 0) *
|
(row.original.realisasi?.qty || 0) *
|
||||||
@@ -504,6 +577,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'realization_status',
|
||||||
header: 'Status Pencairan',
|
header: 'Status Pencairan',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RealizationStatusBadge
|
<RealizationStatusBadge
|
||||||
@@ -512,6 +586,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'bop_status',
|
||||||
header: 'Status BOP',
|
header: 'Status BOP',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
||||||
@@ -556,6 +631,9 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
totalItems={meta?.total_results || 0}
|
totalItems={meta?.total_results || 0}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-0!',
|
containerClassName: 'w-full mb-0!',
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
|
|||||||
@@ -1,25 +1,47 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
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 [activeTabId, setActiveTabId] = useState<string>('1');
|
const router = useRouter();
|
||||||
|
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: '1',
|
id: 'debt-supplier',
|
||||||
label: 'Rekapitulasi Hutang Ke Supplier',
|
label: 'Rekapitulasi Hutang Ke Supplier',
|
||||||
content: <DebtSupplierTab tabId={'1'} />,
|
content: <DebtSupplierTab tabId={'debt-supplier'} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: 'customer-payment',
|
||||||
label: 'Kontrol Pembayaran Customer',
|
label: 'Kontrol Pembayaran Customer',
|
||||||
content: <CustomerPaymentTab tabId={'2'} />,
|
content: <CustomerPaymentTab tabId={'customer-payment'} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'balance-monitoring',
|
||||||
|
label: 'Monitoring Saldo',
|
||||||
|
content: <BalanceMonitoringTab tabId={'balance-monitoring'} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -29,7 +51,7 @@ const FinanceTabs = () => {
|
|||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
variant='boxed'
|
variant='boxed'
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
onTabChange={setActiveTabId}
|
onTabChange={handleTabChange}
|
||||||
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',
|
||||||
|
|||||||
@@ -0,0 +1,602 @@
|
|||||||
|
'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,14 +1,17 @@
|
|||||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect } 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 } from '@/components/input/SelectInput';
|
import { useSelect, OptionType } 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 {
|
||||||
@@ -27,55 +30,70 @@ 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 { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
|
||||||
interface CustomerPaymentTabProps {
|
interface CustomerPaymentTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterParams {
|
const dataTypeOptions: OptionType<string>[] = [
|
||||||
customer_ids?: string;
|
{ value: 'trans_date', label: 'Tanggal Jual/Bayar' },
|
||||||
start_date?: string;
|
{ value: 'realization_date', label: 'Tanggal Realisasi' },
|
||||||
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 isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
||||||
|
useState(false);
|
||||||
|
const isAnyExportLoading =
|
||||||
|
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
||||||
|
|
||||||
// ===== PAGINATION STATE =====
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [pageSize] = 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 dataTypeOptions = useMemo(
|
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||||
() => [
|
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
||||||
{ value: 'trans_date', label: 'Tanggal Jual/Bayar' },
|
|
||||||
{ value: 'realization_date', label: 'Tanggal Realisasi' },
|
const {
|
||||||
],
|
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,
|
||||||
@@ -85,223 +103,188 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<CustomerPaymentFilterType>({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
start_date: null,
|
start_date: tableFilterState.start_date,
|
||||||
end_date: null,
|
end_date: tableFilterState.end_date,
|
||||||
customer_ids: null,
|
customers: tableFilterState.customers,
|
||||||
filter_by: null,
|
filterBy: tableFilterState.filterBy,
|
||||||
},
|
},
|
||||||
validationSchema: CustomerPaymentFilterSchema,
|
onSubmit: (values) => {
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
updateFilter('start_date', values.start_date, true);
|
||||||
setFilterParams({
|
updateFilter('end_date', values.end_date, true);
|
||||||
start_date: values.start_date || undefined,
|
updateFilter('customers', values.customers, true);
|
||||||
end_date: values.end_date || undefined,
|
updateFilter('filterBy', values.filterBy, true);
|
||||||
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();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
const formikResetHandler = () => {
|
||||||
filterModal.openModal();
|
resetFilter();
|
||||||
formik.validateForm();
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
customers: [],
|
||||||
|
filterBy: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 === 'lunas') {
|
if (normalizedValue.includes('belum')) return 'warning';
|
||||||
return 'primary';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedValue.includes('belum')) {
|
|
||||||
return 'warning';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'neutral';
|
return 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== DATE CHANGE HANDLERS =====
|
// ===== DATE CHANGE HANDLERS =====
|
||||||
const handleStartDateChange = useCallback(
|
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
const value = e.target.value;
|
||||||
const value = e.target.value;
|
formik.setFieldValue('start_date', value);
|
||||||
formik.setFieldValue('start_date', value || null);
|
|
||||||
|
|
||||||
if (value && formik.values.end_date) {
|
if (value && formik.values.end_date) {
|
||||||
const startDate = new Date(value);
|
if (new Date(formik.values.end_date) < new Date(value)) {
|
||||||
const endDateObj = new Date(formik.values.end_date);
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
if (endDateObj < startDate) {
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
setHasDateError(true);
|
duration: Infinity,
|
||||||
if (!dateErrorShown) {
|
});
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
setDateErrorShown(true);
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setHasDateError(false);
|
setHasDateError(false);
|
||||||
}
|
if (dateErrorShown) {
|
||||||
},
|
toast.dismiss();
|
||||||
[formik, dateErrorShown]
|
setDateErrorShown(false);
|
||||||
);
|
|
||||||
|
|
||||||
const handleEndDateChange = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('end_date', value || null);
|
|
||||||
|
|
||||||
if (value && formik.values.start_date) {
|
|
||||||
const startDateObj = new Date(formik.values.start_date);
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
setHasDateError(false);
|
setHasDateError(false);
|
||||||
if (dateErrorShown) {
|
}
|
||||||
toast.dismiss();
|
};
|
||||||
setDateErrorShown(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;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[formik, dateErrorShown]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
setHasDateError(false);
|
||||||
const customerIdsValue = useMemo(() => {
|
if (dateErrorShown) {
|
||||||
if (!formik.values.customer_ids) return [];
|
toast.dismiss();
|
||||||
return customerOptions.filter((opt) =>
|
setDateErrorShown(false);
|
||||||
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 =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: customerPayment, isLoading } = useSWR(
|
const { data: customerPayment, isLoading } = useSWR<
|
||||||
() => {
|
BaseApiResponse<CustomerPaymentReport>,
|
||||||
const params = {
|
AxiosError<BaseApiResponse>,
|
||||||
customer_ids: filterParams.customer_ids,
|
SWRHttpKey
|
||||||
filter_by: filterParams.filter_by as
|
>(
|
||||||
| 'trans_date'
|
`${FinanceApi.basePath}/customer-payment${getTableFilterQueryString()}`,
|
||||||
| 'realization_date'
|
httpClientFetcher
|
||||||
| 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(
|
const data: CustomerPaymentReport[] = isResponseSuccess(customerPayment)
|
||||||
() =>
|
? (customerPayment?.data as unknown as CustomerPaymentReport[]) || []
|
||||||
isResponseSuccess(customerPayment)
|
: [];
|
||||||
? (customerPayment?.data as unknown as CustomerPaymentReport[]) || []
|
|
||||||
: [],
|
const meta =
|
||||||
[customerPayment]
|
isResponseSuccess(customerPayment) && customerPayment.meta
|
||||||
);
|
? customerPayment.meta
|
||||||
|
: null;
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const customerPaymentExport = useCallback(async (): Promise<
|
const customerPaymentExport = useCallback(async (): Promise<
|
||||||
CustomerPaymentReport[] | null
|
CustomerPaymentReport[] | null
|
||||||
> => {
|
> => {
|
||||||
const params = {
|
const customer_ids =
|
||||||
customer_ids: filterParams.customer_ids,
|
tableFilterState.customers.length > 0
|
||||||
filter_by: filterParams.filter_by as
|
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
||||||
| 'trans_date'
|
: undefined;
|
||||||
| 'realization_date'
|
const filter_by = tableFilterState.filterBy?.value as
|
||||||
| undefined,
|
| 'trans_date'
|
||||||
start_date: filterParams.start_date,
|
| 'realization_date'
|
||||||
end_date: filterParams.end_date,
|
| undefined;
|
||||||
limit: 100,
|
|
||||||
page: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await FinanceApi.getCustomerPaymentReport(
|
const response = await FinanceApi.getCustomerPaymentReport(
|
||||||
params.customer_ids,
|
customer_ids,
|
||||||
params.filter_by,
|
filter_by,
|
||||||
params.start_date,
|
tableFilterState.start_date || undefined,
|
||||||
params.end_date,
|
tableFilterState.end_date || undefined,
|
||||||
params.page,
|
1,
|
||||||
params.limit
|
100
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response)
|
return isResponseSuccess(response)
|
||||||
? (response.data as unknown as CustomerPaymentReport[])
|
? (response.data as unknown as CustomerPaymentReport[])
|
||||||
: null;
|
: null;
|
||||||
}, [filterParams]);
|
}, [tableFilterState]);
|
||||||
|
|
||||||
// ===== 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 allDataForExport = await customerPaymentExport();
|
const customer_ids =
|
||||||
|
tableFilterState.customers.length > 0
|
||||||
if (
|
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
||||||
!allDataForExport ||
|
: undefined;
|
||||||
!Array.isArray(allDataForExport) ||
|
await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet(
|
||||||
allDataForExport.length === 0
|
customer_ids,
|
||||||
) {
|
tableFilterState.filterBy?.value,
|
||||||
toast.error('Tidak ada data untuk diekspor.');
|
tableFilterState.start_date || undefined,
|
||||||
return;
|
tableFilterState.end_date || undefined
|
||||||
}
|
);
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}, [customerPaymentExport]);
|
}, [tableFilterState]);
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -317,22 +300,18 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerName = filterParams.customer_ids
|
const customerName =
|
||||||
? customerOptions
|
tableFilterState.customers.length > 0
|
||||||
.filter((opt) =>
|
? tableFilterState.customers.map((o) => o.label).join(', ')
|
||||||
filterParams.customer_ids?.split(',').includes(String(opt.value))
|
: 'Semua Customer';
|
||||||
)
|
|
||||||
.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: filterParams.start_date,
|
start_date: tableFilterState.start_date || undefined,
|
||||||
end_date: filterParams.end_date,
|
end_date: tableFilterState.end_date || undefined,
|
||||||
filter_by: filterParams.filter_by as
|
filter_by: tableFilterState.filterBy?.value as
|
||||||
| 'trans_date'
|
| 'trans_date'
|
||||||
| 'realization_date'
|
| 'realization_date'
|
||||||
| undefined,
|
| undefined,
|
||||||
@@ -344,106 +323,103 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsPdfExportLoading(false);
|
setIsPdfExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [customerPaymentExport, filterParams, customerOptions]);
|
}, [customerPaymentExport, tableFilterState]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS COMPONENT =====
|
// ===== TAB ACTIONS =====
|
||||||
const TabActions = useMemo(() => {
|
useEffect(() => {
|
||||||
return function TabActionsComponent() {
|
setTabActions(
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
tabId,
|
||||||
const clearTabActions = useTabActionsStore(
|
<div className='flex flex-row gap-3'>
|
||||||
(state) => state.clearTabActions
|
<ButtonFilter
|
||||||
);
|
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'
|
||||||
|
/>
|
||||||
|
|
||||||
useEffect(() => {
|
<Dropdown
|
||||||
setTabActions(
|
align='end'
|
||||||
tabId,
|
direction='bottom'
|
||||||
<div className='flex flex-row gap-3'>
|
className={{
|
||||||
<ButtonFilter
|
content:
|
||||||
values={filterParams}
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
}}
|
||||||
onClick={() => handleFilterModalOpenRef.current()}
|
trigger={
|
||||||
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5'
|
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'
|
||||||
<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>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Button
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
variant='ghost'
|
<Icon
|
||||||
color='none'
|
icon='heroicons:cloud-arrow-down'
|
||||||
onClick={handleExportExcel}
|
width={20}
|
||||||
isLoading={isExcelExportLoading}
|
height={20}
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
/>
|
||||||
>
|
<span>Export</span>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
Export to Excel
|
<Icon icon='heroicons:chevron-down' width={14} height={14} />
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
</Button>
|
||||||
variant='ghost'
|
}
|
||||||
color='none'
|
>
|
||||||
onClick={handleExportPdf}
|
<Button
|
||||||
isLoading={isPdfExportLoading}
|
variant='ghost'
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
color='none'
|
||||||
>
|
onClick={handleExportExcel}
|
||||||
<Icon icon='heroicons:document' width={20} height={20} />
|
isLoading={isExcelExportLoading}
|
||||||
Export to PDF
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
</Button>
|
>
|
||||||
</Dropdown>
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
</div>
|
Export to Excel - Customer Per Sheet
|
||||||
);
|
</Button>
|
||||||
}, [setTabActions]);
|
<Button
|
||||||
|
variant='ghost'
|
||||||
useEffect(() => {
|
color='none'
|
||||||
return () => {
|
onClick={handleExportExcelGeneral}
|
||||||
clearTabActions(tabId);
|
isLoading={isExcelGeneralExportLoading}
|
||||||
};
|
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} />
|
||||||
return null;
|
Export to Excel - General
|
||||||
};
|
</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,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
useEffect(() => {
|
||||||
|
return () => clearTabActions(tabId);
|
||||||
|
}, [tabId, clearTabActions]);
|
||||||
|
|
||||||
const getTableColumns = (
|
const getTableColumns = (
|
||||||
summary: CustomerPaymentSummary
|
summary: CustomerPaymentSummary
|
||||||
@@ -650,11 +626,7 @@ 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)}
|
||||||
@@ -693,7 +665,6 @@ 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'>
|
||||||
@@ -717,6 +688,27 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isLoading && data.length > 0 && meta && (
|
||||||
|
<div className='w-full ml-auto'>
|
||||||
|
<Pagination
|
||||||
|
totalItems={meta.total_results || 0}
|
||||||
|
itemsPerPage={meta.limit || 0}
|
||||||
|
currentPage={tableFilterState.page}
|
||||||
|
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
||||||
|
onNextPage={() =>
|
||||||
|
setPage(
|
||||||
|
meta.total_pages && tableFilterState.page < meta.total_pages
|
||||||
|
? tableFilterState.page + 1
|
||||||
|
: tableFilterState.page
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
onRowChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map((customerReport) => {
|
data.map((customerReport) => {
|
||||||
@@ -811,6 +803,27 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{!isLoading && data.length > 0 && meta && (
|
||||||
|
<div className='mt-5 px-3'>
|
||||||
|
<Pagination
|
||||||
|
totalItems={meta.total_results || 0}
|
||||||
|
itemsPerPage={meta.limit || 0}
|
||||||
|
currentPage={tableFilterState.page}
|
||||||
|
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
||||||
|
onNextPage={() =>
|
||||||
|
setPage(
|
||||||
|
meta.total_pages && tableFilterState.page < meta.total_pages
|
||||||
|
? tableFilterState.page + 1
|
||||||
|
: tableFilterState.page
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
onRowChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Modal */}
|
{/* Filter Modal */}
|
||||||
@@ -835,7 +848,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={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<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'>
|
||||||
@@ -845,29 +858,18 @@ 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={
|
isError={hasDateError}
|
||||||
(formik.touched.end_date &&
|
|
||||||
Boolean(formik.errors.end_date)) ||
|
|
||||||
hasDateError
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -876,15 +878,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
label='Customer'
|
label='Customer'
|
||||||
placeholder='Pilih Customer'
|
placeholder='Pilih Customer'
|
||||||
options={customerOptions}
|
options={customerOptions}
|
||||||
value={customerIdsValue}
|
value={formik.values.customers}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
formik.setFieldValue(
|
formik.setFieldValue('customers', Array.isArray(val) ? val : [])
|
||||||
'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
|
||||||
@@ -896,14 +893,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
label='Filter Berdasarkan'
|
label='Filter Berdasarkan'
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
options={dataTypeOptions}
|
options={dataTypeOptions}
|
||||||
value={filterByValue}
|
value={formik.values.filterBy ?? null}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
if (!Array.isArray(val)) {
|
formik.setFieldValue(
|
||||||
formik.setFieldValue('filter_by', val?.value || null);
|
'filterBy',
|
||||||
}
|
!Array.isArray(val) ? (val ?? undefined) : undefined
|
||||||
}}
|
)
|
||||||
|
}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isClearable={true}
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -919,7 +917,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 || !formik.isValid || formik.isSubmitting}
|
disabled={hasDateError}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import Pagination from '@/components/Pagination';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
@@ -8,24 +9,15 @@ 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 {
|
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||||
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, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, 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';
|
||||||
@@ -34,6 +26,10 @@ 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',
|
||||||
@@ -51,7 +47,6 @@ 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'
|
||||||
@@ -68,6 +63,11 @@ 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,24 +76,50 @@ 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 isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
||||||
|
useState(false);
|
||||||
|
const isAnyExportLoading =
|
||||||
|
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
||||||
|
|
||||||
// ===== 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,
|
||||||
@@ -101,140 +127,180 @@ 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<DebtSupplierFilterType>({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: null,
|
start_date: tableFilterState.start_date,
|
||||||
endDate: null,
|
end_date: tableFilterState.end_date,
|
||||||
supplierIds: null,
|
suppliers: tableFilterState.suppliers,
|
||||||
filterBy: null,
|
filterBy: tableFilterState.filterBy,
|
||||||
},
|
},
|
||||||
validationSchema: DebtSupplierFilterSchema,
|
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
setFilterParams({
|
updateFilter('start_date', values.start_date, true);
|
||||||
start_date: values.startDate?.toString() || undefined,
|
updateFilter('end_date', values.end_date, true);
|
||||||
end_date: values.endDate?.toString() || undefined,
|
updateFilter('suppliers', values.suppliers, true);
|
||||||
supplier_ids:
|
updateFilter('filterBy', values.filterBy, true);
|
||||||
values.supplierIds?.map((v) => String(v.value)).join(',') ||
|
|
||||||
undefined,
|
|
||||||
filter_by: values.filterBy?.value?.toString() || undefined,
|
|
||||||
});
|
|
||||||
filterModal.closeModal();
|
|
||||||
// setIsSubmitted(true);
|
|
||||||
},
|
|
||||||
onReset: () => {
|
|
||||||
setFilterParams({
|
|
||||||
start_date: undefined,
|
|
||||||
end_date: undefined,
|
|
||||||
supplier_ids: undefined,
|
|
||||||
filter_by: undefined,
|
|
||||||
});
|
|
||||||
// setIsSubmitted(false);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
const formikResetHandler = () => {
|
||||||
filterModal.openModal();
|
resetFilter();
|
||||||
formik.validateForm();
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
suppliers: [],
|
||||||
|
filterBy: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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: debtSupplier, isLoading } = useSWR(
|
const { data: debtSupplierResponse, isLoading } = useSWR<
|
||||||
() => {
|
BaseApiResponse<DebtSupplier[]>,
|
||||||
const params = {
|
AxiosError<BaseApiResponse>,
|
||||||
supplier_ids: filterParams.supplier_ids,
|
SWRHttpKey
|
||||||
filter_by: filterParams.filter_by,
|
>(
|
||||||
start_date: filterParams.start_date,
|
`${DebtSupplierApi.basePath}/debt-supplier${getTableFilterQueryString()}`,
|
||||||
end_date: filterParams.end_date,
|
httpClientFetcher
|
||||||
};
|
|
||||||
|
|
||||||
return ['debt-supplier-report', params];
|
|
||||||
},
|
|
||||||
([, params]) =>
|
|
||||||
DebtSupplierApi.getDebtSupplierReport(
|
|
||||||
params.supplier_ids,
|
|
||||||
params.filter_by,
|
|
||||||
params.start_date,
|
|
||||||
params.end_date
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const data: DebtSupplier[] = useMemo(
|
const data: DebtSupplier[] = isResponseSuccess(debtSupplierResponse)
|
||||||
() =>
|
? ((debtSupplierResponse?.data as unknown as DebtSupplier[]) ?? [])
|
||||||
isResponseSuccess(debtSupplier)
|
: [];
|
||||||
? (debtSupplier?.data as unknown as DebtSupplier[]) || []
|
|
||||||
: [],
|
const meta =
|
||||||
[debtSupplier]
|
isResponseSuccess(debtSupplierResponse) && debtSupplierResponse.meta
|
||||||
);
|
? debtSupplierResponse.meta
|
||||||
|
: null;
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const debtSupplierExport = useCallback(async (): Promise<
|
const debtSupplierExport = useCallback(async (): Promise<
|
||||||
DebtSupplier[] | null
|
DebtSupplier[] | null
|
||||||
> => {
|
> => {
|
||||||
const params = {
|
const supplier_ids =
|
||||||
supplier_ids:
|
tableFilterState.suppliers.length > 0
|
||||||
formik.values.supplierIds && formik.values.supplierIds.length > 0
|
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
||||||
? formik.values.supplierIds.map((v) => String(v.value)).join(',')
|
: undefined;
|
||||||
: 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(
|
||||||
params.supplier_ids,
|
supplier_ids,
|
||||||
params.filter_by,
|
tableFilterState.filterBy?.value,
|
||||||
params.start_date,
|
tableFilterState.start_date || undefined,
|
||||||
params.end_date
|
tableFilterState.end_date || undefined,
|
||||||
|
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 allDataForExport = await debtSupplierExport();
|
const supplier_ids =
|
||||||
|
tableFilterState.suppliers.length > 0
|
||||||
if (
|
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
||||||
!allDataForExport ||
|
: undefined;
|
||||||
!Array.isArray(allDataForExport) ||
|
await DebtSupplierApi.exportToExcelSupplierPerSheet(
|
||||||
allDataForExport.length === 0
|
supplier_ids,
|
||||||
) {
|
tableFilterState.filterBy?.value,
|
||||||
toast.error('Tidak ada data untuk diekspor.');
|
tableFilterState.start_date || undefined,
|
||||||
return;
|
tableFilterState.end_date || undefined
|
||||||
}
|
);
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}, [debtSupplierExport]);
|
}, [tableFilterState]);
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -250,15 +316,18 @@ 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: formik.values.supplierIds
|
supplier_name: supplierName,
|
||||||
?.map((v) => v.label)
|
filter_by: tableFilterState.filterBy?.label,
|
||||||
.join(', '),
|
start_date: tableFilterState.start_date || undefined,
|
||||||
filter_by: formik.values.filterBy?.label,
|
end_date: tableFilterState.end_date || undefined,
|
||||||
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.');
|
||||||
@@ -267,129 +336,103 @@ 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 COMPONENT =====
|
// ===== TAB ACTIONS =====
|
||||||
const TabActions = useMemo(() => {
|
useEffect(() => {
|
||||||
return function TabActionsComponent() {
|
setTabActions(
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
tabId,
|
||||||
const clearTabActions = useTabActionsStore(
|
<div className='flex flex-row gap-3'>
|
||||||
(state) => state.clearTabActions
|
<ButtonFilter
|
||||||
);
|
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'
|
||||||
|
/>
|
||||||
|
|
||||||
useEffect(() => {
|
<Dropdown
|
||||||
setTabActions(
|
align='end'
|
||||||
tabId,
|
direction='bottom'
|
||||||
<div className='flex flex-row gap-3'>
|
className={{
|
||||||
<ButtonFilter
|
content:
|
||||||
values={filterParams}
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
}}
|
||||||
onClick={() => handleFilterModalOpenRef.current()}
|
trigger={
|
||||||
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5'
|
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'
|
||||||
<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>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Button
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
variant='ghost'
|
<Icon
|
||||||
color='none'
|
icon='heroicons:cloud-arrow-down'
|
||||||
onClick={handleExportExcel}
|
width={20}
|
||||||
isLoading={isExcelExportLoading}
|
height={20}
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
/>
|
||||||
>
|
<span>Export</span>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
Export to Excel
|
<Icon icon='heroicons:chevron-down' width={14} height={14} />
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
</Button>
|
||||||
variant='ghost'
|
}
|
||||||
color='none'
|
>
|
||||||
onClick={handleExportPdf}
|
<Button
|
||||||
isLoading={isPdfExportLoading}
|
variant='ghost'
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
color='none'
|
||||||
>
|
onClick={handleExportExcel}
|
||||||
<Icon icon='heroicons:document' width={20} height={20} />
|
isLoading={isExcelExportLoading}
|
||||||
Export to PDF
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
</Button>
|
>
|
||||||
</Dropdown>
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
</div>
|
Export to Excel - Supplier Per Sheet
|
||||||
);
|
</Button>
|
||||||
}, [setTabActions]);
|
<Button
|
||||||
|
variant='ghost'
|
||||||
useEffect(() => {
|
color='none'
|
||||||
return () => {
|
onClick={handleExportExcelGeneral}
|
||||||
clearTabActions(tabId);
|
isLoading={isExcelGeneralExportLoading}
|
||||||
};
|
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} />
|
||||||
return null;
|
Export to Excel - General
|
||||||
};
|
</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,
|
||||||
filterParams,
|
setTabActions,
|
||||||
|
tableFilterState,
|
||||||
|
filterModal.openModal,
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
|
handleExportExcelGeneral,
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
isExcelExportLoading,
|
isExcelExportLoading,
|
||||||
|
isExcelGeneralExportLoading,
|
||||||
isPdfExportLoading,
|
isPdfExportLoading,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => clearTabActions(tabId);
|
||||||
if (dateErrorShown) {
|
}, [tabId, clearTabActions]);
|
||||||
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>[] => [
|
||||||
{
|
{
|
||||||
@@ -604,9 +647,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'>
|
||||||
@@ -630,6 +673,27 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isLoading && data.length > 0 && meta && (
|
||||||
|
<div className='w-full ml-auto'>
|
||||||
|
<Pagination
|
||||||
|
totalItems={meta.total_results || 0}
|
||||||
|
itemsPerPage={meta.limit || 0}
|
||||||
|
currentPage={tableFilterState.page}
|
||||||
|
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
||||||
|
onNextPage={() =>
|
||||||
|
setPage(
|
||||||
|
meta.total_pages && tableFilterState.page < meta.total_pages
|
||||||
|
? tableFilterState.page + 1
|
||||||
|
: tableFilterState.page
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
onRowChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map((supplierReport) => {
|
data.map((supplierReport) => {
|
||||||
@@ -717,6 +781,27 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{!isLoading && data.length > 0 && meta && (
|
||||||
|
<div className='mt-5 px-3'>
|
||||||
|
<Pagination
|
||||||
|
totalItems={meta.total_results || 0}
|
||||||
|
itemsPerPage={meta.limit || 0}
|
||||||
|
currentPage={tableFilterState.page}
|
||||||
|
onPrevPage={() => setPage(Math.max(1, tableFilterState.page - 1))}
|
||||||
|
onNextPage={() =>
|
||||||
|
setPage(
|
||||||
|
meta.total_pages && tableFilterState.page < meta.total_pages
|
||||||
|
? tableFilterState.page + 1
|
||||||
|
: tableFilterState.page
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
onRowChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Modal */}
|
{/* Filter Modal */}
|
||||||
@@ -727,23 +812,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',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
{/* Modal Header */}
|
||||||
{/* Modal Header */}
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
<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'>
|
||||||
<div className='flex items-center gap-2 text-primary'>
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
<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>
|
||||||
@@ -752,153 +837,68 @@ 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='startDate'
|
name='start_date'
|
||||||
value={formik.values.startDate || ''}
|
value={formik.values.start_date || ''}
|
||||||
onChange={(e) => {
|
onChange={handleStartDateChange}
|
||||||
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>
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
<DateInput
|
<DateInput
|
||||||
name='endDate'
|
name='end_date'
|
||||||
value={formik.values.endDate || ''}
|
value={formik.values.end_date || ''}
|
||||||
onChange={(e) => {
|
onChange={handleEndDateChange}
|
||||||
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>
|
||||||
|
|
||||||
<div>
|
<SelectInputCheckbox
|
||||||
<SelectInputCheckbox
|
label='Supplier'
|
||||||
label='Supplier'
|
placeholder='Pilih Supplier'
|
||||||
placeholder='Pilih Supplier'
|
options={supplierOptions}
|
||||||
isMulti
|
value={formik.values.suppliers}
|
||||||
options={supplierOptions}
|
onChange={(val) =>
|
||||||
value={
|
formik.setFieldValue('suppliers', Array.isArray(val) ? val : [])
|
||||||
(formik.values.supplierIds as
|
}
|
||||||
| { value: number; label: string }
|
onInputChange={setSupplierInputValue}
|
||||||
| { value: number; label: string }[]
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
| null
|
isLoading={isLoadingSupplierOptions}
|
||||||
| undefined) || []
|
isClearable
|
||||||
}
|
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>
|
|
||||||
|
|
||||||
<div>
|
<SelectInputRadio
|
||||||
<SelectInputRadio
|
label='Filter Berdasarkan'
|
||||||
label='Filter Berdasarkan'
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
options={dataTypeOptions}
|
||||||
options={dataTypeOptions}
|
value={formik.values.filterBy ?? null}
|
||||||
value={
|
onChange={(val) =>
|
||||||
(formik.values.filterBy as
|
formik.setFieldValue(
|
||||||
| { value: string; label: string }
|
'filterBy',
|
||||||
| { value: string; label: string }[]
|
!Array.isArray(val) ? (val ?? undefined) : undefined
|
||||||
| null
|
)
|
||||||
| undefined) || null
|
}
|
||||||
}
|
className={{ wrapper: 'w-full' }}
|
||||||
onChange={(val) => {
|
isClearable
|
||||||
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>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Modal Footer */}
|
||||||
<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
|
||||||
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'
|
|
||||||
type='reset'
|
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
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
|
||||||
type='submit'
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={hasDateError}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -156,8 +156,17 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
|
formik.setValues({
|
||||||
|
start_date: filterParams.start_date || null,
|
||||||
|
end_date: filterParams.end_date || null,
|
||||||
|
area_ids: filterParams.area_id || null,
|
||||||
|
supplier_ids: filterParams.supplier_id || null,
|
||||||
|
product_ids: filterParams.product_id || null,
|
||||||
|
product_category_ids: filterParams.product_category_id || null,
|
||||||
|
filter_by: filterParams.filter_by || null,
|
||||||
|
sort_by: filterParams.sort_by || null,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { setFieldValue } = formik;
|
const { setFieldValue } = formik;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
export type DailyMarketingReportFilterType = {
|
export type DailyMarketingReportFilterType = {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
search: string | null;
|
search: string | null;
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
location_id: string | null;
|
location_id: string | null;
|
||||||
@@ -14,6 +16,8 @@ export type DailyMarketingReportFilterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DailyMarketingReportFilterSchema = yup.object({
|
export const DailyMarketingReportFilterSchema = yup.object({
|
||||||
|
page: yup.number().nullable(),
|
||||||
|
pageSize: yup.number().nullable(),
|
||||||
search: yup.string().nullable(),
|
search: yup.string().nullable(),
|
||||||
area_id: yup.string().nullable(),
|
area_id: yup.string().nullable(),
|
||||||
location_id: yup.string().nullable(),
|
location_id: yup.string().nullable(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
export type HppPerKandangFilterType = {
|
export type HppPerKandangFilterType = {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
location_id: string | null;
|
location_id: string | null;
|
||||||
kandang_id: string | null;
|
kandang_id: string | null;
|
||||||
@@ -12,6 +14,8 @@ export type HppPerKandangFilterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const HppPerKandangFilterSchema = yup.object({
|
export const HppPerKandangFilterSchema = yup.object({
|
||||||
|
page: yup.number().nullable(),
|
||||||
|
pageSize: yup.number().nullable(),
|
||||||
area_id: yup.string().nullable(),
|
area_id: yup.string().nullable(),
|
||||||
location_id: yup.string().nullable(),
|
location_id: yup.string().nullable(),
|
||||||
kandang_id: yup.string().nullable(),
|
kandang_id: yup.string().nullable(),
|
||||||
|
|||||||
@@ -17,16 +17,10 @@ import {
|
|||||||
formatVechicleNumber,
|
formatVechicleNumber,
|
||||||
formatTitleCase,
|
formatTitleCase,
|
||||||
} from '@/lib/helper';
|
} from '@/lib/helper';
|
||||||
import {
|
import { DailyMarketingRow } from '@/types/api/report/marketing';
|
||||||
DailyMarketingRow,
|
|
||||||
DailyMarketingReportResponse,
|
|
||||||
} from '@/types/api/report/marketing';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF';
|
|
||||||
import { generateDailyMarketingExcel } from '@/components/pages/report/marketing/export/DailyMarketingExportXLSX';
|
|
||||||
import { pdf } from '@react-pdf/renderer';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -39,8 +33,6 @@ import Modal, { useModal } from '@/components/Modal';
|
|||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton';
|
import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton';
|
||||||
import { useEffect as useEffectHook } from 'react';
|
import { useEffect as useEffectHook } from 'react';
|
||||||
import { httpClient } from '@/services/http/client';
|
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
|
||||||
import {
|
import {
|
||||||
MARKETING_DATE_FILTER_TYPE_OPTIONS,
|
MARKETING_DATE_FILTER_TYPE_OPTIONS,
|
||||||
MARKETING_TYPE_OPTIONS,
|
MARKETING_TYPE_OPTIONS,
|
||||||
@@ -53,6 +45,8 @@ interface DailyMarketingTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FilterParams {
|
interface FilterParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
area_id?: string;
|
area_id?: string;
|
||||||
location_id?: string;
|
location_id?: string;
|
||||||
warehouse_id?: string;
|
warehouse_id?: string;
|
||||||
@@ -116,6 +110,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<DailyMarketingReportFilterType>({
|
const formik = useFormik<DailyMarketingReportFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
search: null,
|
search: null,
|
||||||
area_id: null,
|
area_id: null,
|
||||||
location_id: null,
|
location_id: null,
|
||||||
@@ -130,6 +126,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
validationSchema: DailyMarketingReportFilterSchema,
|
validationSchema: DailyMarketingReportFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
setFilterParams({
|
setFilterParams({
|
||||||
|
page: values.page || undefined,
|
||||||
|
pageSize: values.pageSize || undefined,
|
||||||
area_id: values.area_id || undefined,
|
area_id: values.area_id || undefined,
|
||||||
location_id: values.location_id || undefined,
|
location_id: values.location_id || undefined,
|
||||||
warehouse_id: values.warehouse_id || undefined,
|
warehouse_id: values.warehouse_id || undefined,
|
||||||
@@ -150,8 +148,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
|
formik.setValues({
|
||||||
|
page: formik.values.page,
|
||||||
|
pageSize: formik.values.pageSize,
|
||||||
|
search: formik.values.search,
|
||||||
|
area_id: filterParams.area_id || null,
|
||||||
|
location_id: filterParams.location_id || null,
|
||||||
|
warehouse_id: filterParams.warehouse_id || null,
|
||||||
|
customer_id: filterParams.customer_id || null,
|
||||||
|
start_date: filterParams.start_date || null,
|
||||||
|
end_date: filterParams.end_date || null,
|
||||||
|
filter_by: filterParams.filter_by || null,
|
||||||
|
marketing_type: filterParams.marketing_type || null,
|
||||||
|
sort_by: filterParams.sort_by || null,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== SEARCH CHANGE HANDLER =====
|
// ===== SEARCH CHANGE HANDLER =====
|
||||||
@@ -222,6 +233,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
if (searchValue) params.set('search', searchValue);
|
||||||
|
if (filterParams.page) params.set('page', String(filterParams.page));
|
||||||
|
if (filterParams.pageSize)
|
||||||
|
params.set('limit', String(filterParams.pageSize));
|
||||||
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
||||||
if (filterParams.location_id)
|
if (filterParams.location_id)
|
||||||
params.set('location_id', filterParams.location_id);
|
params.set('location_id', filterParams.location_id);
|
||||||
@@ -262,67 +276,30 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
[dailyMarketings]
|
[dailyMarketings]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
|
||||||
const dailyMarketingsExport = useCallback(async (): Promise<
|
|
||||||
DailyMarketingRow[] | null
|
|
||||||
> => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
|
||||||
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
|
||||||
if (filterParams.location_id)
|
|
||||||
params.set('location_id', filterParams.location_id);
|
|
||||||
if (filterParams.warehouse_id)
|
|
||||||
params.set('warehouse_id', filterParams.warehouse_id);
|
|
||||||
if (filterParams.customer_id)
|
|
||||||
params.set('customer_id', filterParams.customer_id);
|
|
||||||
if (filterParams.start_date)
|
|
||||||
params.set('start_date', filterParams.start_date);
|
|
||||||
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
|
|
||||||
if (filterParams.filter_by) params.set('filter_by', filterParams.filter_by);
|
|
||||||
if (filterParams.marketing_type)
|
|
||||||
params.set('marketing_type', filterParams.marketing_type);
|
|
||||||
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
|
||||||
params.set('limit', '9999999');
|
|
||||||
|
|
||||||
const queryString = `?${params.toString()}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await httpClient<DailyMarketingReportResponse>(
|
|
||||||
`${MarketingReportApi.basePath}${queryString}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseError(response)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data || [];
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, [filterParams, searchValue]);
|
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const allDataForExport = await dailyMarketingsExport();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (!allDataForExport || allDataForExport.length === 0) {
|
if (searchValue) params.set('search', searchValue);
|
||||||
toast.error('Tidak ada data untuk diekspor.');
|
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
||||||
return;
|
if (filterParams.location_id)
|
||||||
}
|
params.set('location_id', filterParams.location_id);
|
||||||
|
if (filterParams.warehouse_id)
|
||||||
|
params.set('warehouse_id', filterParams.warehouse_id);
|
||||||
|
if (filterParams.customer_id)
|
||||||
|
params.set('customer_id', filterParams.customer_id);
|
||||||
|
if (filterParams.start_date)
|
||||||
|
params.set('start_date', filterParams.start_date);
|
||||||
|
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
|
||||||
|
if (filterParams.filter_by)
|
||||||
|
params.set('filter_by', filterParams.filter_by);
|
||||||
|
if (filterParams.marketing_type)
|
||||||
|
params.set('marketing_type', filterParams.marketing_type);
|
||||||
|
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
||||||
|
|
||||||
const period =
|
await MarketingReportApi.exportDailyMarketingToExcel(params.toString());
|
||||||
filterParams.start_date && filterParams.end_date
|
|
||||||
? `${formatDate(filterParams.start_date, 'DD-MMM-YYYY')}_to_${formatDate(filterParams.end_date, 'DD-MMM-YYYY')}`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await generateDailyMarketingExcel({
|
|
||||||
data: allDataForExport,
|
|
||||||
summaryTotal: summaryTotal,
|
|
||||||
period: period,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
@@ -330,34 +307,39 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [filterParams, dailyMarketingsExport, summaryTotal]);
|
}, [filterParams, searchValue]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const allDataForExport = await dailyMarketingsExport();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (!allDataForExport || allDataForExport.length === 0) {
|
if (searchValue) params.set('search', searchValue);
|
||||||
toast.error('Tidak ada data untuk diekspor.');
|
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
||||||
return;
|
if (filterParams.location_id)
|
||||||
}
|
params.set('location_id', filterParams.location_id);
|
||||||
|
if (filterParams.warehouse_id)
|
||||||
|
params.set('warehouse_id', filterParams.warehouse_id);
|
||||||
|
if (filterParams.customer_id)
|
||||||
|
params.set('customer_id', filterParams.customer_id);
|
||||||
|
if (filterParams.start_date)
|
||||||
|
params.set('start_date', filterParams.start_date);
|
||||||
|
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
|
||||||
|
if (filterParams.filter_by)
|
||||||
|
params.set('filter_by', filterParams.filter_by);
|
||||||
|
if (filterParams.marketing_type)
|
||||||
|
params.set('marketing_type', filterParams.marketing_type);
|
||||||
|
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
||||||
|
|
||||||
const dailyMarketingReportPdfBlob = await pdf(
|
await MarketingReportApi.exportDailyMarketingToPDF(params.toString());
|
||||||
<DailyMarketingReportPDF data={allDataForExport} total={summaryTotal} />
|
|
||||||
).toBlob();
|
|
||||||
|
|
||||||
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
dailyMarketingReportPdfBlob
|
|
||||||
);
|
|
||||||
window.open(dailyMarketingReportPdfUrl, '_blank');
|
|
||||||
|
|
||||||
toast.success('PDF berhasil dibuat.');
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsPdfExportLoading(false);
|
setIsPdfExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [dailyMarketingsExport, summaryTotal]);
|
}, [filterParams, searchValue]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS COMPONENT =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
const TabActions = useMemo(() => {
|
const TabActions = useMemo(() => {
|
||||||
@@ -572,7 +554,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'qty',
|
accessorKey: 'qty',
|
||||||
cell: (props) => formatNumber(props.row.original.qty),
|
cell: (props) => formatNumber(props.row.original.qty),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_qty
|
{summaryTotal?.total_qty
|
||||||
? formatNumber(summaryTotal.total_qty)
|
? formatNumber(summaryTotal.total_qty)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -585,7 +567,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'average_weight_kg',
|
accessorKey: 'average_weight_kg',
|
||||||
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_weight_kg
|
{summaryTotal?.average_weight_kg
|
||||||
? formatNumber(summaryTotal.average_weight_kg)
|
? formatNumber(summaryTotal.average_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -598,7 +580,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'total_weight_kg',
|
accessorKey: 'total_weight_kg',
|
||||||
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_weight_kg
|
{summaryTotal?.total_weight_kg
|
||||||
? formatNumber(summaryTotal.total_weight_kg)
|
? formatNumber(summaryTotal.total_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -611,9 +593,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'sales_price_per_kg',
|
accessorKey: 'sales_price_per_kg',
|
||||||
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_sales_price
|
{summaryTotal?.average_sales_price
|
||||||
? formatNumber(summaryTotal.average_sales_price)
|
? formatCurrency(summaryTotal.average_sales_price)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -624,7 +606,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'hpp_price_per_kg',
|
accessorKey: 'hpp_price_per_kg',
|
||||||
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_hpp_price_per_kg
|
{summaryTotal?.total_hpp_price_per_kg
|
||||||
? formatCurrency(summaryTotal.total_hpp_price_per_kg)
|
? formatCurrency(summaryTotal.total_hpp_price_per_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -637,7 +619,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'sales_amount',
|
accessorKey: 'sales_amount',
|
||||||
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_sales_amount
|
{summaryTotal?.total_sales_amount
|
||||||
? formatCurrency(summaryTotal.total_sales_amount)
|
? formatCurrency(summaryTotal.total_sales_amount)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -688,6 +670,27 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
<Table
|
<Table
|
||||||
data={data}
|
data={data}
|
||||||
columns={getTableColumns()}
|
columns={getTableColumns()}
|
||||||
|
pageSize={filterParams.pageSize}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(dailyMarketings)
|
||||||
|
? dailyMarketings?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(dailyMarketings)
|
||||||
|
? dailyMarketings?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={(newPage) =>
|
||||||
|
setFilterParams((prevVal) => ({ ...prevVal, page: newPage }))
|
||||||
|
}
|
||||||
|
onPageSizeChange={(newPageSize) =>
|
||||||
|
setFilterParams((prevVal) => ({
|
||||||
|
...prevVal,
|
||||||
|
pageSize: newPageSize,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
isLoading={isLoading}
|
||||||
renderFooter={data.length > 0}
|
renderFooter={data.length > 0}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-0!',
|
containerClassName: 'w-full mb-0!',
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ interface HppPerKandangTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FilterParams {
|
interface FilterParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
area_id?: string;
|
area_id?: string;
|
||||||
location_id?: string;
|
location_id?: string;
|
||||||
kandang_id?: string;
|
kandang_id?: string;
|
||||||
@@ -108,6 +110,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<HppPerKandangFilterType>({
|
const formik = useFormik<HppPerKandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
area_id: null,
|
area_id: null,
|
||||||
location_id: null,
|
location_id: null,
|
||||||
kandang_id: null,
|
kandang_id: null,
|
||||||
@@ -120,6 +124,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
validationSchema: HppPerKandangFilterSchema,
|
validationSchema: HppPerKandangFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
setFilterParams({
|
setFilterParams({
|
||||||
|
page: values.page || undefined,
|
||||||
|
pageSize: values.pageSize || undefined,
|
||||||
area_id: values.area_id || undefined,
|
area_id: values.area_id || undefined,
|
||||||
location_id: values.location_id || undefined,
|
location_id: values.location_id || undefined,
|
||||||
kandang_id: values.kandang_id || undefined,
|
kandang_id: values.kandang_id || undefined,
|
||||||
@@ -146,8 +152,19 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
|
formik.setValues({
|
||||||
|
page: formik.values.page,
|
||||||
|
pageSize: formik.values.pageSize,
|
||||||
|
area_id: filterParams.area_id || null,
|
||||||
|
location_id: filterParams.location_id || null,
|
||||||
|
kandang_id: filterParams.kandang_id || null,
|
||||||
|
weight_min: filterParams.weight_min || null,
|
||||||
|
weight_max: filterParams.weight_max || null,
|
||||||
|
period: filterParams.period || null,
|
||||||
|
sort_by: filterParams.sort_by || null,
|
||||||
|
show_unrecorded: filterParams.show_unrecorded ?? false,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== WEIGHT CHANGE HANDLERS =====
|
// ===== WEIGHT CHANGE HANDLERS =====
|
||||||
@@ -257,6 +274,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
period: filterParams.period,
|
period: filterParams.period,
|
||||||
sort_by: filterParams.sort_by,
|
sort_by: filterParams.sort_by,
|
||||||
show_unrecorded: filterParams.show_unrecorded,
|
show_unrecorded: filterParams.show_unrecorded,
|
||||||
|
page: filterParams.page,
|
||||||
|
pageSize: filterParams.pageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ['hpp-per-kandang-report', params];
|
return ['hpp-per-kandang-report', params];
|
||||||
@@ -271,7 +290,9 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
params.weight_max,
|
params.weight_max,
|
||||||
params.period,
|
params.period,
|
||||||
params.sort_by,
|
params.sort_by,
|
||||||
params.show_unrecorded
|
params.show_unrecorded,
|
||||||
|
params.page,
|
||||||
|
params.pageSize
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -321,7 +342,9 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
params.weight_max,
|
params.weight_max,
|
||||||
params.period,
|
params.period,
|
||||||
params.sort_by,
|
params.sort_by,
|
||||||
params.show_unrecorded
|
params.show_unrecorded,
|
||||||
|
params.page,
|
||||||
|
params.limit
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
@@ -466,6 +489,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={filterParams}
|
values={filterParams}
|
||||||
|
excludeFields={['page', 'pageSize']}
|
||||||
onClick={() => handleFilterModalOpenRef.current()}
|
onClick={() => handleFilterModalOpenRef.current()}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -845,6 +869,25 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
<Table
|
<Table
|
||||||
data={data}
|
data={data}
|
||||||
columns={getTableColumns()}
|
columns={getTableColumns()}
|
||||||
|
pageSize={filterParams.pageSize}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(hppPerKandang) ? hppPerKandang?.meta?.page : 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(hppPerKandang)
|
||||||
|
? hppPerKandang?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={(newPage) =>
|
||||||
|
setFilterParams((prevVal) => ({ ...prevVal, page: newPage }))
|
||||||
|
}
|
||||||
|
onPageSizeChange={(newPageSize) =>
|
||||||
|
setFilterParams((prevVal) => ({
|
||||||
|
...prevVal,
|
||||||
|
pageSize: newPageSize,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
isLoading={isLoading}
|
||||||
renderFooter={data.length > 0}
|
renderFooter={data.length > 0}
|
||||||
renderCustomRow={renderCustomRow}
|
renderCustomRow={renderCustomRow}
|
||||||
className={{
|
className={{
|
||||||
|
|||||||
+36
-1
@@ -263,8 +263,43 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
|
const restoredAreaId = filterParams.area_id
|
||||||
|
? areaOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.area_id
|
||||||
|
) || { value: filterParams.area_id, label: filterParams.area_id }
|
||||||
|
: null;
|
||||||
|
const restoredLocationId = filterParams.location_id
|
||||||
|
? locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.location_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.location_id,
|
||||||
|
label: filterParams.location_id,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const restoredProjectFlockId = filterParams.project_flock_id
|
||||||
|
? projectFlockOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.project_flock_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.project_flock_id,
|
||||||
|
label: filterParams.project_flock_id,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const restoredKandangId = filterParams.project_flock_kandang_id
|
||||||
|
? projectFlockKandangOptions.find(
|
||||||
|
(opt) => String(opt.value) === filterParams.project_flock_kandang_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.project_flock_kandang_id,
|
||||||
|
label: filterParams.project_flock_kandang_id,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
formik.setValues({
|
||||||
|
area_id: restoredAreaId,
|
||||||
|
location_id: restoredLocationId,
|
||||||
|
project_flock_id: restoredProjectFlockId,
|
||||||
|
kandang_id: restoredKandangId,
|
||||||
|
});
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
formik.validateForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
|
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
|
||||||
|
|||||||
+11
-1
@@ -197,6 +197,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
icon: 'heroicons-outline:folder',
|
icon: 'heroicons-outline:folder',
|
||||||
permission: [
|
permission: [
|
||||||
'lti.inventory.product_stock.list',
|
'lti.inventory.product_stock.list',
|
||||||
|
'lti.inventory.stock_log.list',
|
||||||
'lti.inventory.product_warehouses.list',
|
'lti.inventory.product_warehouses.list',
|
||||||
'lti.inventory.transfer.list',
|
'lti.inventory.transfer.list',
|
||||||
],
|
],
|
||||||
@@ -204,7 +205,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
{
|
{
|
||||||
text: 'Stok Produk',
|
text: 'Stok Produk',
|
||||||
link: '/inventory/product',
|
link: '/inventory/product',
|
||||||
permission: ['lti.inventory.product_stock.list'],
|
permission: [
|
||||||
|
'lti.inventory.product_stock.list',
|
||||||
|
'lti.inventory.stock_log.list',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Penyesuaian Stok',
|
text: 'Penyesuaian Stok',
|
||||||
@@ -236,6 +240,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
'lti.master.uoms.list',
|
'lti.master.uoms.list',
|
||||||
'lti.master.warehouses.list',
|
'lti.master.warehouses.list',
|
||||||
'lti.master.production_standards.list',
|
'lti.master.production_standards.list',
|
||||||
|
'lti.system_settings.update',
|
||||||
],
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
@@ -303,6 +308,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/master-data/production-standard',
|
link: '/master-data/production-standard',
|
||||||
permission: ['lti.master.production_standards.list'],
|
permission: ['lti.master.production_standards.list'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Konfigurasi Sistem',
|
||||||
|
link: '/master-data/system-config',
|
||||||
|
permission: ['lti.system_settings.update'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -218,4 +218,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/master-data/production-standard/detail/edit/': [
|
'/master-data/production-standard/detail/edit/': [
|
||||||
'lti.master.production_standards.update',
|
'lti.master.production_standards.update',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'/master-data/system-config/': ['lti.system_settings.update'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface DatePickerProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
formatDisplay?: (date: string) => string;
|
formatDisplay?: (date: string) => string;
|
||||||
|
hasError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DatePicker({
|
export function DatePicker({
|
||||||
@@ -28,6 +29,7 @@ export function DatePicker({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
placeholder = 'Select date',
|
placeholder = 'Select date',
|
||||||
formatDisplay,
|
formatDisplay,
|
||||||
|
hasError = false,
|
||||||
}: DatePickerProps) {
|
}: DatePickerProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [currentMonth, setCurrentMonth] = useState(() => {
|
const [currentMonth, setCurrentMonth] = useState(() => {
|
||||||
@@ -154,7 +156,7 @@ export function DatePicker({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className='w-full justify-start text-left font-normal border-gray-200 hover:bg-gray-50'
|
className={`w-full justify-start text-left font-normal hover:bg-gray-50 ${hasError ? 'border-red-500 focus:ring-red-500' : 'border-gray-200'}`}
|
||||||
>
|
>
|
||||||
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
||||||
{date ? (
|
{date ? (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -89,7 +89,10 @@ export function Dashboard() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
||||||
|
order_by: 'asc',
|
||||||
|
sort_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const target = e.target as HTMLDivElement;
|
const target = e.target as HTMLDivElement;
|
||||||
|
|||||||
+320
-37
@@ -40,11 +40,12 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef, Row } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'ALL', label: 'Semua Status' },
|
{ value: 'ALL', label: 'Semua Status' },
|
||||||
@@ -59,6 +60,7 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
pullet_close: 'Pullet Close',
|
pullet_close: 'Pullet Close',
|
||||||
produksi_open: 'Produksi Open',
|
produksi_open: 'Produksi Open',
|
||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
|
empty_kandang: 'Kandang Kosong',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ListDailyChecklistContent() {
|
export function ListDailyChecklistContent() {
|
||||||
@@ -87,6 +89,9 @@ export function ListDailyChecklistContent() {
|
|||||||
date_from: 'date_from',
|
date_from: 'date_from',
|
||||||
date_to: 'date_to',
|
date_to: 'date_to',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persist: true,
|
||||||
|
storeName: 'list-daily-checklist-content-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -105,7 +110,10 @@ export function ListDailyChecklistContent() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
||||||
|
order_by: 'asc',
|
||||||
|
sort_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
const checklistList = isResponseSuccess(checklistListRes)
|
const checklistList = isResponseSuccess(checklistListRes)
|
||||||
? checklistListRes.data || []
|
? checklistListRes.data || []
|
||||||
@@ -122,12 +130,29 @@ export function ListDailyChecklistContent() {
|
|||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
|
const [showBulkApproveModal, setShowBulkApproveModal] = useState(false);
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
|
const [showBulkRejectModal, setShowBulkRejectModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
const selectedRowIds = Object.keys(rowSelection);
|
||||||
|
|
||||||
|
const selectedRowItems = selectedRowIds.map((itemId) =>
|
||||||
|
checklistList.find((item) => item.id === parseInt(itemId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableEnableRowSelectionHandler: (
|
||||||
|
row: Row<DailyChecklist>
|
||||||
|
) => boolean = (row) => {
|
||||||
|
return (
|
||||||
|
row.original.status !== 'APPROVED' && row.original.status !== 'REJECTED'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDetail = (item: DailyChecklist) => {
|
const handleDetail = (item: DailyChecklist) => {
|
||||||
router.push(
|
router.push(
|
||||||
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
||||||
@@ -135,13 +160,7 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item: DailyChecklist) => {
|
const handleEdit = (item: DailyChecklist) => {
|
||||||
const formattedDate = new Date(item.date).toISOString().split('T')[0];
|
router.push(`/daily-checklist/daily-checklist?checklistId=${item.id}`);
|
||||||
const kandangId = item.kandang?.id ?? '';
|
|
||||||
const category = item.category;
|
|
||||||
|
|
||||||
router.push(
|
|
||||||
`/daily-checklist/daily-checklist?date=${formattedDate}&kandang_id=${kandangId}&category=${category}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = (item: DailyChecklist) => {
|
const handleApprove = (item: DailyChecklist) => {
|
||||||
@@ -149,21 +168,22 @@ export function ListDailyChecklistContent() {
|
|||||||
setShowApproveModal(true);
|
setShowApproveModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBulkApprove = () => {
|
||||||
|
setShowBulkApproveModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReject = (item: DailyChecklist) => {
|
const handleReject = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
setShowRejectModal(true);
|
setShowRejectModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (item: DailyChecklist) => {
|
const handleBulkReject = () => {
|
||||||
// ✅ VALIDATION: Only DRAFT can be deleted
|
setRejectReason('');
|
||||||
if (item.status !== 'DRAFT') {
|
setShowBulkRejectModal(true);
|
||||||
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
};
|
||||||
description: `Status saat ini: ${item.status}`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleDelete = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
};
|
};
|
||||||
@@ -195,6 +215,31 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmBulkApprove = async () => {
|
||||||
|
if (!selectedRowIds.length) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setActionLoading(true);
|
||||||
|
|
||||||
|
const approveRes = await DailyChecklistApi.bulkApprove(selectedRowIds);
|
||||||
|
|
||||||
|
if (isResponseError(approveRes)) {
|
||||||
|
toast.error('Gagal approve checklist: ' + approveRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshChecklistList();
|
||||||
|
toast.success('Checklist berhasil di-approve');
|
||||||
|
setShowBulkApproveModal(false);
|
||||||
|
setRowSelection({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error approving checklist:', error);
|
||||||
|
toast.error('Terjadi kesalahan');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmReject = async () => {
|
const confirmReject = async () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
@@ -229,6 +274,40 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmBulkReject = async () => {
|
||||||
|
if (!selectedRowIds.length) return;
|
||||||
|
|
||||||
|
if (!rejectReason.trim()) {
|
||||||
|
toast.error('Alasan reject harus diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setActionLoading(true);
|
||||||
|
|
||||||
|
const rejectRes = await DailyChecklistApi.bulkReject(
|
||||||
|
selectedRowIds,
|
||||||
|
rejectReason
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(rejectRes)) {
|
||||||
|
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshChecklistList();
|
||||||
|
toast.success('Checklist berhasil di-reject');
|
||||||
|
setShowBulkRejectModal(false);
|
||||||
|
setRowSelection({});
|
||||||
|
setRejectReason('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rejecting checklist:', error);
|
||||||
|
toast.error('Terjadi kesalahan');
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
@@ -325,6 +404,37 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<div className='w-full flex flex-row justify-center'>
|
||||||
|
<CheckboxInput
|
||||||
|
name='allRow'
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
indeterminate={table.getIsSomeRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isCheckboxDisabled =
|
||||||
|
!row.getCanSelect() ||
|
||||||
|
row.original.status === 'APPROVED' ||
|
||||||
|
row.original.status === 'REJECTED';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CheckboxInput
|
||||||
|
name='row'
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
disabled={isCheckboxDisabled}
|
||||||
|
indeterminate={row.getIsSomeSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'date',
|
accessorKey: 'date',
|
||||||
header: 'Tanggal',
|
header: 'Tanggal',
|
||||||
@@ -437,19 +547,17 @@ export function ListDailyChecklistContent() {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{row.original.status === 'DRAFT' && (
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
<Button
|
||||||
<Button
|
size='sm'
|
||||||
size='sm'
|
variant='destructive'
|
||||||
variant='destructive'
|
onClick={() => handleDelete(row.original)}
|
||||||
onClick={() => handleDelete(row.original)}
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
>
|
||||||
>
|
<Trash2 className='w-4 h-4 mr-1' />
|
||||||
<Trash2 className='w-4 h-4 mr-1' />
|
Hapus
|
||||||
Hapus
|
</Button>
|
||||||
</Button>
|
</RequirePermission>
|
||||||
</RequirePermission>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -459,13 +567,39 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div className='mb-6'>
|
<div className='mb-6 flex flex-row justify-between items-center gap-3'>
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
<div>
|
||||||
List Daily Checklist
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
</h1>
|
List Daily Checklist
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
</h1>
|
||||||
Daftar semua checklist harian
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
</p>
|
Daftar semua checklist harian
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<div className='flex flex-row items-center gap-3'>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
onClick={handleBulkApprove}
|
||||||
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
|
>
|
||||||
|
<CheckCircle className='w-4 h-4 mr-1' />
|
||||||
|
Bulk Approve {`(${selectedRowIds.length}) item`}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='destructive'
|
||||||
|
onClick={handleBulkReject}
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
<XCircle className='w-4 h-4 mr-1' />
|
||||||
|
Bulk Reject {`(${selectedRowIds.length}) item`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Card */}
|
{/* Main Card */}
|
||||||
@@ -588,6 +722,10 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
isLoading={isLoadingChecklistList}
|
isLoading={isLoadingChecklistList}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
|
withCheckbox
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn({
|
||||||
'w-full mb-20':
|
'w-full mb-20':
|
||||||
@@ -666,6 +804,76 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Bulk Approve Modal */}
|
||||||
|
<Dialog
|
||||||
|
open={showBulkApproveModal}
|
||||||
|
onOpenChange={setShowBulkApproveModal}
|
||||||
|
>
|
||||||
|
<DialogContent className='sm:max-w-md max-h-[80vh] overflow-y-auto bg-white rounded-xl shadow-lg'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Approve Checklist</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Apakah Anda yakin ingin approve {selectedRowIds.length} checklist
|
||||||
|
ini?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
|
||||||
|
{selectedRowItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item?.id ?? 0}
|
||||||
|
className='bg-gray-50 rounded-lg p-4 space-y-2'
|
||||||
|
>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Tanggal:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{formatDate(item?.date ?? '')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kandang:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.kandang?.name ?? '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kategori:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.category
|
||||||
|
? (CATEGORY_LABELS[item.category] ?? item?.category)
|
||||||
|
: item?.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Progress:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.progress}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowBulkApproveModal(false)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className='border-gray-200'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={confirmBulkApprove}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Memproses...' : 'Ya, Approve'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Reject Modal */}
|
{/* Reject Modal */}
|
||||||
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
@@ -735,6 +943,81 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Bulk Reject Modal */}
|
||||||
|
<Dialog open={showBulkRejectModal} onOpenChange={setShowBulkRejectModal}>
|
||||||
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Reject Checklist</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Berikan alasan reject untuk checklist ini
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
|
||||||
|
{selectedRowItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item?.id ?? 0}
|
||||||
|
className='bg-gray-50 rounded-lg p-4 space-y-2 mb-4'
|
||||||
|
>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Tanggal:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{formatDate(item?.date ?? '')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kandang:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.kandang?.name ?? '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between text-sm'>
|
||||||
|
<span className='text-gray-600'>Kategori:</span>
|
||||||
|
<span className='font-medium text-gray-900'>
|
||||||
|
{item?.category
|
||||||
|
? CATEGORY_LABELS[item.category] || item?.category
|
||||||
|
: item?.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor='reject-reason'>
|
||||||
|
Alasan Reject <span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id='reject-reason'
|
||||||
|
value={rejectReason}
|
||||||
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
|
placeholder='Tuliskan alasan reject...'
|
||||||
|
className='mt-1.5 border-gray-200 min-h-[100px]'
|
||||||
|
disabled={actionLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => setShowBulkRejectModal(false)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className='border-gray-200'
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={confirmBulkReject}
|
||||||
|
disabled={actionLoading}
|
||||||
|
variant='destructive'
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Memproses...' : 'Ya, Reject'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Modal */}
|
{/* Delete Modal */}
|
||||||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
|||||||
+190
-33
@@ -2,7 +2,14 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Share2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import * as htmlToImage from 'html-to-image';
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Badge } from '@/figma-make/components/base/badge';
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
@@ -53,6 +60,7 @@ interface ChecklistHeader {
|
|||||||
progress_percent: number;
|
progress_percent: number;
|
||||||
total_phases: number;
|
total_phases: number;
|
||||||
total_activities: number;
|
total_activities: number;
|
||||||
|
empty_kandang_end_date?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PhaseGroup {
|
interface PhaseGroup {
|
||||||
@@ -106,6 +114,7 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
pullet_close: 'Pullet Close',
|
pullet_close: 'Pullet Close',
|
||||||
produksi_open: 'Produksi Open',
|
produksi_open: 'Produksi Open',
|
||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
|
empty_kandang: 'Kandang Kosong',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
||||||
@@ -137,6 +146,8 @@ export function DetailDailyChecklistContent() {
|
|||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
|
const [isGeneratingImage, setIsGeneratingImage] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (checklistId) {
|
if (checklistId) {
|
||||||
fetchChecklistDetail();
|
fetchChecklistDetail();
|
||||||
@@ -169,6 +180,9 @@ export function DetailDailyChecklistContent() {
|
|||||||
|
|
||||||
setDocuments(rawDetailChecklist?.document_urls || []);
|
setDocuments(rawDetailChecklist?.document_urls || []);
|
||||||
|
|
||||||
|
const emptyKandangEndDate =
|
||||||
|
rawDetailChecklist?.empty_kandang?.end_date ?? null;
|
||||||
|
|
||||||
const checklistData = {
|
const checklistData = {
|
||||||
id: rawDetailChecklist?.id,
|
id: rawDetailChecklist?.id,
|
||||||
date: rawDetailChecklist?.date,
|
date: rawDetailChecklist?.date,
|
||||||
@@ -195,6 +209,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: 0,
|
total_phases: 0,
|
||||||
total_activities: 0,
|
total_activities: 0,
|
||||||
|
empty_kandang_end_date: emptyKandangEndDate,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -262,6 +277,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
||||||
total_activities: tasks.length,
|
total_activities: tasks.length,
|
||||||
|
empty_kandang_end_date: emptyKandangEndDate,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -312,6 +328,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: progressPercent,
|
progress_percent: progressPercent,
|
||||||
total_phases: uniquePhases.size,
|
total_phases: uniquePhases.size,
|
||||||
total_activities: uniqueActivities.size,
|
total_activities: uniqueActivities.size,
|
||||||
|
empty_kandang_end_date: emptyKandangEndDate,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching checklist detail:', error);
|
console.error('Error fetching checklist detail:', error);
|
||||||
@@ -547,6 +564,103 @@ export function DetailDailyChecklistContent() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMobileDevice = () => {
|
||||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||||
|
navigator.userAgent
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusMessage = () => {
|
||||||
|
switch (header?.status) {
|
||||||
|
case 'DRAFT':
|
||||||
|
return 'Checklist harian perlu disubmit';
|
||||||
|
case 'SUBMITTED':
|
||||||
|
return 'Checklist harian menunggu persetujuan';
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'Checklist harian telah disetujui';
|
||||||
|
case 'REJECTED':
|
||||||
|
return 'Checklist harian telah ditolak';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareHandler = async () => {
|
||||||
|
const isMobile = isMobileDevice();
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
setIsGeneratingImage(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseTitle = `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`;
|
||||||
|
const statusMsg = getStatusMessage();
|
||||||
|
const statusInfo = `\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}`;
|
||||||
|
const urlMessage = `\n\nView full checklist: ${window.location.href}`;
|
||||||
|
const fullMessage = baseTitle + statusInfo + urlMessage;
|
||||||
|
|
||||||
|
let shareData: ShareData;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
const htmlBlob = await htmlToImage.toBlob(document.body, {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
});
|
||||||
|
const imgFile = new File(
|
||||||
|
[htmlBlob!],
|
||||||
|
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
|
||||||
|
{
|
||||||
|
type: 'image/png',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
shareData = {
|
||||||
|
files: [imgFile],
|
||||||
|
title: baseTitle,
|
||||||
|
text: fullMessage,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
shareData = {
|
||||||
|
title: baseTitle,
|
||||||
|
text: fullMessage,
|
||||||
|
url: window.location.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGeneratingImage(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!navigator.canShare(shareData)) {
|
||||||
|
toast.error(
|
||||||
|
'Gagal membagikan checklist, coba dengan perangkat yang berbeda'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.share(shareData);
|
||||||
|
toast.success('Checklist berhasil dibagikan');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Gagal membagikan checklist');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareToWhatsAppHandler = async () => {
|
||||||
|
const isMobile = isMobileDevice();
|
||||||
|
setIsGeneratingImage(true);
|
||||||
|
|
||||||
|
const statusMsg = getStatusMessage();
|
||||||
|
const category = header?.category || '';
|
||||||
|
const message = encodeURIComponent(
|
||||||
|
`Daily Checklist\n\nTanggal: ${formatDate(header?.date || '')}\nKandang: ${header?.kandang_name}\nKategori: ${CATEGORY_LABELS[category] || category}\nProgress: ${header?.progress_percent}%\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}`
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsGeneratingImage(false);
|
||||||
|
|
||||||
|
const whatsappUrl = isMobile
|
||||||
|
? `https://wa.me/?text=${message}`
|
||||||
|
: `https://web.whatsapp.com/send?text=${message}`;
|
||||||
|
|
||||||
|
window.open(whatsappUrl, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
@@ -573,8 +687,8 @@ export function DetailDailyChecklistContent() {
|
|||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Page Title with Back Button */}
|
{/* Action Buttons */}
|
||||||
<div className='mb-6 flex items-center gap-4'>
|
<div className='mb-6 flex items-start sm:items-center justify-between gap-4 flex-wrap'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -584,37 +698,68 @@ export function DetailDailyChecklistContent() {
|
|||||||
<ArrowLeft className='w-4 h-4 mr-1' />
|
<ArrowLeft className='w-4 h-4 mr-1' />
|
||||||
Kembali
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
<div className='flex-1'>
|
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
<div className='flex items-center gap-2 flex-wrap'>
|
||||||
Detail Daily Checklist
|
{header.status === 'SUBMITTED' && (
|
||||||
</h1>
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
<div className='flex gap-2 flex-wrap'>
|
||||||
Lihat detail checklist harian
|
<Button
|
||||||
</p>
|
onClick={handleApprove}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
|
>
|
||||||
|
<CheckCircle className='w-4 h-4 mr-2' />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleReject}
|
||||||
|
disabled={actionLoading}
|
||||||
|
variant='destructive'
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
<XCircle className='w-4 h-4 mr-2' />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={shareHandler}
|
||||||
|
disabled={isGeneratingImage}
|
||||||
|
className='border-gray-200'
|
||||||
|
>
|
||||||
|
<Share2 className='w-4 h-4 mr-1' />
|
||||||
|
{!isGeneratingImage && 'Bagikan'}
|
||||||
|
|
||||||
|
{isGeneratingImage && 'Memuat...'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={shareToWhatsAppHandler}
|
||||||
|
disabled={isGeneratingImage}
|
||||||
|
className='border-gray-200'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:whatsapp' className='w-4 h-4 mr-1' />
|
||||||
|
{!isGeneratingImage && 'Bagikan via WhatsApp'}
|
||||||
|
|
||||||
|
{isGeneratingImage && 'Memuat...'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{header.status === 'SUBMITTED' && (
|
</div>
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
|
||||||
<div className='flex gap-2'>
|
{/* Page Title */}
|
||||||
<Button
|
<div className='mb-6'>
|
||||||
onClick={handleApprove}
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
disabled={actionLoading}
|
Detail Daily Checklist
|
||||||
className='bg-green-600 hover:bg-green-700 text-white'
|
</h1>
|
||||||
>
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
<CheckCircle className='w-4 h-4 mr-2' />
|
Lihat detail checklist harian
|
||||||
Approve
|
</p>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleReject}
|
|
||||||
disabled={actionLoading}
|
|
||||||
variant='destructive'
|
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
|
||||||
>
|
|
||||||
<XCircle className='w-4 h-4 mr-2' />
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</RequirePermission>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header Info Card */}
|
{/* Header Info Card */}
|
||||||
@@ -639,6 +784,18 @@ export function DetailDailyChecklistContent() {
|
|||||||
{CATEGORY_LABELS[header.category] || header.category}
|
{CATEGORY_LABELS[header.category] || header.category}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{header.category === 'empty_kandang' && (
|
||||||
|
<div>
|
||||||
|
<Label className='text-xs text-gray-500'>
|
||||||
|
Tanggal Selesai Kandang Kosong
|
||||||
|
</Label>
|
||||||
|
<p className='text-sm font-medium text-gray-900 mt-1'>
|
||||||
|
{header.empty_kandang_end_date
|
||||||
|
? formatDate(header.empty_kandang_end_date)
|
||||||
|
: '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Label className='text-xs text-gray-500'>Status</Label>
|
<Label className='text-xs text-gray-500'>Status</Label>
|
||||||
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ export function MasterEmployeeContent() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
||||||
|
order_by: 'asc',
|
||||||
|
sort_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const target = e.target as HTMLDivElement;
|
const target = e.target as HTMLDivElement;
|
||||||
@@ -217,7 +220,9 @@ export function MasterEmployeeContent() {
|
|||||||
'Error creating employee:',
|
'Error creating employee:',
|
||||||
createEmployeeResponse.message
|
createEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error('Gagal menambahkan ABK');
|
toast.error(
|
||||||
|
'Gagal menambahkan ABK: ' + createEmployeeResponse.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +243,9 @@ export function MasterEmployeeContent() {
|
|||||||
'Error updating employee:',
|
'Error updating employee:',
|
||||||
updateEmployeeResponse.message
|
updateEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error('Gagal menambahkan ABK');
|
toast.error(
|
||||||
|
'Gagal memperbarui ABK: ' + updateEmployeeResponse.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ import { cn } from '@/lib/helper';
|
|||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
|
||||||
import { UserApi } from '@/services/api/user';
|
import { UserApi } from '@/services/api/user';
|
||||||
|
|
||||||
export function MasterKandangContent() {
|
export function MasterKandangContent() {
|
||||||
@@ -108,12 +107,6 @@ export function MasterKandangContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
options: kandangOptions,
|
|
||||||
isLoadingMore: isLoadingKandangOptionsMore,
|
|
||||||
loadMore: loadMoreKandang,
|
|
||||||
} = useSelect(KandangApi.basePath, 'id', 'name');
|
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
||||||
@@ -375,7 +368,9 @@ export function MasterKandangContent() {
|
|||||||
name='search'
|
name='search'
|
||||||
placeholder='Cari kandang...'
|
placeholder='Cari kandang...'
|
||||||
value={tableFilterState.search}
|
value={tableFilterState.search}
|
||||||
onChange={(e) => updateFilter('search', e.target.value)}
|
onChange={(e) =>
|
||||||
|
updateFilter('search', e.target.value, true)
|
||||||
|
}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full sm:w-[280px] border-gray-200',
|
wrapper: 'w-full sm:w-[280px] border-gray-200',
|
||||||
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
||||||
@@ -390,7 +385,11 @@ export function MasterKandangContent() {
|
|||||||
<Select
|
<Select
|
||||||
value={tableFilterState.location_id}
|
value={tableFilterState.location_id}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateFilter('location_id', value === 'all' ? '' : value)
|
updateFilter(
|
||||||
|
'location_id',
|
||||||
|
value === 'all' ? '' : value,
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='w-[180px] border-gray-200'>
|
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { SystemSettingsApi } from '@/services/api/system-settings';
|
||||||
|
import { SystemSetting } from '@/types/api/system-settings/system-setting';
|
||||||
|
|
||||||
|
const ALLOW_NEGATIVE_PAKAN_OVK_KEY = 'allow_negative_pakan_ovk';
|
||||||
|
|
||||||
|
function SettingToggle({
|
||||||
|
setting,
|
||||||
|
onToggle,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
setting: SystemSetting;
|
||||||
|
onToggle: (key: string, currentValue: boolean) => void;
|
||||||
|
loading: boolean;
|
||||||
|
}) {
|
||||||
|
const isEnabled = setting.value === 'true';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-start justify-between gap-4 py-5'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<p className='text-sm font-medium text-gray-900'>
|
||||||
|
{setting.key === ALLOW_NEGATIVE_PAKAN_OVK_KEY
|
||||||
|
? 'Mode Migrasi PAKAN & OVK'
|
||||||
|
: setting.key}
|
||||||
|
</p>
|
||||||
|
{setting.description && (
|
||||||
|
<p className='text-sm text-gray-500 mt-0.5'>{setting.description}</p>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center mt-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
isEnabled
|
||||||
|
? 'bg-amber-100 text-amber-700'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isEnabled ? 'Aktif' : 'Nonaktif'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
role='switch'
|
||||||
|
aria-checked={isEnabled}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => onToggle(setting.key, isEnabled)}
|
||||||
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[#0069e0] focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||||
|
isEnabled ? 'bg-[#0069e0]' : 'bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden='true'
|
||||||
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
isEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemConfigContent() {
|
||||||
|
const [toggling, setToggling] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: settingsResponse,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshSettings,
|
||||||
|
} = useSWR(SystemSettingsApi.basePath, SystemSettingsApi.getAllFetcher, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggle = async (key: string, currentValue: boolean) => {
|
||||||
|
if (key !== ALLOW_NEGATIVE_PAKAN_OVK_KEY) return;
|
||||||
|
|
||||||
|
setToggling(key);
|
||||||
|
try {
|
||||||
|
const res = await SystemSettingsApi.setAllowNegativePakanOvk({
|
||||||
|
value: !currentValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseError(res)) {
|
||||||
|
toast.error(res.message || 'Gagal mengubah pengaturan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSettings();
|
||||||
|
toast.success(
|
||||||
|
!currentValue
|
||||||
|
? 'Mode migrasi PAKAN & OVK diaktifkan'
|
||||||
|
: 'Mode migrasi PAKAN & OVK dinonaktifkan'
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
toast.error('Terjadi kesalahan saat mengubah pengaturan');
|
||||||
|
} finally {
|
||||||
|
setToggling(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = isResponseSuccess(settingsResponse)
|
||||||
|
? settingsResponse.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (isLoading && !settingsResponse) {
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Konfigurasi Sistem
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data •{' '}
|
||||||
|
<span className='text-[#0069e0]'>Konfigurasi Sistem</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-12 text-center text-gray-500'>
|
||||||
|
Memuat data...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Konfigurasi Sistem
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data •{' '}
|
||||||
|
<span className='text-[#0069e0]'>Konfigurasi Sistem</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-0'>
|
||||||
|
<div className='px-6 py-4 border-b border-gray-200/60'>
|
||||||
|
<h2 className='text-base font-semibold text-gray-800'>
|
||||||
|
Pengaturan Global
|
||||||
|
</h2>
|
||||||
|
<p className='text-sm text-gray-500 mt-0.5'>
|
||||||
|
Pengaturan ini berlaku untuk seluruh sistem.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='px-6 divide-y divide-gray-200/60'>
|
||||||
|
{settings.length === 0 ? (
|
||||||
|
<p className='py-10 text-center text-sm text-gray-500'>
|
||||||
|
Tidak ada pengaturan tersedia.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
settings.map((setting) => (
|
||||||
|
<SettingToggle
|
||||||
|
key={setting.key}
|
||||||
|
setting={setting}
|
||||||
|
onToggle={handleToggle}
|
||||||
|
loading={toggling === setting.key}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -137,6 +137,8 @@ export function DailyChecklistReportsContent() {
|
|||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
||||||
area_id: tableFilterState.area_id,
|
area_id: tableFilterState.area_id,
|
||||||
location_id: tableFilterState.location_id,
|
location_id: tableFilterState.location_id,
|
||||||
|
order_by: 'asc',
|
||||||
|
sort_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
@@ -159,17 +161,24 @@ export function DailyChecklistReportsContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { options: employeeOptions } = useSelect(
|
const {
|
||||||
EmployeeApi.basePath,
|
options: employeeOptions,
|
||||||
'id',
|
loadMore: loadMoreEmployee,
|
||||||
'name',
|
isLoadingMore: isLoadingMoreEmployee,
|
||||||
'search',
|
} = useSelect(EmployeeApi.basePath, 'id', 'name', 'search', {
|
||||||
{
|
order_by: 'asc',
|
||||||
page: '1',
|
sort_by: 'name',
|
||||||
limit: '500',
|
kandang_id: tableFilterState.kandang_id,
|
||||||
kandang_id: tableFilterState.kandang_id,
|
});
|
||||||
|
|
||||||
|
const handleEmployeeScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
||||||
|
if (!isLoadingMoreEmployee) {
|
||||||
|
loadMoreEmployee();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
const currentMonthMaxDay = new Date(
|
const currentMonthMaxDay = new Date(
|
||||||
Number(tableFilterState.tahun),
|
Number(tableFilterState.tahun),
|
||||||
@@ -493,7 +502,7 @@ export function DailyChecklistReportsContent() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Semua ABK' />
|
<SelectValue placeholder='Semua ABK' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent onScroll={handleEmployeeScroll}>
|
||||||
<SelectItem value='ALL'>Semua ABK</SelectItem>
|
<SelectItem value='ALL'>Semua ABK</SelectItem>
|
||||||
{employeeOptions.map((employee) => (
|
{employeeOptions.map((employee) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -503,6 +512,11 @@ export function DailyChecklistReportsContent() {
|
|||||||
{employee.label}
|
{employee.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{isLoadingMoreEmployee && (
|
||||||
|
<div className='flex justify-center p-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
BaseApiResponse,
|
BaseApiResponse,
|
||||||
ErrorApiResponse,
|
ErrorApiResponse,
|
||||||
@@ -15,3 +16,40 @@ export const isResponseError = <T>(
|
|||||||
): res is ErrorApiResponse => {
|
): res is ErrorApiResponse => {
|
||||||
return res?.status === 'error';
|
return res?.status === 'error';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getErrorMessage = async (
|
||||||
|
error: unknown,
|
||||||
|
fallbackMessage: string
|
||||||
|
) => {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const responseData = error.response?.data;
|
||||||
|
|
||||||
|
if (responseData instanceof Blob) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(await responseData.text()) as {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
return parsed.message || fallbackMessage;
|
||||||
|
} catch {
|
||||||
|
return fallbackMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
responseData &&
|
||||||
|
typeof responseData === 'object' &&
|
||||||
|
'message' in responseData &&
|
||||||
|
typeof responseData.message === 'string'
|
||||||
|
) {
|
||||||
|
return responseData.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.message || fallbackMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackMessage;
|
||||||
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user