mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 192a2be8b9 |
@@ -48,6 +48,3 @@ next-env.d.ts
|
|||||||
|
|
||||||
# rtk
|
# rtk
|
||||||
rtk.exe
|
rtk.exe
|
||||||
|
|
||||||
# local specs
|
|
||||||
/local-specs
|
|
||||||
+2
-21
@@ -30,10 +30,6 @@ 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
|
||||||
- |
|
- |
|
||||||
@@ -45,11 +41,7 @@ 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:
|
||||||
@@ -150,10 +142,6 @@ 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
|
||||||
@@ -182,9 +170,6 @@ 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
|
||||||
@@ -200,7 +185,7 @@ deploy:staging:
|
|||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# ====== (Branch production) ======
|
# ====== STAGING (Branch production) ======
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
build:production:
|
build:production:
|
||||||
<<: *build_template
|
<<: *build_template
|
||||||
@@ -213,10 +198,6 @@ 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
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
git add .
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Project-local RTK filters — commit this file with your repo.
|
|
||||||
# Filters here override user-global and built-in filters.
|
|
||||||
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
|
||||||
schema_version = 1
|
|
||||||
|
|
||||||
# Example: suppress build noise from a custom tool
|
|
||||||
# [filters.my-tool]
|
|
||||||
# description = "Compact my-tool output"
|
|
||||||
# match_command = "^my-tool\\s+build"
|
|
||||||
# strip_ansi = true
|
|
||||||
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
|
|
||||||
# max_lines = 30
|
|
||||||
# on_empty = "my-tool: ok"
|
|
||||||
@@ -1,486 +0,0 @@
|
|||||||
# 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(
|
||||||
['expense-detail', expenseId],
|
expenseId,
|
||||||
([_, id]) => ExpenseApi.getSingle(Number(id))
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!expenseId) {
|
if (!expenseId) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { SystemConfigContent } from '@/figma-make/components/pages/master-data/system-config/SystemConfigContent';
|
|
||||||
|
|
||||||
const SystemConfigPage = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full'>
|
|
||||||
<SystemConfigContent />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemConfigPage;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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'>
|
||||||
Total Item: {totalItems} | Page {currentPage} of {totalPages}
|
Page {currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ const Table = <TData extends object>({
|
|||||||
const tableOptions: TableOptions<TData> = {
|
const tableOptions: TableOptions<TData> = {
|
||||||
columns,
|
columns,
|
||||||
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion
|
||||||
defaultColumn: { sortDescFirst: false },
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
|||||||
+11
-14
@@ -6,7 +6,6 @@ export interface TabItem {
|
|||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
content?: ReactNode;
|
content?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hide?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabsProps
|
export interface TabsProps
|
||||||
@@ -123,19 +122,17 @@ const Tabs = ({
|
|||||||
>
|
>
|
||||||
<div className={getSideContentClasses()}>
|
<div className={getSideContentClasses()}>
|
||||||
<div role='tablist' className={getTabsClasses()}>
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
{tabs.map(({ id, label, disabled, hide }) =>
|
{tabs.map(({ id, label, disabled }) => (
|
||||||
hide ? null : (
|
<button
|
||||||
<button
|
key={id}
|
||||||
key={id}
|
role='tab'
|
||||||
role='tab'
|
className={getTabClasses(id === activeTabId, disabled)}
|
||||||
className={getTabClasses(id === activeTabId, disabled)}
|
onClick={() => !disabled && handleTabChange(id)}
|
||||||
onClick={() => !disabled && handleTabChange(id)}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
>
|
||||||
>
|
{label}
|
||||||
{label}
|
</button>
|
||||||
</button>
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{sideContent && sideContent}
|
{sideContent && sideContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ const useSelect = <T,>(
|
|||||||
|
|
||||||
const qs = new URLSearchParams({
|
const qs = new URLSearchParams({
|
||||||
...(params ?? {}),
|
...(params ?? {}),
|
||||||
[searchKey ? searchKey : 'search']: inputValue ?? '',
|
[searchKey]: inputValue ?? '',
|
||||||
[pageKey]: String(pageIndex + 1),
|
[pageKey]: String(pageIndex + 1),
|
||||||
[limitKey]: String(limit),
|
[limitKey]: String(limit),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
secondaryButton={
|
secondaryButton={
|
||||||
secondaryButton
|
secondaryButton
|
||||||
? {
|
? {
|
||||||
...secondaryButton,
|
|
||||||
text: secondaryButton?.text ?? 'Tidak',
|
text: secondaryButton?.text ?? 'Tidak',
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
if (secondaryButton && secondaryButton?.onClick) {
|
if (secondaryButton && secondaryButton?.onClick) {
|
||||||
|
|||||||
@@ -173,8 +173,8 @@ const DashboardProduction = () => {
|
|||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect<ProjectFlockKandang>(
|
} = useSelect<ProjectFlockKandang>(
|
||||||
ProjectFlockKandangApi.basePath,
|
ProjectFlockKandangApi.basePath,
|
||||||
'kandang_id',
|
'id',
|
||||||
'kandang.name',
|
'name_with_period',
|
||||||
'search',
|
'search',
|
||||||
{
|
{
|
||||||
location_id:
|
location_id:
|
||||||
@@ -362,7 +362,7 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
||||||
{dashboardProductionData && (
|
{exporting && dashboardProductionData && (
|
||||||
<>
|
<>
|
||||||
{/* Export Stats Charts */}
|
{/* Export Stats Charts */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -10,14 +10,16 @@ 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 = [
|
||||||
@@ -48,8 +50,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,7 +2,6 @@
|
|||||||
|
|
||||||
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';
|
||||||
|
|
||||||
@@ -20,7 +19,6 @@ 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';
|
||||||
@@ -28,7 +26,7 @@ import {
|
|||||||
UploadRequestDocumentsFormSchema,
|
UploadRequestDocumentsFormSchema,
|
||||||
UploadRequestDocumentsFormValues,
|
UploadRequestDocumentsFormValues,
|
||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } 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';
|
||||||
@@ -48,11 +46,6 @@ 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({
|
||||||
@@ -102,24 +95,17 @@ 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>({
|
||||||
@@ -160,31 +146,7 @@ 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);
|
||||||
|
|
||||||
@@ -426,24 +388,6 @@ 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'>
|
||||||
@@ -589,19 +533,6 @@ 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>
|
||||||
@@ -617,15 +548,21 @@ 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={requestDocument.path}
|
href={documentUrl}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-blue-500 underline'
|
className='text-blue-500 underline'
|
||||||
>
|
>
|
||||||
{requestDocument.name}{' '}
|
{requestDocument.path}{' '}
|
||||||
<Icon
|
<Icon
|
||||||
icon='cuida:open-in-new-tab-outline'
|
icon='cuida:open-in-new-tab-outline'
|
||||||
width={12}
|
width={12}
|
||||||
@@ -821,21 +758,6 @@ 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,60 +3,26 @@ 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: { value: number; label: string } | null;
|
location_id: string | null;
|
||||||
vendor: { value: number; label: string } | null;
|
vendor_id: 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.string().nullable(),
|
realization_date: yup
|
||||||
location: yup
|
.string()
|
||||||
.object({
|
.nullable()
|
||||||
value: yup.number().required(),
|
.test(
|
||||||
label: yup.string().required(),
|
'is-greater-or-equal-transaction',
|
||||||
})
|
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
|
||||||
.nullable(),
|
function (value) {
|
||||||
vendor: yup
|
const { transaction_date } = this.parent;
|
||||||
.object({
|
if (!transaction_date || !value) return true;
|
||||||
value: yup.number().required(),
|
return new Date(value) >= new Date(transaction_date);
|
||||||
label: yup.string().required(),
|
}
|
||||||
})
|
),
|
||||||
.nullable(),
|
location_id: yup.string().nullable(),
|
||||||
category: yup
|
vendor_id: yup.string().nullable(),
|
||||||
.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, useCallback, useEffect, useMemo, useState } from 'react';
|
import { RefObject } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -11,11 +11,8 @@ 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,
|
||||||
@@ -34,143 +31,64 @@ 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: null,
|
location_id: null,
|
||||||
vendor: null,
|
vendor_id: 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();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const locationValue = formik.values.location_id
|
||||||
setSelectedLocationId(
|
? locationOptions.find(
|
||||||
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
);
|
) || null
|
||||||
}, [initialValues?.location]);
|
: null;
|
||||||
|
|
||||||
const { resetForm } = formik;
|
const vendorValue = formik.values.vendor_id
|
||||||
|
? vendorOptions.find(
|
||||||
const formikResetHandler = useCallback(() => {
|
(opt) => String(opt.value) === formik.values.vendor_id
|
||||||
resetForm({
|
) || null
|
||||||
values: {
|
: null;
|
||||||
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 value = val as OptionType | null;
|
const locationId =
|
||||||
formik.setFieldValue('location', value);
|
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
||||||
formik.setFieldValue('project_flock', null);
|
formik.setFieldValue('location_id', locationId);
|
||||||
formik.setFieldValue('project_flock_kandang', null);
|
|
||||||
setSelectedLocationId(value?.value ? String(value.value) : '');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('vendor', val as OptionType | null);
|
const vendorId =
|
||||||
|
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}
|
||||||
@@ -180,7 +98,7 @@ const ExpensesFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formikResetHandler}
|
onReset={formik.handleReset}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -203,41 +121,49 @@ 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'>
|
||||||
<DateInput
|
<div className='flex flex-col'>
|
||||||
name='transaction_date'
|
<span className='py-2 text-xs font-semibold'>Tanggal</span>
|
||||||
label='Tanggal Transaksi'
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
placeholder='Tanggal Transaksi'
|
<DateInput
|
||||||
value={formik.values.transaction_date || ''}
|
name='transaction_date'
|
||||||
onChange={formik.handleChange}
|
placeholder='Tanggal Transaksi'
|
||||||
onBlur={formik.handleBlur}
|
value={formik.values.transaction_date || ''}
|
||||||
isError={
|
onChange={formik.handleChange}
|
||||||
formik.touched.transaction_date &&
|
onBlur={formik.handleBlur}
|
||||||
!!formik.errors.transaction_date
|
isError={
|
||||||
}
|
formik.touched.transaction_date &&
|
||||||
/>
|
!!formik.errors.transaction_date
|
||||||
|
}
|
||||||
<DateInput
|
/>
|
||||||
name='realization_date'
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
label='Tanggal Realisasi'
|
<DateInput
|
||||||
placeholder='Tanggal Realisasi'
|
name='realization_date'
|
||||||
value={formik.values.realization_date || ''}
|
placeholder='Tanggal Realisasi'
|
||||||
onChange={formik.handleChange}
|
value={formik.values.realization_date || ''}
|
||||||
onBlur={formik.handleBlur}
|
onChange={formik.handleChange}
|
||||||
isError={
|
onBlur={formik.handleBlur}
|
||||||
formik.touched.realization_date &&
|
isError={
|
||||||
!!formik.errors.realization_date
|
formik.touched.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={formik.values.location}
|
value={locationValue}
|
||||||
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' }}
|
||||||
@@ -247,87 +173,14 @@ const ExpensesFilterModal = ({
|
|||||||
label='Vendor'
|
label='Vendor'
|
||||||
placeholder='Pilih Vendor'
|
placeholder='Pilih Vendor'
|
||||||
options={vendorOptions}
|
options={vendorOptions}
|
||||||
value={formik.values.vendor}
|
value={vendorValue}
|
||||||
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,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
import {
|
useCallback,
|
||||||
CellContext,
|
useEffect,
|
||||||
ColumnDef,
|
useMemo,
|
||||||
SortingState,
|
useRef,
|
||||||
Updater,
|
useState,
|
||||||
} from '@tanstack/react-table';
|
} from 'react';
|
||||||
|
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';
|
||||||
@@ -29,7 +30,7 @@ import {
|
|||||||
FINANCE_TRANSACTION_TYPE_OPTIONS,
|
FINANCE_TRANSACTION_TYPE_OPTIONS,
|
||||||
} from '@/config/constant';
|
} from '@/config/constant';
|
||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
|
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
@@ -38,8 +39,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 ButtonFilter from '@/components/helper/ButtonFilter';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
|
||||||
import {
|
import {
|
||||||
FinanceTableFilterSchema,
|
FinanceTableFilterSchema,
|
||||||
FinanceTableFilterValues,
|
FinanceTableFilterValues,
|
||||||
@@ -176,6 +176,9 @@ 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,
|
||||||
@@ -184,18 +187,14 @@ const FinanceTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
transactionTypes: '',
|
transactionTypes: '',
|
||||||
bankIds: '',
|
bankIds: '',
|
||||||
customerIds: '',
|
customerIds: '',
|
||||||
supplierIds: '',
|
supplierIds: '',
|
||||||
sort_by: '',
|
sortBy: '',
|
||||||
orderBy: '',
|
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
bankNames: '',
|
|
||||||
customerNames: '',
|
|
||||||
supplierNames: '',
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -204,14 +203,10 @@ const FinanceTable = () => {
|
|||||||
bankIds: 'bank_ids',
|
bankIds: 'bank_ids',
|
||||||
customerIds: 'customer_ids',
|
customerIds: 'customer_ids',
|
||||||
supplierIds: 'supplier_ids',
|
supplierIds: 'supplier_ids',
|
||||||
sort_by: 'sort_by',
|
sortBy: 'sort_date',
|
||||||
orderBy: 'sort_order',
|
|
||||||
startDate: 'start_date',
|
startDate: 'start_date',
|
||||||
endDate: 'end_date',
|
endDate: 'end_date',
|
||||||
},
|
},
|
||||||
excludeKeysFromUrl: ['bankNames', 'customerNames', 'supplierNames'],
|
|
||||||
persist: true,
|
|
||||||
storeName: 'finance-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -234,14 +229,13 @@ const FinanceTable = () => {
|
|||||||
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
|
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
|
||||||
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isExportLoading, setIsExportLoading] = useState(false);
|
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
// ===== Formik for Filter =====
|
// ===== Formik for Filter =====
|
||||||
const filterFormik = useFormik<FinanceTableFilterValues>({
|
const filterFormik = useFormik<FinanceTableFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
search: tableFilterState.search || '',
|
search: searchValue,
|
||||||
transaction_types: '',
|
transaction_types: '',
|
||||||
bank_ids: '',
|
bank_ids: '',
|
||||||
customer_ids: '',
|
customer_ids: '',
|
||||||
@@ -251,48 +245,29 @@ const FinanceTable = () => {
|
|||||||
end_date: '',
|
end_date: '',
|
||||||
},
|
},
|
||||||
validationSchema: FinanceTableFilterSchema,
|
validationSchema: FinanceTableFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
enableReinitialize: true,
|
||||||
updateFilter('search', values.search, true);
|
onSubmit: (values) => {
|
||||||
updateFilter('transactionTypes', values.transaction_types, true);
|
updateFilter('search', values.search);
|
||||||
updateFilter('bankIds', values.bank_ids, true);
|
setSearchValue(values.search);
|
||||||
updateFilter('customerIds', values.customer_ids, true);
|
updateFilter('transactionTypes', values.transaction_types);
|
||||||
updateFilter('supplierIds', values.supplier_ids, true);
|
updateFilter('bankIds', values.bank_ids);
|
||||||
updateFilter('sort_by', values.sort_by, true);
|
updateFilter('customerIds', values.customer_ids);
|
||||||
updateFilter('startDate', values.start_date, true);
|
updateFilter('supplierIds', values.supplier_ids);
|
||||||
updateFilter('endDate', values.end_date, true);
|
updateFilter('sortBy', values.sort_by);
|
||||||
// Save display names for restoration on modal reopen
|
updateFilter('startDate', values.start_date);
|
||||||
const toNames = (val: OptionType | OptionType[] | null) =>
|
updateFilter('endDate', values.end_date);
|
||||||
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: () => {
|
||||||
setSelectedTransactionType(null);
|
updateFilter('search', '');
|
||||||
setSelectedBank(null);
|
resetSearchValue();
|
||||||
setSelectedCustomerId(null);
|
updateFilter('transactionTypes', '');
|
||||||
setSelectedSupplierId(null);
|
updateFilter('bankIds', '');
|
||||||
setSelectedSortBy(null);
|
updateFilter('customerIds', '');
|
||||||
updateFilter('search', '', true);
|
updateFilter('supplierIds', '');
|
||||||
updateFilter('transactionTypes', '', true);
|
updateFilter('sortBy', '');
|
||||||
updateFilter('bankIds', '', true);
|
updateFilter('startDate', '');
|
||||||
updateFilter('customerIds', '', true);
|
updateFilter('endDate', '');
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -345,10 +320,40 @@ 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 = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const searchChangeHandler = useCallback(
|
||||||
updateFilter('search', e.target.value, true);
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
};
|
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
|
||||||
@@ -404,26 +409,6 @@ const FinanceTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sorting: SortingState = tableFilterState.sort_by
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: tableFilterState.sort_by,
|
|
||||||
desc: tableFilterState.orderBy === 'desc',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
||||||
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
||||||
if (next.length > 0) {
|
|
||||||
updateFilter('sort_by', next[0].id, true);
|
|
||||||
updateFilter('orderBy', next[0].desc ? 'desc' : 'asc', true);
|
|
||||||
} else {
|
|
||||||
updateFilter('sort_by', '', true);
|
|
||||||
updateFilter('orderBy', '', true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const endDate = filterFormik.values.end_date;
|
const endDate = filterFormik.values.end_date;
|
||||||
@@ -484,88 +469,28 @@ 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 exportToExcel = async () => {
|
const resetFilterHandler = () => {
|
||||||
setIsExportLoading(true);
|
setSelectedTransactionType(null);
|
||||||
try {
|
setSelectedBank(null);
|
||||||
await FinanceApi.exportToExcel(getTableFilterQueryString());
|
setSelectedCustomerId(null);
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
setSelectedSupplierId(null);
|
||||||
} catch (error) {
|
setSelectedSortBy(null);
|
||||||
toast.error(
|
|
||||||
await getErrorMessage(error, 'Gagal mengekspor data finance.')
|
filterFormik.resetForm();
|
||||||
);
|
|
||||||
} finally {
|
updateFilter('search', '');
|
||||||
setIsExportLoading(false);
|
resetSearchValue();
|
||||||
}
|
updateFilter('transactionTypes', '');
|
||||||
|
updateFilter('bankIds', '');
|
||||||
|
updateFilter('customerIds', '');
|
||||||
|
updateFilter('supplierIds', '');
|
||||||
|
updateFilter('sortBy', '');
|
||||||
|
updateFilter('startDate', '');
|
||||||
|
updateFilter('endDate', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -584,12 +509,10 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'ID',
|
header: 'ID',
|
||||||
accessorKey: 'payment_code',
|
accessorKey: 'payment_code',
|
||||||
enableSorting: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'References Number',
|
header: 'References Number',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
enableSorting: true,
|
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.reference_number;
|
const value = props.row.original.reference_number;
|
||||||
return <span>{value ?? '-'}</span>;
|
return <span>{value ?? '-'}</span>;
|
||||||
@@ -598,7 +521,6 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Jenis Transaksi',
|
header: 'Jenis Transaksi',
|
||||||
accessorKey: 'transaction_type',
|
accessorKey: 'transaction_type',
|
||||||
enableSorting: true,
|
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
const value = props.row.original.transaction_type
|
const value = props.row.original.transaction_type
|
||||||
.split('_')
|
.split('_')
|
||||||
@@ -608,8 +530,7 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorKey: 'customer_name',
|
accessorFn: (finance: Finance) => finance.party?.name,
|
||||||
enableSorting: true,
|
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
if (props.row.original.party?.id) {
|
if (props.row.original.party?.id) {
|
||||||
return <span>{props.row.original.party?.name}</span>;
|
return <span>{props.row.original.party?.name}</span>;
|
||||||
@@ -618,23 +539,13 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Pembayaran',
|
header: 'Tanggal',
|
||||||
accessorKey: 'payment_date',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
||||||
cell: (props) =>
|
|
||||||
formatDate(props.row.original.payment_date, 'DD MMM YYYY'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Tanggal Dibuat',
|
|
||||||
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>;
|
||||||
@@ -642,26 +553,20 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorKey: 'bank',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
finance.bank
|
||||||
cell: (props) =>
|
? `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`
|
||||||
props.row.original.bank
|
|
||||||
? `${props.row.original.bank?.alias} - ${props.row.original.bank?.account_number} - ${props.row.original.bank?.owner}`
|
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pengeluaran (Rp)',
|
header: 'Pengeluaran (Rp)',
|
||||||
accessorKey: 'expense_amount',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
formatCurrency(Math.abs(finance.expense_amount)),
|
||||||
cell: (props) =>
|
|
||||||
formatCurrency(Math.abs(props.row.original.expense_amount)),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
accessorKey: 'income_amount',
|
accessorFn: (finance: Finance) =>
|
||||||
enableSorting: true,
|
formatCurrency(Math.abs(finance.income_amount)),
|
||||||
cell: (props) =>
|
|
||||||
formatCurrency(Math.abs(props.row.original.income_amount)),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
@@ -700,6 +605,27 @@ 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'>
|
||||||
@@ -761,65 +687,25 @@ const FinanceTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ButtonFilter
|
<Button
|
||||||
values={tableFilterState}
|
variant='outline'
|
||||||
excludeFields={[
|
color='none'
|
||||||
'page',
|
|
||||||
'pageSize',
|
|
||||||
'search',
|
|
||||||
'orderBy',
|
|
||||||
'bankNames',
|
|
||||||
'customerNames',
|
|
||||||
'supplierNames',
|
|
||||||
]}
|
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className={cn(
|
||||||
/>
|
'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',
|
||||||
|
{
|
||||||
<Dropdown
|
'border-primary-gradient text-primary': hasFilters,
|
||||||
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>Ekspor</span>
|
|
||||||
|
|
||||||
<div className='w-px self-stretch bg-base-content/10' />
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:chevron-down'
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
variant='ghost'
|
Filter
|
||||||
color='none'
|
{hasFilters && (
|
||||||
onClick={exportToExcel}
|
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
||||||
isLoading={isExportLoading}
|
{activeFiltersCount}
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
</span>
|
||||||
>
|
)}
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
</Button>
|
||||||
Ekspor ke Excel
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -855,9 +741,6 @@ const FinanceTable = () => {
|
|||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
|
||||||
setSorting={handleSortingChange}
|
|
||||||
manualSorting
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
headerColumnClassName: 'text-nowrap',
|
||||||
@@ -991,9 +874,19 @@ 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='reset'
|
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={() => {
|
||||||
|
filterFormik.resetForm();
|
||||||
|
setSelectedTransactionType(null);
|
||||||
|
setSelectedBank(null);
|
||||||
|
setSelectedCustomerId(null);
|
||||||
|
setSelectedSupplierId(null);
|
||||||
|
setSelectedSortBy(null);
|
||||||
|
resetFilterHandler();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import useSWR from 'swr';
|
import { usePathname } from 'next/navigation';
|
||||||
|
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';
|
||||||
@@ -24,6 +25,7 @@ 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';
|
||||||
@@ -98,31 +100,25 @@ 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: undefined,
|
productFilter: '',
|
||||||
warehouseFilter: undefined,
|
warehouseFilter: '',
|
||||||
transactionTypeFilter: undefined,
|
transactionTypeFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -135,8 +131,6 @@ 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 =====
|
||||||
@@ -145,27 +139,22 @@ const InventoryAdjustmentTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<AdjustmentFilterType>({
|
const formik = useFormik<AdjustmentFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product: tableFilterState.productFilter,
|
product_id: null,
|
||||||
warehouse: tableFilterState.warehouseFilter,
|
warehouse: null,
|
||||||
transaction_type: tableFilterState.transactionTypeFilter,
|
transaction_type: null,
|
||||||
},
|
},
|
||||||
validationSchema: AdjustmentFilterSchema,
|
validationSchema: AdjustmentFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product || undefined, true);
|
updateFilter('productFilter', values.product_id || '');
|
||||||
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
updateFilter('warehouseFilter', String(values.warehouse?.value) || '');
|
||||||
updateFilter(
|
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
||||||
'transactionTypeFilter',
|
|
||||||
values.transaction_type || undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', undefined, true);
|
updateFilter('productFilter', '');
|
||||||
updateFilter('warehouseFilter', undefined, true);
|
updateFilter('warehouseFilter', '');
|
||||||
updateFilter('transactionTypeFilter', undefined, true);
|
updateFilter('transactionTypeFilter', '');
|
||||||
filterModal.closeModal();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,9 +193,14 @@ const InventoryAdjustmentTable = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterProductChange = useCallback(
|
||||||
formik.setFieldValue('product', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
};
|
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
|
||||||
@@ -214,20 +208,38 @@ const InventoryAdjustmentTable = () => {
|
|||||||
formik.setFieldValue('warehouse', val);
|
formik.setFieldValue('warehouse', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterTransactionTypeChange = (
|
const handleFilterTransactionTypeChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const type = val as OptionType | null;
|
||||||
formik.setFieldValue('transaction_type', val);
|
const typeValue = type?.value ? String(type.value) : null;
|
||||||
};
|
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 {
|
||||||
@@ -264,8 +276,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
||||||
@@ -486,8 +507,6 @@ 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'
|
||||||
@@ -577,7 +596,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={formik.values.product}
|
value={productIdValue}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -601,7 +620,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Tipe Transaksi'
|
label='Tipe Transaksi'
|
||||||
placeholder='Pilih Tipe Transaksi'
|
placeholder='Pilih Tipe Transaksi'
|
||||||
options={transactionTypeOptions}
|
options={transactionTypeOptions}
|
||||||
value={formik.values.transaction_type}
|
value={transactionTypeValue}
|
||||||
onChange={handleFilterTransactionTypeChange}
|
onChange={handleFilterTransactionTypeChange}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -611,9 +630,13 @@ 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='reset'
|
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={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
|
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 = Yup.object().shape({
|
export const AdjustmentFilterSchema = object().shape({
|
||||||
product: Yup.object({
|
product_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
warehouse_id: string().nullable(),
|
||||||
label: Yup.string().nullable(),
|
transaction_type: 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?: OptionType<string>;
|
product_id: string | null;
|
||||||
warehouse?: OptionType<string>;
|
transaction_type: string | null;
|
||||||
transaction_type?: OptionType<string>;
|
warehouse: OptionType<number> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
import useSWR from 'swr';
|
ChangeEventHandler,
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -13,6 +20,7 @@ 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';
|
||||||
@@ -100,21 +108,20 @@ 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: undefined,
|
productFilter: '',
|
||||||
warehouseFilter: undefined,
|
warehouseFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -122,8 +129,6 @@ 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 =====
|
||||||
@@ -132,20 +137,19 @@ const MovementTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<MovementFilterType>({
|
const formik = useFormik<MovementFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product: tableFilterState.productFilter,
|
product_id: null,
|
||||||
warehouse: tableFilterState.warehouseFilter,
|
warehouse_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: MovementFilterSchema,
|
validationSchema: MovementFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product || undefined, true);
|
updateFilter('productFilter', values.product_id || '');
|
||||||
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
updateFilter('warehouseFilter', values.warehouse_id || '');
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', undefined, true);
|
updateFilter('productFilter', '');
|
||||||
updateFilter('warehouseFilter', undefined, true);
|
updateFilter('warehouseFilter', '');
|
||||||
filterModal.closeModal();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,23 +180,47 @@ const MovementTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterProductChange = useCallback(
|
||||||
formik.setFieldValue('product', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
};
|
const product = val as OptionType | null;
|
||||||
|
const productId = product?.value ? String(product.value) : null;
|
||||||
|
formik.setFieldValue('product_id', productId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
const handleFilterWarehouseChange = (
|
const handleFilterWarehouseChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const warehouse = val as OptionType | null;
|
||||||
formik.setFieldValue('warehouse', val);
|
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
||||||
};
|
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>([]);
|
||||||
@@ -227,8 +255,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
||||||
@@ -427,7 +464,7 @@ const MovementTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={formik.values.product}
|
value={productIdValue}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -439,7 +476,7 @@ const MovementTable = () => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih Gudang'
|
placeholder='Pilih Gudang'
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
value={formik.values.warehouse}
|
value={warehouseIdValue}
|
||||||
onChange={handleFilterWarehouseChange}
|
onChange={handleFilterWarehouseChange}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
isLoading={isLoadingWarehouseOptions}
|
isLoading={isLoadingWarehouseOptions}
|
||||||
@@ -452,9 +489,13 @@ 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='reset'
|
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={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const MovementFilterSchema = Yup.object().shape({
|
export const MovementFilterSchema = object().shape({
|
||||||
product: Yup.object({
|
product_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
warehouse_id: 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?: OptionType<string>;
|
product_id: string | null;
|
||||||
warehouse?: OptionType<string>;
|
warehouse_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const [productQtyErrorShown, setProductQtyErrorShown] = useState(false);
|
const [productQtyErrorShown, setProductQtyErrorShown] = useState(false);
|
||||||
const [deliveryQtyErrorShown, setDeliveryQtyErrorShown] = useState(false);
|
const [deliveryQtyErrorShown, setDeliveryQtyErrorShown] = useState(false);
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
const productStockCacheRef = useRef<
|
|
||||||
Map<number, { quantity: number; transfer_available_qty?: number }>
|
|
||||||
>(new Map());
|
|
||||||
|
|
||||||
// ===== FORM HANDLERS =====
|
// ===== FORM HANDLERS =====
|
||||||
const createMovementHandler = useCallback(
|
const createMovementHandler = useCallback(
|
||||||
@@ -340,7 +337,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||||
prevSourceWarehouseId !== null
|
prevSourceWarehouseId !== null
|
||||||
) {
|
) {
|
||||||
productStockCacheRef.current = new Map();
|
|
||||||
formik.setFieldValue('products', [
|
formik.setFieldValue('products', [
|
||||||
{
|
{
|
||||||
product: null,
|
product: null,
|
||||||
@@ -403,15 +399,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
: [];
|
: [];
|
||||||
}, [productWarehouses]);
|
}, [productWarehouses]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
productWarehouseOptions.forEach((pw) => {
|
|
||||||
productStockCacheRef.current.set(pw.product_id, {
|
|
||||||
quantity: pw.quantity,
|
|
||||||
transfer_available_qty: pw.transfer_available_qty,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [productWarehouseOptions]);
|
|
||||||
|
|
||||||
// ===== HELPER FUNCTIONS =====
|
// ===== HELPER FUNCTIONS =====
|
||||||
const isRepeaterInputError = <T extends 'products' | 'deliveries'>(
|
const isRepeaterInputError = <T extends 'products' | 'deliveries'>(
|
||||||
arrayName: T,
|
arrayName: T,
|
||||||
@@ -853,12 +840,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const getAvailableStock = useCallback(
|
const getAvailableStock = useCallback(
|
||||||
(productId: number) => {
|
(productId: number) => {
|
||||||
if (type === 'detail') return 0;
|
if (type === 'detail') return 0;
|
||||||
const live = productWarehouseOptions.find(
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
(pw) => pw.product_id === productId
|
(pw) => pw.product_id === productId
|
||||||
);
|
);
|
||||||
if (live) return live.transfer_available_qty ?? live.quantity ?? 0;
|
|
||||||
const cached = productStockCacheRef.current.get(productId);
|
return (
|
||||||
return cached?.transfer_available_qty ?? cached?.quantity ?? 0;
|
productWarehouse?.transfer_available_qty ??
|
||||||
|
productWarehouse?.quantity ??
|
||||||
|
0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[productWarehouseOptions, type]
|
[productWarehouseOptions, type]
|
||||||
);
|
);
|
||||||
@@ -866,25 +856,20 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const getTotalStock = useCallback(
|
const getTotalStock = useCallback(
|
||||||
(productId: number) => {
|
(productId: number) => {
|
||||||
if (type === 'detail') return 0;
|
if (type === 'detail') return 0;
|
||||||
const live = productWarehouseOptions.find(
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
(pw) => pw.product_id === productId
|
(pw) => pw.product_id === productId
|
||||||
);
|
);
|
||||||
if (live) return live.quantity ?? 0;
|
return productWarehouse?.quantity ?? 0;
|
||||||
return productStockCacheRef.current.get(productId)?.quantity ?? 0;
|
|
||||||
},
|
},
|
||||||
[productWarehouseOptions, type]
|
[productWarehouseOptions, type]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasAvailableQty = useCallback(
|
const hasAvailableQty = useCallback(
|
||||||
(productId: number) => {
|
(productId: number) => {
|
||||||
const live = productWarehouseOptions.find(
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
(pw) => pw.product_id === productId
|
(pw) => pw.product_id === productId
|
||||||
);
|
);
|
||||||
if (live) return live.transfer_available_qty !== undefined;
|
return productWarehouse?.transfer_available_qty !== undefined;
|
||||||
return (
|
|
||||||
productStockCacheRef.current.get(productId)?.transfer_available_qty !==
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[productWarehouseOptions]
|
[productWarehouseOptions]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,23 +4,17 @@ 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, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, 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';
|
||||||
@@ -77,79 +71,25 @@ 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(
|
||||||
@@ -157,8 +97,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
||||||
@@ -233,163 +182,96 @@ const InventoryProductTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='w-full'>
|
||||||
<div className='w-full'>
|
{/* Header Section */}
|
||||||
{/* Header Section */}
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
{/* Action Buttons */}
|
||||||
{/* Action Buttons */}
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
<RequirePermission permissions='lti.inventory.product_stock.create'>
|
||||||
<RequirePermission permissions='lti.inventory.product_stock.create'>
|
<Button
|
||||||
<Button
|
href='/inventory/product/add'
|
||||||
href='/inventory/product/add'
|
color='primary'
|
||||||
color='primary'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
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} />
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
Add Product
|
||||||
Add Product
|
</Button>
|
||||||
</Button>
|
</RequirePermission>
|
||||||
</RequirePermission>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search and Filter */}
|
{/* Search */}
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='Search'
|
placeholder='Search'
|
||||||
value={tableFilterState.search ?? ''}
|
value={tableFilterState.search ?? ''}
|
||||||
onChange={searchChangeHandler}
|
onChange={searchChangeHandler}
|
||||||
startAdornment={
|
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>
|
||||||
|
) : !isResponseSuccess(inventoryProducts) ||
|
||||||
|
inventoryProducts.data?.length === 0 ? (
|
||||||
|
<div className='p-3'>
|
||||||
|
<InventoryProductTableSkeleton
|
||||||
|
columns={columns}
|
||||||
|
icon={
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:magnifying-glass'
|
icon='heroicons:document-text'
|
||||||
|
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>
|
||||||
{/* Table Section */}
|
data={
|
||||||
<div className='flex flex-col mb-4'>
|
isResponseSuccess(inventoryProducts)
|
||||||
{isLoading ? (
|
? inventoryProducts?.data
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
: []
|
||||||
<span className='loading loading-spinner loading-xl' />
|
}
|
||||||
</div>
|
columns={columns}
|
||||||
) : !isResponseSuccess(inventoryProducts) ||
|
pageSize={tableFilterState.pageSize}
|
||||||
inventoryProducts.data?.length === 0 ? (
|
page={
|
||||||
<div className='p-3'>
|
isResponseSuccess(inventoryProducts)
|
||||||
<InventoryProductTableSkeleton
|
? inventoryProducts?.meta?.page
|
||||||
columns={columns}
|
: 0
|
||||||
icon={
|
}
|
||||||
<Icon
|
totalItems={
|
||||||
icon='heroicons:document-text'
|
isResponseSuccess(inventoryProducts)
|
||||||
className='text-white'
|
? inventoryProducts?.meta?.total_results
|
||||||
width={20}
|
: 0
|
||||||
height={20}
|
}
|
||||||
/>
|
onPageChange={setPage}
|
||||||
}
|
onPageSizeChange={setPageSize}
|
||||||
/>
|
isLoading={isLoading}
|
||||||
</div>
|
sorting={sorting}
|
||||||
) : (
|
setSorting={setSorting}
|
||||||
<Table<InventoryProduct>
|
className={{
|
||||||
data={
|
containerClassName: cn('p-3 mb-0'),
|
||||||
isResponseSuccess(inventoryProducts)
|
headerColumnClassName: 'text-nowrap',
|
||||||
? 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,16 +1,8 @@
|
|||||||
'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';
|
||||||
|
|
||||||
@@ -19,34 +11,17 @@ const InventoryProductDetail = ({
|
|||||||
}: {
|
}: {
|
||||||
inventoryProduct?: InventoryProduct;
|
inventoryProduct?: InventoryProduct;
|
||||||
}) => {
|
}) => {
|
||||||
const filterModal = useModal();
|
const stockLogs = useMemo(() => {
|
||||||
|
return (
|
||||||
const { state: filterState, updateFilter } = useTableFilter<{
|
inventoryProduct?.product_warehouses?.flatMap((warehouse) =>
|
||||||
warehouse_ids: OptionType<number>[];
|
warehouse.stock_logs.map((log) => ({
|
||||||
}>({
|
...log,
|
||||||
initial: {
|
warehouse_name: warehouse.warehouse_name,
|
||||||
warehouse_ids: [],
|
warehouse_id: warehouse.warehouse_id,
|
||||||
},
|
}))
|
||||||
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'>
|
||||||
@@ -139,29 +114,7 @@ const InventoryProductDetail = ({
|
|||||||
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
<StockLogTable stockLogs={stockLogs} />
|
||||||
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
'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,183 +1,95 @@
|
|||||||
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 { StockLogApi } from '@/services/api/inventory';
|
import { StockLog } from '@/types/api/inventory/product';
|
||||||
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 = ({
|
||||||
productWarehouse,
|
stockLogs,
|
||||||
}: {
|
}: {
|
||||||
productWarehouse: ProductWarehouseStock;
|
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
|
||||||
}) => {
|
}) => {
|
||||||
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 (
|
||||||
<div ref={containerRef}>
|
<Card
|
||||||
<Card
|
title='Informasi Stock Produk'
|
||||||
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
collapsible
|
||||||
collapsible
|
variant='bordered'
|
||||||
variant='bordered'
|
className={{
|
||||||
|
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={{
|
||||||
wrapper: 'w-full',
|
containerClassName: 'mt-6',
|
||||||
|
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',
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<div className='flex justify-end px-6 pt-4'>
|
</Card>
|
||||||
<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,42 +1,13 @@
|
|||||||
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'
|
||||||
@@ -48,14 +19,32 @@ const StockProductWarehouseTable = ({
|
|||||||
>
|
>
|
||||||
<Table<ProductWarehouseStock>
|
<Table<ProductWarehouseStock>
|
||||||
data={productWarehouseStock ?? []}
|
data={productWarehouseStock ?? []}
|
||||||
columns={stockProductWarehouseTableColumns}
|
columns={[
|
||||||
pageSize={tableFilterState.pageSize}
|
{
|
||||||
page={tableFilterState.page ?? 0}
|
header: 'Nama Gudang',
|
||||||
totalItems={productWarehouseStock?.length ?? 0}
|
accessorKey: 'warehouse_name',
|
||||||
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 mb-0',
|
containerClassName: 'mt-6',
|
||||||
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,11 +849,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||||
disabled={deliveryRejected}
|
disabled={deliveryRejected}
|
||||||
>
|
>
|
||||||
{marketing?.data?.latest_approval?.step_number === 1 &&
|
Approve
|
||||||
'Approve'}
|
|
||||||
|
|
||||||
{marketing?.data?.latest_approval?.step_number === 2 &&
|
|
||||||
'Deliver Item'}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useCallback, useMemo } from 'react';
|
import { RefObject, 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,31 +17,20 @@ 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, ProductApi } from '@/services/api/master-data';
|
import { CustomerApi } 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();
|
||||||
@@ -49,13 +38,36 @@ const MarketingFilterModal = ({
|
|||||||
|
|
||||||
// ===== OPTIONS =====
|
// ===== OPTIONS =====
|
||||||
const {
|
const {
|
||||||
options: productsOptions,
|
rawData: productsRawData,
|
||||||
isLoadingOptions: isLoadingProductsOptions,
|
isLoadingOptions: isLoadingProductsOptions,
|
||||||
setInputValue: setProductsInputValue,
|
setInputValue: setProductsInputValue,
|
||||||
loadMore: loadMoreProducts,
|
loadMore: loadMoreProducts,
|
||||||
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
} = useSelect<BaseMarketing>(
|
||||||
include_all: 'true',
|
MarketingApi.basePath,
|
||||||
});
|
'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,
|
||||||
@@ -66,19 +78,6 @@ 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(),
|
||||||
@@ -88,29 +87,18 @@ 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);
|
||||||
@@ -123,22 +111,6 @@ 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[]);
|
||||||
};
|
};
|
||||||
@@ -154,27 +126,6 @@ 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}
|
||||||
@@ -184,7 +135,7 @@ const MarketingFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formikResetHandler}
|
onReset={formik.handleReset}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -241,37 +192,6 @@ 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,39 +2,26 @@
|
|||||||
|
|
||||||
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 {
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
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 {
|
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
||||||
CellContext,
|
|
||||||
ColumnDef,
|
|
||||||
Row,
|
|
||||||
SortingState,
|
|
||||||
Updater,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import { 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';
|
||||||
@@ -167,21 +154,12 @@ 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 {
|
||||||
@@ -194,17 +172,8 @@ 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',
|
||||||
@@ -212,43 +181,9 @@ 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,
|
||||||
@@ -263,64 +198,26 @@ 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('product_names', values.product_names?.join(','));
|
updateFilter('status', values.status ? values.status.toString() : '');
|
||||||
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', '', true);
|
updateFilter('product_ids', '');
|
||||||
updateFilter('product_names', '', true);
|
updateFilter('status', '');
|
||||||
updateFilter('status', '', true);
|
updateFilter('customer_id', '');
|
||||||
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -329,13 +226,10 @@ const MarketingTable = () => {
|
|||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const productsClickHandler = useCallback(
|
const productsClickHandler = (item: Marketing) => {
|
||||||
(item: Marketing) => {
|
setSelectedItem(item);
|
||||||
setSelectedItem(item);
|
productsModal.openModal();
|
||||||
productsModal.openModal();
|
};
|
||||||
},
|
|
||||||
[productsModal]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMarketingHandler = async () => {
|
const deleteMarketingHandler = async () => {
|
||||||
const deleteMarketingRes = await MarketingApi.delete(
|
const deleteMarketingRes = await MarketingApi.delete(
|
||||||
@@ -357,226 +251,75 @@ 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 eligibleSelectedRows = selectedRowsData.filter((row) => {
|
const hasApprovable = selectedRowsData.some(
|
||||||
const approval = row.latest_approval;
|
(row) =>
|
||||||
|
row.latest_approval.step_number === 1 &&
|
||||||
if (approval.action === 'REJECTED') {
|
row.latest_approval.action !== 'REJECTED'
|
||||||
return false;
|
);
|
||||||
}
|
const hasRejectable = selectedRowsData.some(
|
||||||
|
(row) =>
|
||||||
if (selectedApprovalStep === null) {
|
row.latest_approval.step_number === 1 &&
|
||||||
return approval.step_number === 1 || approval.step_number === 2;
|
row.latest_approval.action !== 'REJECTED'
|
||||||
}
|
);
|
||||||
|
|
||||||
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 = eligibleSelectedRows.map((row) => row.id);
|
const idsToProcess =
|
||||||
const nextApprovalStatus =
|
approveAction === 'APPROVED'
|
||||||
selectedApprovalStep === 1
|
? selectedRowsData
|
||||||
? 'SALES_ORDER'
|
.filter((row) => row.latest_approval.step_number === 1)
|
||||||
: selectedApprovalStep === 2
|
.map((row) => row.id)
|
||||||
? 'DELIVERY_ORDER'
|
: selectedRowsData
|
||||||
: null;
|
.filter((row) => row.latest_approval.step_number === 2)
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (approveAction === 'APPROVED' && selectedApprovalStep !== 1) {
|
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
|
||||||
toast.error('Approve tahap ini harus menggunakan tanggal pengiriman.');
|
idsToProcess,
|
||||||
|
approveAction,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(approveMarketingRes)) {
|
||||||
confirmationModal.closeModal();
|
confirmationModal.closeModal();
|
||||||
return;
|
toast.success(approveMarketingRes?.message as string);
|
||||||
}
|
|
||||||
|
|
||||||
if (approveAction === 'APPROVED' && !nextApprovalStatus) {
|
|
||||||
toast.error('Status approval berikutnya tidak valid.');
|
|
||||||
confirmationModal.closeModal();
|
|
||||||
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) => {
|
||||||
setIsDeliveryLoading(true);
|
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
||||||
try {
|
deliveryModal.closeModal();
|
||||||
const res = await SalesOrderApi.delivery(
|
toast.success(res?.message as string);
|
||||||
selectedItem?.id as number,
|
refreshMarketing?.();
|
||||||
notes
|
router.push(
|
||||||
);
|
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
||||||
deliveryModal.closeModal();
|
);
|
||||||
toast.success(res?.message as string);
|
|
||||||
refreshMarketing?.();
|
|
||||||
router.push(
|
|
||||||
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsDeliveryLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowCanSelect = useCallback(
|
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
||||||
(row: Row<Marketing>): boolean => {
|
const approval = row.original.latest_approval;
|
||||||
const approval = row.original.latest_approval;
|
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
||||||
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);
|
||||||
@@ -586,53 +329,6 @@ 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 [
|
||||||
{
|
{
|
||||||
@@ -640,22 +336,7 @@ const MarketingTable = () => {
|
|||||||
size: 1,
|
size: 1,
|
||||||
header: ({ table }) => {
|
header: ({ table }) => {
|
||||||
const allRows = table.getRowModel().rows;
|
const allRows = table.getRowModel().rows;
|
||||||
const stepForBulkSelection =
|
const selectableRows = allRows.filter(getRowCanSelect);
|
||||||
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 &&
|
||||||
@@ -697,7 +378,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'so_number',
|
accessorKey: 'so_do_number',
|
||||||
header: 'No. Order',
|
header: 'No. Order',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
return props.row.original.do_number
|
return props.row.original.do_number
|
||||||
@@ -713,7 +394,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'approval.step_name',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -748,12 +429,10 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer',
|
accessorKey: 'customer.name',
|
||||||
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)
|
||||||
@@ -770,7 +449,6 @@ 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) {
|
||||||
@@ -792,14 +470,6 @@ const MarketingTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
header: 'Tanggal Dibuat',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.created_at
|
|
||||||
? formatDate(props.row.original.created_at, 'DD MMM yyyy')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
maxSize: 80,
|
maxSize: 80,
|
||||||
@@ -834,13 +504,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [
|
}, []);
|
||||||
deleteModal,
|
|
||||||
deliveryModal,
|
|
||||||
getRowCanSelect,
|
|
||||||
productsClickHandler,
|
|
||||||
selectedApprovalStep,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -863,7 +527,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 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>
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
@@ -877,7 +541,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Reject ({idsToProcess.length} Item)
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
@@ -893,7 +557,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Approve ({idsToProcess.length} Item)
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
@@ -902,18 +566,7 @@ const MarketingTable = () => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={[
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
'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();
|
||||||
}}
|
}}
|
||||||
@@ -959,17 +612,7 @@ 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} />
|
||||||
Ekspor ke Excel
|
Export to 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>
|
||||||
@@ -1003,9 +646,6 @@ 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
|
||||||
@@ -1037,16 +677,14 @@ 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 tahap ${selectedApprovalStep ?? '-'} (${idsToProcess.length} data)?`}
|
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${idsToProcess.length} data)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
isLoading: isApproveLoading,
|
|
||||||
onClick: confirmationModal.closeModal,
|
onClick: confirmationModal.closeModal,
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
||||||
isLoading: isApproveLoading,
|
|
||||||
onClick: approveMarketingHandler,
|
onClick: approveMarketingHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1070,169 +708,14 @@ 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={{
|
||||||
@@ -1294,7 +777,6 @@ const MarketingTable = () => {
|
|||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
initialValues={marketingFilterInitialValues}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ const SalesOrderFormModal = ({
|
|||||||
})
|
})
|
||||||
.filter((item) => Boolean(item)),
|
.filter((item) => Boolean(item)),
|
||||||
} as UpdateDeliveryOrderPayload);
|
} as UpdateDeliveryOrderPayload);
|
||||||
|
|
||||||
switch (modalAction) {
|
switch (modalAction) {
|
||||||
case 'add':
|
case 'add':
|
||||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||||
@@ -262,7 +261,11 @@ const SalesOrderFormModal = ({
|
|||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
||||||
useFormikErrorList(formik);
|
useFormikErrorList(formik, {
|
||||||
|
onAfterSubmit: () => {
|
||||||
|
router.push('/marketing');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ================== FORM REPEATER HANDLER ==================
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ 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',
|
||||||
'Seluruh data pengiriman harus diisi lengkap!',
|
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
|
||||||
function (items) {
|
function (items) {
|
||||||
if (!items || items.length === 0) return false;
|
if (!items || items.length === 0) return false;
|
||||||
|
|
||||||
// VALIDASI: seluruh item harus valid full
|
// VALIDASI: minimal 1 item valid full
|
||||||
const itemSchema = DeliveryOrderProductSchema;
|
const itemSchema = DeliveryOrderProductSchema;
|
||||||
|
|
||||||
const hasValidItem = items.every((item) => {
|
const hasValidItem = items.some((item) => {
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
return itemSchema.isValidSync(item, { abortEarly: true });
|
return itemSchema.isValidSync(item, { abortEarly: true });
|
||||||
});
|
});
|
||||||
@@ -123,17 +123,8 @@ export const SalesProductToFieldValues = (
|
|||||||
total_price: product.total_price,
|
total_price: product.total_price,
|
||||||
marketing_type: product.marketing_type
|
marketing_type: product.marketing_type
|
||||||
? {
|
? {
|
||||||
value:
|
value: product.marketing_type,
|
||||||
product.marketing_type === 'AYAM' ||
|
label: formatTitleCase(product.marketing_type),
|
||||||
product.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM,AYAM_PULLET'
|
|
||||||
: product.marketing_type,
|
|
||||||
label: formatTitleCase(
|
|
||||||
product.marketing_type === 'AYAM' ||
|
|
||||||
product.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM'
|
|
||||||
: product.marketing_type
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: product.convertion_unit
|
convertion_unit: product.convertion_unit
|
||||||
@@ -153,11 +144,9 @@ export const DeliveryProductToFieldValues = (
|
|||||||
delivery: BaseDeliveryOrder
|
delivery: BaseDeliveryOrder
|
||||||
): DeliveryOrderProductFormValues[] => {
|
): DeliveryOrderProductFormValues[] => {
|
||||||
const data = delivery.deliveries.map((item) => {
|
const data = delivery.deliveries.map((item) => {
|
||||||
const salesOrder =
|
const salesOrder = salesOrders.find(
|
||||||
salesOrders.find((so) => so.id === item.marketing_product_id) ??
|
(so) => so.product_warehouse.id === item.product_warehouse.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,
|
||||||
@@ -191,20 +180,11 @@ 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: item.marketing_product_id ?? salesOrder?.id,
|
marketing_product_id: salesOrder?.id,
|
||||||
marketing_type: salesOrder?.marketing_type
|
marketing_type: salesOrder?.marketing_type
|
||||||
? {
|
? {
|
||||||
value:
|
value: salesOrder?.marketing_type,
|
||||||
salesOrder?.marketing_type === 'AYAM' ||
|
label: formatTitleCase(salesOrder?.marketing_type),
|
||||||
salesOrder?.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM,AYAM_PULLET'
|
|
||||||
: salesOrder?.marketing_type,
|
|
||||||
label: formatTitleCase(
|
|
||||||
salesOrder?.marketing_type === 'AYAM' ||
|
|
||||||
salesOrder?.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM'
|
|
||||||
: salesOrder?.marketing_type
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: salesOrder?.convertion_unit
|
convertion_unit: salesOrder?.convertion_unit
|
||||||
@@ -214,7 +194,7 @@ export const DeliveryProductToFieldValues = (
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
marketing_product: {
|
marketing_product: {
|
||||||
id: item.marketing_product_id ?? salesOrder?.id,
|
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,6 +146,15 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ============ Fetch Data ============
|
// ============ Fetch Data ============
|
||||||
|
const { data: productData } = useSWR(
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.basePath + '/' + selectedProduct?.value
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.getSingle(Number(selectedProduct?.value))
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
// Options Week dari minggu 1 - 22
|
// Options Week dari minggu 1 - 22
|
||||||
// const optionsWeek = useMemo(() => {
|
// const optionsWeek = useMemo(() => {
|
||||||
@@ -181,19 +190,12 @@ 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 =
|
const found = doItem.deliveries.find(
|
||||||
doItem.deliveries.find(
|
(d) =>
|
||||||
(d) => d.marketing_product_id === marketingProductId
|
d.product_warehouse.id ===
|
||||||
) ??
|
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,
|
||||||
@@ -401,10 +403,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
if (
|
if (!Boolean(initialValues.qty)) {
|
||||||
!Boolean(initialValues.qty) &&
|
|
||||||
!Boolean(initialValues.marketing_product_id)
|
|
||||||
) {
|
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
} else {
|
} else {
|
||||||
setFormikValues({
|
setFormikValues({
|
||||||
@@ -414,7 +413,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
});
|
});
|
||||||
if (initialValues?.marketing_product_id) {
|
if (initialValues?.marketing_product_id) {
|
||||||
setSelectedProduct({
|
setSelectedProduct({
|
||||||
value: initialValues?.marketing_product_id,
|
value: initialValues?.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);
|
||||||
}
|
}
|
||||||
@@ -431,8 +430,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
handleBlurField(currentInput);
|
handleBlurField(currentInput);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'uom',
|
'uom',
|
||||||
initialValues?.marketing_product?.product_warehouse_data?.product?.uom
|
isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
|
||||||
?.name ?? ''
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -805,8 +803,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-gray-500'>
|
<span className='text-sm text-gray-500'>
|
||||||
{initialValues?.marketing_product?.product_warehouse_data
|
{isResponseSuccess(productData)
|
||||||
?.product?.uom?.name ?? ''}
|
? productData?.data?.uom.name
|
||||||
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -817,8 +816,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty +
|
)?.qty +
|
||||||
' ' +
|
' ' +
|
||||||
(initialValues?.marketing_product?.product_warehouse_data
|
(isResponseSuccess(productData)
|
||||||
?.product?.uom?.name ?? '')
|
? productData?.data?.uom.name
|
||||||
|
: '')
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -252,11 +252,6 @@ const SalesOrderProductForm = ({
|
|||||||
setSelectedProductWarehouse(productWarehouse || null);
|
setSelectedProductWarehouse(productWarehouse || null);
|
||||||
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
||||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||||
|
|
||||||
if (productWarehouse?.quantity) {
|
|
||||||
handleFieldChange('qty', productWarehouse?.quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
||||||
if (
|
if (
|
||||||
productWarehouse?.week !== undefined &&
|
productWarehouse?.week !== undefined &&
|
||||||
|
|||||||
@@ -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 !== undefined && item.qty !== null && item.qty !== ''
|
{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 !== undefined && item.qty !== null && item.qty !== ''
|
{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, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, 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,6 +20,8 @@ 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',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AreasTable = () => {
|
const AreasTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const AreasTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
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>([]);
|
||||||
@@ -134,8 +137,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, 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,6 +20,8 @@ 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',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BanksTable = () => {
|
const BanksTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const BanksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
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>([]);
|
||||||
@@ -134,8 +137,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, 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,6 +20,8 @@ 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',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CustomersTable = () => {
|
const CustomersTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const CustomersTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
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>([]);
|
||||||
@@ -136,8 +139,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -189,11 +201,6 @@ const CustomersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'bank_name',
|
|
||||||
header: 'Nama Bank',
|
|
||||||
cell: (props) => props.row.original.bank_name || '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Customer, unknown>) => {
|
cell: (props: CellContext<Customer, unknown>) => {
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export const CustomerFormSchema = Yup.object({
|
|||||||
.email('Format email tidak valid!')
|
.email('Format email tidak valid!')
|
||||||
.required('Email wajib diisi!'),
|
.required('Email wajib diisi!'),
|
||||||
|
|
||||||
bank_name: Yup.string()
|
|
||||||
.min(3, 'Nama bank minimal 3 karakter!')
|
|
||||||
.required('Nama bank wajib diisi!'),
|
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ const CustomerForm = ({
|
|||||||
},
|
},
|
||||||
type: normalizeType(initialValues?.type),
|
type: normalizeType(initialValues?.type),
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
bank_name: initialValues?.bank_name ?? '',
|
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
@@ -165,7 +164,6 @@ const CustomerForm = ({
|
|||||||
pic_id: values.picId,
|
pic_id: values.picId,
|
||||||
type: (values.type as OptionType).value as string,
|
type: (values.type as OptionType).value as string,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
bank_name: values.bank_name,
|
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,22 +286,6 @@ const CustomerForm = ({
|
|||||||
errorMessage={formik.errors.phone}
|
errorMessage={formik.errors.phone}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label='Nama Bank'
|
|
||||||
name='bank_name'
|
|
||||||
placeholder='Masukkan nama bank customer'
|
|
||||||
value={formik.values.bank_name}
|
|
||||||
onChange={(e) =>
|
|
||||||
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
|
||||||
}
|
|
||||||
onBlur={formik.handleBlur}
|
|
||||||
isError={
|
|
||||||
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.bank_name}
|
|
||||||
readOnly={formType === 'detail'}
|
|
||||||
/>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, 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,6 +20,8 @@ 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',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FlockTable = () => {
|
const FlockTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const FlockTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
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>([]);
|
||||||
@@ -136,8 +139,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
|
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';
|
||||||
@@ -28,6 +35,7 @@ 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,
|
||||||
@@ -114,21 +122,20 @@ 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: undefined,
|
locationFilter: '',
|
||||||
picFilter: undefined,
|
picFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -136,8 +143,6 @@ 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 =====
|
||||||
@@ -146,34 +151,22 @@ const KandangsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<KandangFilterType>({
|
const formik = useFormik<KandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
location: tableFilterState.locationFilter,
|
location_id: null,
|
||||||
pic: tableFilterState.picFilter,
|
pic_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: KandangFilterSchema,
|
validationSchema: KandangFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('locationFilter', values.location || undefined, true);
|
updateFilter('locationFilter', values.location_id || '');
|
||||||
updateFilter('picFilter', values.pic || undefined, true);
|
updateFilter('picFilter', values.pic_id || '');
|
||||||
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,
|
||||||
@@ -201,15 +194,43 @@ const KandangsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterLocationChange = (
|
const handleFilterLocationChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const location = val as OptionType | null;
|
||||||
setFieldValue('location', val);
|
const locationId = location?.value ? String(location.value) : null;
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterPicChange = (val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('location_id', locationId);
|
||||||
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 = () => {
|
||||||
@@ -234,8 +255,17 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -445,13 +475,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={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={formik.values.location}
|
value={locationIdValue}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
@@ -464,7 +494,7 @@ const KandangsTable = () => {
|
|||||||
label='PIC'
|
label='PIC'
|
||||||
placeholder='Pilih PIC'
|
placeholder='Pilih PIC'
|
||||||
options={picOptions}
|
options={picOptions}
|
||||||
value={formik.values.pic}
|
value={picIdValue}
|
||||||
onChange={handleFilterPicChange}
|
onChange={handleFilterPicChange}
|
||||||
onInputChange={setPicInputValue}
|
onInputChange={setPicInputValue}
|
||||||
isLoading={isLoadingPicOptions}
|
isLoading={isLoadingPicOptions}
|
||||||
@@ -480,14 +510,17 @@ 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={formikResetHandler}
|
onClick={() => {
|
||||||
|
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}
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const KandangFilterSchema = Yup.object().shape({
|
export const KandangFilterSchema = object().shape({
|
||||||
location: Yup.object({
|
location_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
pic_id: 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?: OptionType<string>;
|
location_id: string | null;
|
||||||
pic?: OptionType<string>;
|
pic_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
|
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';
|
||||||
@@ -25,6 +32,7 @@ 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,
|
||||||
@@ -110,27 +118,25 @@ 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: undefined,
|
areaFilter: '',
|
||||||
},
|
},
|
||||||
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 =====
|
||||||
@@ -139,28 +145,19 @@ const LocationsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<LocationFilterType>({
|
const formik = useFormik<LocationFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area: tableFilterState.areaFilter,
|
area_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: LocationFilterSchema,
|
validationSchema: LocationFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area || undefined, true);
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
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,
|
||||||
@@ -175,9 +172,24 @@ const LocationsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterAreaChange = useCallback(
|
||||||
formik.setFieldValue('area', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
};
|
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 = () => {
|
||||||
@@ -200,10 +212,19 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -404,13 +425,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={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={formik.values.area}
|
value={areaIdValue}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -426,7 +447,10 @@ 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={formikResetHandler}
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const LocationFilterSchema = Yup.object().shape({
|
export const LocationFilterSchema = object().shape({
|
||||||
area: Yup.object({
|
area_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LocationFilterType = {
|
export type LocationFilterType = {
|
||||||
area?: OptionType<string>;
|
area_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ 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',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NonstocksTable = () => {
|
const NonstocksTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,16 +114,22 @@ const NonstocksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
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 {
|
||||||
@@ -137,7 +148,8 @@ const NonstocksTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, 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';
|
||||||
@@ -20,6 +21,7 @@ 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',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProductCategoryTable = () => {
|
const ProductCategoryTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -115,10 +120,12 @@ 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 {
|
||||||
@@ -137,7 +144,8 @@ const ProductCategoryTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -206,6 +214,10 @@ 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,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
|
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';
|
||||||
@@ -26,6 +33,7 @@ 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,
|
||||||
@@ -111,27 +119,25 @@ 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: undefined,
|
productCategoryFilter: '',
|
||||||
},
|
},
|
||||||
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 =====
|
||||||
@@ -140,32 +146,19 @@ const ProductsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<ProductFilterType>({
|
const formik = useFormik<ProductFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_category: tableFilterState.productCategoryFilter,
|
product_category_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: ProductFilterSchema,
|
validationSchema: ProductFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter(
|
updateFilter('productCategoryFilter', values.product_category_id || '');
|
||||||
'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,
|
||||||
@@ -180,11 +173,25 @@ const ProductsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductCategoryChange = (
|
const handleFilterProductCategoryChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const category = val as OptionType | null;
|
||||||
formik.setFieldValue('product_category', val);
|
const categoryId = category?.value ? String(category.value) : null;
|
||||||
};
|
|
||||||
|
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 = () => {
|
||||||
@@ -192,6 +199,10 @@ const ProductsTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -209,8 +220,13 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -461,13 +477,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={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Kategori Produk'
|
label='Kategori Produk'
|
||||||
placeholder='Pilih Kategori Produk'
|
placeholder='Pilih Kategori Produk'
|
||||||
options={productCategoryOptions}
|
options={productCategoryOptions}
|
||||||
value={formik.values.product_category}
|
value={productCategoryIdValue}
|
||||||
onChange={handleFilterProductCategoryChange}
|
onChange={handleFilterProductCategoryChange}
|
||||||
onInputChange={setProductCategoryInputValue}
|
onInputChange={setProductCategoryInputValue}
|
||||||
isLoading={isLoadingProductCategoryOptions}
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
@@ -483,7 +499,10 @@ 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={formikResetHandler}
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const ProductFilterSchema = Yup.object().shape({
|
export const ProductFilterSchema = object().shape({
|
||||||
product_category: Yup.object({
|
product_category_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProductFilterType = {
|
export type ProductFilterType = {
|
||||||
product_category?: OptionType<string>;
|
product_category_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,44 +128,27 @@ 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: filterInitialValues,
|
initialValues: {
|
||||||
|
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(
|
||||||
() => [
|
() => [
|
||||||
@@ -398,7 +381,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={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Kategori'
|
label='Kategori'
|
||||||
@@ -414,9 +397,13 @@ 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='reset'
|
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={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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';
|
||||||
@@ -29,7 +30,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,
|
||||||
@@ -116,21 +117,20 @@ 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: undefined,
|
categoryFilter: '',
|
||||||
flagFilter: undefined,
|
flagFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -138,8 +138,6 @@ const SuppliersTable = () => {
|
|||||||
categoryFilter: 'category_id',
|
categoryFilter: 'category_id',
|
||||||
flagFilter: 'flag',
|
flagFilter: 'flag',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'supplier-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -148,33 +146,26 @@ const SuppliersTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<SupplierFilterType>({
|
const formik = useFormik<SupplierFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
category: tableFilterState.categoryFilter,
|
category_id: null,
|
||||||
flag: tableFilterState.flagFilter === 'EKSPEDISI',
|
flag: false,
|
||||||
},
|
},
|
||||||
validationSchema: SupplierFilterSchema,
|
validationSchema: SupplierFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('categoryFilter', values.category || undefined, true);
|
updateFilter('categoryFilter', values.category_id || '');
|
||||||
updateFilter('flagFilter', values.flag === true ? 'EKSPEDISI' : '', true);
|
updateFilter(
|
||||||
|
'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) =====
|
||||||
@@ -196,11 +187,15 @@ const SuppliersTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterCategoryChange = (
|
const handleFilterCategoryChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const option = val as OptionType | null;
|
||||||
setFieldValue('category', val);
|
const categoryId = option?.value ? String(option.value) : null;
|
||||||
};
|
|
||||||
|
setFieldValue('category_id', categoryId);
|
||||||
|
},
|
||||||
|
[setFieldValue]
|
||||||
|
);
|
||||||
|
|
||||||
const handleFilterFlagChange = useCallback(
|
const handleFilterFlagChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
@@ -218,13 +213,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;
|
||||||
@@ -248,6 +243,14 @@ 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 {
|
||||||
@@ -266,7 +269,8 @@ const SuppliersTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -326,11 +330,6 @@ const SuppliersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'bank_name',
|
|
||||||
header: 'Nama Bank',
|
|
||||||
cell: (props) => props.row.original.bank_name || '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'address',
|
accessorKey: 'address',
|
||||||
header: 'Alamat',
|
header: 'Alamat',
|
||||||
@@ -492,13 +491,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={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Kategori'
|
label='Kategori'
|
||||||
placeholder='Pilih Kategori'
|
placeholder='Pilih Kategori'
|
||||||
options={categoryOptions}
|
options={categoryOptions}
|
||||||
value={formik.values.category}
|
value={categoryIdValue}
|
||||||
onChange={handleFilterCategoryChange}
|
onChange={handleFilterCategoryChange}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -518,9 +517,13 @@ 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='reset'
|
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={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, boolean, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const SupplierFilterSchema = Yup.object().shape({
|
export const SupplierFilterSchema = object().shape({
|
||||||
category: Yup.object({
|
category_id: string().nullable(),
|
||||||
value: Yup.string().required(),
|
flag: boolean().nullable(),
|
||||||
label: Yup.string().required(),
|
|
||||||
}).nullable(),
|
|
||||||
|
|
||||||
flag: Yup.boolean().nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SupplierFilterType = {
|
export type SupplierFilterType = {
|
||||||
category?: OptionType<string>;
|
category_id: string | null;
|
||||||
flag: boolean | null;
|
flag: boolean | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ export const SupplierFormSchema = Yup.object({
|
|||||||
npwp: Yup.string()
|
npwp: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor NPWP hanya boleh berisi angka!')
|
||||||
.required('Nomor NPWP wajib diisi!'),
|
.required('Nomor NPWP wajib diisi!'),
|
||||||
bank_name: Yup.string()
|
|
||||||
.min(3, 'Nama bank minimal 3 karakter!')
|
|
||||||
.required('Nama bank wajib diisi!'),
|
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ const SupplierForm = ({
|
|||||||
email: initialValues?.email ?? '',
|
email: initialValues?.email ?? '',
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
npwp: initialValues?.npwp ?? '',
|
npwp: initialValues?.npwp ?? '',
|
||||||
bank_name: initialValues?.bank_name ?? '',
|
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
due_date: initialValues?.due_date ?? 1,
|
due_date: initialValues?.due_date ?? 1,
|
||||||
};
|
};
|
||||||
@@ -150,7 +149,6 @@ const SupplierForm = ({
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
npwp: values.npwp,
|
npwp: values.npwp,
|
||||||
bank_name: values.bank_name,
|
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
due_date: parseInt(values.due_date.toString()),
|
due_date: parseInt(values.due_date.toString()),
|
||||||
};
|
};
|
||||||
@@ -370,22 +368,6 @@ const SupplierForm = ({
|
|||||||
errorMessage={formik.errors.npwp}
|
errorMessage={formik.errors.npwp}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label='Nama Bank'
|
|
||||||
name='bank_name'
|
|
||||||
placeholder='Masukkan nama bank supplier'
|
|
||||||
value={formik.values.bank_name}
|
|
||||||
onChange={(e) =>
|
|
||||||
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
|
||||||
}
|
|
||||||
onBlur={formik.handleBlur}
|
|
||||||
isError={
|
|
||||||
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.bank_name}
|
|
||||||
readOnly={formType === 'detail'}
|
|
||||||
/>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, 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,6 +20,8 @@ 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',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UomsTable = () => {
|
const UomsTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,16 +114,22 @@ const UomsTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
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 {
|
||||||
@@ -135,7 +146,8 @@ const UomsTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import {
|
||||||
|
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';
|
||||||
@@ -24,6 +31,7 @@ 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,
|
||||||
@@ -112,6 +120,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WarehousesTable = () => {
|
const WarehousesTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -130,8 +141,6 @@ 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 =====
|
||||||
@@ -140,36 +149,27 @@ const WarehousesTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<WarehouseFilterType>({
|
const formik = useFormik<WarehouseFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: tableFilterState.areaFilter || null,
|
area_id: null,
|
||||||
active_project_flock:
|
active_project_flock: false,
|
||||||
tableFilterState.activeProjectFlockFilter === 'true',
|
|
||||||
},
|
},
|
||||||
validationSchema: WarehouseFilterSchema,
|
validationSchema: WarehouseFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '', true);
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
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 formikResetHandler = () => {
|
const { setFieldValue } = formik;
|
||||||
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,6 +243,26 @@ 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 {
|
||||||
@@ -261,7 +281,8 @@ const WarehousesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -486,7 +507,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={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
@@ -517,7 +538,10 @@ 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={formikResetHandler}
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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';
|
||||||
@@ -22,6 +23,7 @@ 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';
|
||||||
@@ -43,7 +45,6 @@ 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,
|
||||||
@@ -147,6 +148,7 @@ 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);
|
||||||
@@ -172,9 +174,6 @@ 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',
|
||||||
@@ -186,11 +185,7 @@ 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 =====
|
||||||
@@ -211,7 +206,8 @@ 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,
|
||||||
@@ -261,18 +257,6 @@ 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);
|
||||||
},
|
},
|
||||||
@@ -282,9 +266,6 @@ 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();
|
||||||
@@ -326,55 +307,40 @@ 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;
|
||||||
const found = areaOptions.find(
|
return (
|
||||||
(opt) => String(opt.value) === formik.values.area_id
|
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||||
|
null
|
||||||
);
|
);
|
||||||
if (found) return found;
|
}, [formik.values.area_id, areaOptions]);
|
||||||
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;
|
||||||
const found = locationOptions.find(
|
return (
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
);
|
);
|
||||||
if (found) return found;
|
}, [formik.values.location_id, locationOptions]);
|
||||||
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;
|
||||||
const found = kandangOptions.find(
|
return (
|
||||||
(opt) => String(opt.value) === formik.values.kandang_id
|
kandangOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.kandang_id
|
||||||
|
) || null
|
||||||
);
|
);
|
||||||
if (found) return found;
|
}, [formik.values.kandang_id, kandangOptions]);
|
||||||
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;
|
||||||
@@ -384,6 +350,13 @@ 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;
|
||||||
@@ -452,11 +425,18 @@ 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) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprovalHandler = async (
|
const confirmApprovalHandler = async (
|
||||||
notes: string,
|
notes: string,
|
||||||
approvalAction: 'APPROVED' | 'REJECTED'
|
approvalAction: 'APPROVED' | 'REJECTED'
|
||||||
@@ -574,7 +554,6 @@ 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]);
|
||||||
|
|
||||||
@@ -797,6 +776,14 @@ 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();
|
||||||
@@ -985,17 +972,55 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
|
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={[
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
'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>
|
||||||
|
|
||||||
@@ -1324,14 +1349,18 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
isClearable={true}
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NumberInput
|
<SelectInputRadio
|
||||||
label='Periode'
|
label='Periode'
|
||||||
name='period'
|
placeholder='Pilih Periode'
|
||||||
placeholder='Masukkan Periode'
|
options={periodOptions}
|
||||||
value={formik.values.period ?? ''}
|
value={periodValue}
|
||||||
onChange={formik.handleChange}
|
onChange={(val) => {
|
||||||
onBlur={formik.handleBlur}
|
if (!Array.isArray(val)) {
|
||||||
|
formik.setFieldValue('period', val?.value || null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ 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[];
|
||||||
};
|
};
|
||||||
@@ -110,12 +109,6 @@ 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,10 +152,6 @@ 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: '',
|
||||||
@@ -265,7 +261,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingFlocks,
|
isLoadingOptions: isLoadingFlocks,
|
||||||
options: optionsFlock,
|
options: optionsFlock,
|
||||||
loadMore: loadMoreFlock,
|
loadMore: loadMoreFlock,
|
||||||
} = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
location_id: selectedLocation,
|
location_id: selectedLocation,
|
||||||
area_id: selectedArea,
|
area_id: selectedArea,
|
||||||
@@ -283,7 +279,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
setInputValue: setInputValueLocation,
|
setInputValue: setInputValueLocation,
|
||||||
loadMore: loadMoreLocation,
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
? selectedArea
|
? selectedArea
|
||||||
@@ -295,7 +291,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
setInputValue: setInputValueProductionStandard,
|
setInputValue: setInputValueProductionStandard,
|
||||||
loadMore: loadMoreProductionStandard,
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +307,7 @@ const ProjectFlockForm = ({
|
|||||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||||
|
|
||||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||||
selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
|
`${selectedFlock?.toString()}/periods`,
|
||||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -533,7 +529,6 @@ 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: {
|
||||||
@@ -573,7 +568,6 @@ 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 {
|
||||||
@@ -799,7 +793,6 @@ 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;
|
||||||
|
|
||||||
@@ -1029,18 +1022,12 @@ const ProjectFlockForm = ({
|
|||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='periode'
|
name='period'
|
||||||
label='Periode'
|
label='Periode'
|
||||||
|
disabled
|
||||||
|
readOnly
|
||||||
placeholder='Periode Flock'
|
placeholder='Periode Flock'
|
||||||
value={formik.values.periode}
|
value={selectedLocation ? inputPeriod : ''}
|
||||||
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,6 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
import 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';
|
||||||
@@ -12,7 +18,6 @@ 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';
|
||||||
@@ -34,11 +39,13 @@ 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 { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
import { 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';
|
||||||
@@ -68,26 +75,6 @@ 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,
|
||||||
@@ -279,111 +266,80 @@ 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: null,
|
areaFilter: '',
|
||||||
locationFilter: null,
|
locationFilter: '',
|
||||||
projectFlockFilter: null,
|
kandangFilter: '',
|
||||||
kandangFilter: null,
|
projectFlockKandangFilter: '',
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
persist: true,
|
|
||||||
storeName: 'recording-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [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: tableFilterState.areaFilter,
|
area_id: null,
|
||||||
location_id: tableFilterState.locationFilter,
|
location_id: null,
|
||||||
project_flock_id: tableFilterState.projectFlockFilter,
|
kandang_id: null,
|
||||||
kandang_id: tableFilterState.kandangFilter,
|
project_flock_kandang_id: null,
|
||||||
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, true);
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
updateFilter('locationFilter', values.location_id, true);
|
updateFilter('locationFilter', values.location_id || '');
|
||||||
updateFilter('projectFlockFilter', values.project_flock_id, true);
|
updateFilter('kandangFilter', values.kandang_id || '');
|
||||||
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) =>
|
||||||
@@ -399,14 +355,10 @@ 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,
|
||||||
@@ -418,6 +370,13 @@ 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,
|
||||||
@@ -428,9 +387,7 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'search',
|
'search',
|
||||||
{
|
locationParams
|
||||||
area_id: String(formik.values.area_id?.value),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -445,6 +402,13 @@ 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,
|
||||||
@@ -456,41 +420,34 @@ 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 (!project_flock_id || !projectFlocksRawData) return [];
|
if (!filterProjectFlock || !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((pf) =>
|
const selectedProjectFlockData = data.find(
|
||||||
pf.id === formik.values.project_flock_id?.value
|
(pf) => pf.id === filterProjectFlock.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 || '',
|
||||||
}));
|
}));
|
||||||
}, [project_flock_id, projectFlocksRawData]);
|
}, [filterProjectFlock, projectFlocksRawData]);
|
||||||
|
|
||||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
if (!project_flock_id?.value || !kandang_id?.value) return null;
|
if (!filterProjectFlock || !filterKandang) return null;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
project_flock_id: project_flock_id.value.toString(),
|
project_flock_id: filterProjectFlock.value.toString(),
|
||||||
kandang_id: kandang_id.value.toString(),
|
kandang_id: filterKandang.value.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||||
}, [project_flock_id, kandang_id]);
|
}, [filterProjectFlock, filterKandang]);
|
||||||
|
|
||||||
const { data: projectFlockKandangLookupData } = useSWR(
|
const { data: projectFlockKandangLookupData } = useSWR(
|
||||||
projectFlockKandangLookupUrl,
|
projectFlockKandangLookupUrl,
|
||||||
@@ -512,45 +469,118 @@ 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);
|
||||||
formik.setFieldValue('project_flock_kandang_id', pfkId);
|
setFilterProjectFlockKandangId(projectFlockKandangLookup.id);
|
||||||
|
formikRef.current.setFieldValue('project_flock_kandang_id', pfkId);
|
||||||
} else {
|
} else {
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
setFilterProjectFlockKandangId(undefined);
|
||||||
|
formikRef.current.setFieldValue('project_flock_kandang_id', null);
|
||||||
}
|
}
|
||||||
}, [projectFlockKandangLookup]);
|
}, [projectFlockKandangLookup]);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterAreaChange = useCallback(
|
||||||
formik.setFieldValue('area_id', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('location_id', null);
|
const area = val as OptionType | null;
|
||||||
formik.setFieldValue('project_flock_id', null);
|
const areaId = area?.value ? String(area.value) : null;
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterLocationChange = (
|
formik.setFieldValue('area_id', areaId);
|
||||||
val: OptionType | OptionType[] | null
|
formik.setFieldValue('location_id', null);
|
||||||
) => {
|
formik.setFieldValue('kandang_id', null);
|
||||||
formik.setFieldValue('location_id', val);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
formik.setFieldValue('project_flock_id', null);
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterProjectFlockChange = (
|
setFilterArea(area);
|
||||||
val: OptionType | OptionType[] | null
|
setFilterLocation(null);
|
||||||
) => {
|
setFilterProjectFlock(null);
|
||||||
formik.setFieldValue('project_flock_id', val);
|
setFilterKandang(null);
|
||||||
formik.setFieldValue('kandang_id', null);
|
setFilterLocationAreaId(areaId || '');
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
setFilterProjectFlockLocationId('');
|
||||||
};
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
const handleFilterKandangChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterLocationChange = useCallback(
|
||||||
formik.setFieldValue('kandang_id', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
const location = val as OptionType | 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 = () => {
|
||||||
@@ -558,9 +588,25 @@ const RecordingTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const isRecordingApproved = useCallback((recording: Recording): boolean => {
|
||||||
updateFilter('search', e.target.value, true);
|
return (
|
||||||
};
|
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);
|
||||||
@@ -652,60 +698,6 @@ 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> = {};
|
||||||
@@ -866,8 +858,7 @@ const RecordingTable = () => {
|
|||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
{props.row.original.day} (Minggu ke-
|
{props.row.original.day} (Minggu ke-
|
||||||
{props.row.original.week} hari ke-
|
{props.row.original.project_flock.production_standart.week})
|
||||||
{props.row.original.excess_days})
|
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1113,7 +1104,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)} butir`
|
? `${value.toFixed(2)}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1129,7 +1120,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)} btr`
|
? `${value.toFixed(2)}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1377,16 +1368,6 @@ 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>
|
||||||
@@ -1465,13 +1446,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={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={formik.values.area_id}
|
value={areaIdValue}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -1484,13 +1465,13 @@ const RecordingTable = () => {
|
|||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={formik.values.location_id}
|
value={locationIdValue}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isDisabled={!formik.values.area_id?.value}
|
isDisabled={!filterArea}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1498,13 +1479,13 @@ const RecordingTable = () => {
|
|||||||
label='Project Flock'
|
label='Project Flock'
|
||||||
placeholder='Pilih Project Flock'
|
placeholder='Pilih Project Flock'
|
||||||
options={projectFlockOptions}
|
options={projectFlockOptions}
|
||||||
value={formik.values.project_flock_id}
|
value={projectFlockIdValue}
|
||||||
onChange={handleFilterProjectFlockChange}
|
onChange={handleFilterProjectFlockChange}
|
||||||
onInputChange={setProjectFlockInputValue}
|
onInputChange={setProjectFlockInputValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!formik.values.location_id?.value}
|
isDisabled={!filterLocation}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1512,35 +1493,11 @@ const RecordingTable = () => {
|
|||||||
label='Kandang'
|
label='Kandang'
|
||||||
placeholder='Pilih Kandang'
|
placeholder='Pilih Kandang'
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
value={formik.values.kandang_id}
|
value={kandangIdValue}
|
||||||
onChange={handleFilterKandangChange}
|
onChange={handleFilterKandangChange}
|
||||||
isLoading={!formik.values.project_flock_id?.value}
|
isLoading={!filterProjectFlock}
|
||||||
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>
|
||||||
@@ -1548,16 +1505,30 @@ 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='reset'
|
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={() => {
|
||||||
|
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={!formik.isValid || formik.isSubmitting}
|
disabled={
|
||||||
|
!formik.isValid ||
|
||||||
|
formik.isSubmitting ||
|
||||||
|
!formik.values.kandang_id
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1580,76 +1551,6 @@ 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,40 +1,15 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const RecordingFilterSchema = Yup.object().shape({
|
export const RecordingFilterSchema = object().shape({
|
||||||
area_id: Yup.object({
|
area_id: string().nullable(),
|
||||||
value: Yup.number().nullable(),
|
location_id: string().nullable(),
|
||||||
label: Yup.string().nullable(),
|
kandang_id: string().nullable(),
|
||||||
}).nullable(),
|
project_flock_kandang_id: string().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: OptionType<number> | null;
|
area_id: string | null;
|
||||||
location_id: OptionType<number> | null;
|
location_id: string | null;
|
||||||
project_flock_id: OptionType<number> | null;
|
kandang_id: string | null;
|
||||||
kandang_id: OptionType<number> | null;
|
project_flock_kandang_id: string | null;
|
||||||
project_flock_kandang_id: number | null;
|
|
||||||
approval_status: OptionType<string> | null;
|
|
||||||
project_flock_category: OptionType<string> | null;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ 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;
|
||||||
@@ -31,19 +29,11 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
} | null;
|
} | null;
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
stocks: {
|
stocks: {
|
||||||
product_warehouse_id:
|
product_warehouse_id: number;
|
||||||
| {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
depletions: {
|
depletions: {
|
||||||
product_warehouse_id?: {
|
product_warehouse_id?: number;
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
source_product_warehouse_id?: number;
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
@@ -51,48 +41,34 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
|
|
||||||
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||||
eggs: {
|
eggs: {
|
||||||
product_warehouse_id?: {
|
product_warehouse_id?: number;
|
||||||
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: {
|
product_warehouse_id: number;
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DepletionSchema = {
|
export type DepletionSchema = {
|
||||||
product_warehouse_id?: {
|
product_warehouse_id?: number;
|
||||||
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?: {
|
product_warehouse_id?: number;
|
||||||
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.object({
|
product_warehouse_id: Yup.number()
|
||||||
value: Yup.number().min(1).required(),
|
|
||||||
label: Yup.string().required(),
|
|
||||||
})
|
|
||||||
.required('Produk wajib diisi!')
|
.required('Produk wajib diisi!')
|
||||||
.typeError('Produk wajib diisi!'),
|
.min(1, '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!')
|
||||||
@@ -100,12 +76,9 @@ 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.object({
|
product_warehouse_id: Yup.number()
|
||||||
value: Yup.number().min(1).required(),
|
|
||||||
label: Yup.string().required(),
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.typeError('Depletions harus berupa angka!'),
|
||||||
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!'),
|
||||||
@@ -115,12 +88,9 @@ 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.object({
|
product_warehouse_id: Yup.number()
|
||||||
value: Yup.number().min(1).required(),
|
|
||||||
label: Yup.string().required(),
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.typeError('Kondisi telur harus berupa angka!'),
|
||||||
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!'),
|
||||||
});
|
});
|
||||||
@@ -278,18 +248,14 @@ 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: {
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
value: stock.product_warehouse_id,
|
|
||||||
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
|
||||||
},
|
|
||||||
qty:
|
qty:
|
||||||
(stock as RecordingStock).qty ||
|
(stock as { qty?: number; usage_amount?: number }).qty ||
|
||||||
((stock as RecordingStock).usage_amount || 0) +
|
(stock as { qty?: number; usage_amount?: number }).usage_amount ||
|
||||||
((stock as RecordingStock).pending_qty || 0) ||
|
|
||||||
'',
|
'',
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: undefined,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -297,16 +263,13 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
(
|
(
|
||||||
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
||||||
) => ({
|
) => ({
|
||||||
product_warehouse_id: {
|
product_warehouse_id: depletion.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: undefined,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -318,15 +281,12 @@ export const getRecordingLayingFormInitialValues = (
|
|||||||
...getRecordingGrowingFormInitialValues(initialValues),
|
...getRecordingGrowingFormInitialValues(initialValues),
|
||||||
|
|
||||||
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
||||||
product_warehouse_id: {
|
product_warehouse_id: egg.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: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -31,14 +31,12 @@ import {
|
|||||||
RecordingApi,
|
RecordingApi,
|
||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
} from '@/services/api/production';
|
} from '@/services/api/production';
|
||||||
import { ProductionStandardApi, ProductApi } from '@/services/api/master-data';
|
import { ProductionStandardApi } 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';
|
||||||
|
|
||||||
@@ -501,20 +499,6 @@ 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) => {
|
||||||
@@ -522,7 +506,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?.value ?? 0,
|
product_warehouse_id: depletion.product_warehouse_id!,
|
||||||
...(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,
|
||||||
@@ -533,13 +517,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const stocks = recordingRestriction.canEditStock
|
const stocks = recordingRestriction.canEditStock
|
||||||
? (values.stocks ?? [])
|
? (values.stocks ?? [])
|
||||||
.filter((s) => s.product_warehouse_id?.value && s.qty)
|
.filter((s) => s.product_warehouse_id && s.qty)
|
||||||
.map((stock) => ({
|
.map((stock) => ({
|
||||||
// In migration mode, product_warehouse_id field holds product.id;
|
product_warehouse_id: stock.product_warehouse_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,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
@@ -551,19 +531,15 @@ 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?.value && d.qty)
|
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||||
.map((depletion) => ({
|
.map((depletion) => ({
|
||||||
product_warehouse_id: depletion.product_warehouse_id?.value ?? 0,
|
product_warehouse_id: depletion.product_warehouse_id!,
|
||||||
...(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,
|
||||||
}),
|
}),
|
||||||
@@ -573,7 +549,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?.value ?? 0,
|
product_warehouse_id: egg.product_warehouse_id!,
|
||||||
qty: Number(egg.qty) || 0,
|
qty: Number(egg.qty) || 0,
|
||||||
weight:
|
weight:
|
||||||
typeof egg.weight === 'number'
|
typeof egg.weight === 'number'
|
||||||
@@ -583,11 +559,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const stocks = recordingRestriction.canEditStock
|
const stocks = recordingRestriction.canEditStock
|
||||||
? values.stocks
|
? values.stocks
|
||||||
.filter((s) => s.product_warehouse_id?.value && s.qty)
|
.filter((s) => s.product_warehouse_id && s.qty)
|
||||||
.map((stock) => ({
|
.map((stock) => ({
|
||||||
...(isMigrationMode
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
? { 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,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
@@ -600,7 +574,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
...(eggs && eggs.length > 0 && { eggs }),
|
...(eggs && eggs.length > 0 && { eggs }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[isMigrationMode, recordingRestriction.canEditStock]
|
[recordingRestriction.canEditStock]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRecordingEditable = useCallback((recording?: Recording) => {
|
const isRecordingEditable = useCallback((recording?: Recording) => {
|
||||||
@@ -629,13 +603,11 @@ 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: stockProductsPW,
|
rawData: stockProducts,
|
||||||
isLoadingOptions: isLoadingStockProductsPW,
|
isLoadingOptions: isLoadingStockProducts,
|
||||||
loadMore: loadMoreStockProductsPW,
|
loadMore: loadMoreStockProducts,
|
||||||
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
||||||
flags: 'PAKAN,OVK',
|
flags: 'PAKAN,OVK',
|
||||||
limit: '100',
|
limit: '100',
|
||||||
@@ -644,29 +616,6 @@ 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,
|
||||||
@@ -1050,9 +999,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const items: Array<ProductWarehouse | null | undefined> = [];
|
const items: Array<ProductWarehouse | null | undefined> = [];
|
||||||
|
|
||||||
if (!isMigrationMode && isResponseSuccess(stockProductsPW)) {
|
if (isResponseSuccess(stockProducts)) {
|
||||||
items.push(
|
items.push(
|
||||||
...((stockProductsPW.data as unknown as ProductWarehouse[]) ?? [])
|
...((stockProducts.data as unknown as ProductWarehouse[]) ?? [])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,8 +1035,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
mergeKnownProductWarehouses(items);
|
mergeKnownProductWarehouses(items);
|
||||||
}, [
|
}, [
|
||||||
isMigrationMode,
|
stockProducts,
|
||||||
stockProductsPW,
|
|
||||||
depletionProductsData,
|
depletionProductsData,
|
||||||
eggProductsData,
|
eggProductsData,
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -1118,20 +1066,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const unifiedStockProducts = useMemo(() => {
|
const unifiedStockProducts = useMemo(() => {
|
||||||
if (isMigrationMode) {
|
const options = isResponseSuccess(stockProducts)
|
||||||
// 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(
|
||||||
stockProductsPW.data as unknown as ProductWarehouse[]
|
stockProducts.data as unknown as ProductWarehouse[]
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -1148,9 +1085,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [
|
}, [
|
||||||
isMigrationMode,
|
stockProducts,
|
||||||
stockProductsMaster,
|
|
||||||
stockProductsPW,
|
|
||||||
buildProductWarehouseOptions,
|
buildProductWarehouseOptions,
|
||||||
initialValues,
|
initialValues,
|
||||||
type,
|
type,
|
||||||
@@ -1269,22 +1204,6 @@ 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 = [];
|
||||||
}
|
}
|
||||||
@@ -1305,7 +1224,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
selectedKandang,
|
selectedKandang,
|
||||||
recordingRestriction.canEditStock,
|
recordingRestriction.canEditStock,
|
||||||
recordingRestriction.canEditDepletion,
|
recordingRestriction.canEditDepletion,
|
||||||
isMigrationMode,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const formik = useFormik<
|
const formik = useFormik<
|
||||||
@@ -1417,35 +1335,6 @@ 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;
|
||||||
|
|
||||||
@@ -1462,7 +1351,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?.value) return null;
|
if (!stock || !stock.product_warehouse_id) return null;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[formik.values.stocks, type]
|
[formik.values.stocks, type]
|
||||||
@@ -1472,7 +1361,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) => Number(s.product_warehouse_id) === Number(productWarehouseId)
|
(s) => s.product_warehouse_id === productWarehouseId
|
||||||
) as RecordingStock | undefined;
|
) as RecordingStock | undefined;
|
||||||
if (existingStock) {
|
if (existingStock) {
|
||||||
return {
|
return {
|
||||||
@@ -1492,25 +1381,21 @@ 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?.value) return null;
|
if (!stock || !stock.product_warehouse_id) return null;
|
||||||
|
|
||||||
const isDetail = (type as 'add' | 'edit' | 'detail') === 'detail';
|
const isDetail = (type as 'add' | 'edit' | 'detail') === 'detail';
|
||||||
const availableStock = getAvailableStock(
|
const availableStock = getAvailableStock(stock.product_warehouse_id);
|
||||||
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(
|
const { pendingQty } = getStockPendingInfo(stock.product_warehouse_id);
|
||||||
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(availableStock)} | pending:{' '}
|
(tersedia: {formatNumber(requestedUsage)} | pending:{' '}
|
||||||
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
||||||
pakai: {formatNumber(requestedUsage)})
|
pakai: {formatNumber(requestedUsage + pendingQty)})
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1609,10 +1494,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.value !== 0
|
s.product_warehouse_id !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((s) => s.product_warehouse_id?.value) || [];
|
.map((s) => s.product_warehouse_id) || [];
|
||||||
|
|
||||||
return unifiedStockProducts.filter(
|
return unifiedStockProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1629,10 +1514,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.value !== 0
|
d.product_warehouse_id !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((d) => d.product_warehouse_id?.value) || [];
|
.map((d) => d.product_warehouse_id) || [];
|
||||||
|
|
||||||
return depletionProducts.filter(
|
return depletionProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1649,10 +1534,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.value !== 0
|
e.product_warehouse_id !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((e) => e.product_warehouse_id?.value) || [];
|
.map((e) => e.product_warehouse_id) || [];
|
||||||
|
|
||||||
return eggProducts.filter(
|
return eggProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1698,9 +1583,7 @@ 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] instanceof Object
|
? (errorField[column] as string)
|
||||||
? (errorField[column] as OptionType)?.label
|
|
||||||
: (errorField[column] as string)
|
|
||||||
: '',
|
: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1731,14 +1614,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: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1746,7 +1629,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: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1795,14 +1678,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: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1810,7 +1693,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: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1848,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: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1863,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: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -2076,7 +1959,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newStocks = [
|
const newStocks = [
|
||||||
...(formik.values.stocks || []),
|
...(formik.values.stocks || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2108,7 +1991,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newDepletions = [
|
const newDepletions = [
|
||||||
...(formik.values.depletions || []),
|
...(formik.values.depletions || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2142,7 +2025,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newEggs = [
|
const newEggs = [
|
||||||
...((formik.values as RecordingLayingFormValues).eggs || []),
|
...((formik.values as RecordingLayingFormValues).eggs || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2185,7 +2068,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: null, qty: '' }]);
|
setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
||||||
@@ -2907,15 +2790,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
key={`stock-product-${idx}-${stock.product_warehouse_id?.value}`}
|
key={`stock-product-${idx}-${stock.product_warehouse_id}`}
|
||||||
value={stock.product_warehouse_id}
|
value={
|
||||||
onInputChange={setStockInputValue}
|
unifiedStockProducts.find(
|
||||||
|
(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
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableStockProductOptions(idx)}
|
options={getAvailableStockProductOptions(idx)}
|
||||||
@@ -2951,9 +2839,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
}
|
}
|
||||||
isClearable={type !== 'detail'}
|
isClearable={type !== 'detail'}
|
||||||
inputPrefix={
|
inputPrefix={
|
||||||
stock.product_warehouse_id?.value
|
stock.product_warehouse_id
|
||||||
? getProductFlagBadgeAdornment(
|
? getProductFlagBadgeAdornment(
|
||||||
stock.product_warehouse_id.value
|
stock.product_warehouse_id
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -2989,7 +2877,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
inputSuffix={
|
inputSuffix={
|
||||||
stock.product_warehouse_id
|
stock.product_warehouse_id
|
||||||
? getProductUomSuffix(
|
? getProductUomSuffix(
|
||||||
stock.product_warehouse_id.value,
|
stock.product_warehouse_id,
|
||||||
'stock'
|
'stock'
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
@@ -3182,13 +3070,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
value={depletion.product_warehouse_id}
|
value={
|
||||||
|
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
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableDepletionProductOptions(idx)}
|
options={getAvailableDepletionProductOptions(idx)}
|
||||||
@@ -3251,7 +3145,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
inputSuffix={
|
inputSuffix={
|
||||||
depletion.product_warehouse_id
|
depletion.product_warehouse_id
|
||||||
? getProductUomSuffix(
|
? getProductUomSuffix(
|
||||||
depletion.product_warehouse_id.value,
|
depletion.product_warehouse_id,
|
||||||
'depletion'
|
'depletion'
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
@@ -3429,13 +3323,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
value={egg.product_warehouse_id}
|
value={
|
||||||
|
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
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableEggProductOptions(idx)}
|
options={getAvailableEggProductOptions(idx)}
|
||||||
|
|||||||
+23
-35
@@ -40,9 +40,6 @@ 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]
|
||||||
@@ -58,9 +55,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
const detailModal = useModal();
|
const detailModal = useModal();
|
||||||
|
|
||||||
const maxSourceQuantity =
|
const totalEnteredChickenForTransfer =
|
||||||
transferToLaying?.sources.reduce(
|
transferToLaying?.sources.reduce(
|
||||||
(acc, item) => acc + Number(item.product_warehouse.quantity),
|
(acc, item) => acc + Number(item.qty),
|
||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
@@ -70,9 +67,8 @@ const TransferToLayingDetailModal = () => {
|
|||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
|
|
||||||
const totalAvailableChickenForTransfer =
|
const totalAvailableChickenForTransfer =
|
||||||
maxSourceQuantity - totalTransferedChicken;
|
totalEnteredChickenForTransfer - totalTransferedChicken;
|
||||||
|
|
||||||
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
||||||
if (shouldPushToRoute) {
|
if (shouldPushToRoute) {
|
||||||
@@ -165,34 +161,11 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
<span className='w-full py-2 text-xs font-semibold'>
|
||||||
<span className='text-nowrap'>
|
Kandang Asal{' '}
|
||||||
Kandang Asal{' '}
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
<span className='text-error'> *</span>
|
||||||
<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 && (
|
||||||
@@ -252,6 +225,21 @@ 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 && (
|
||||||
@@ -316,7 +304,7 @@ const TransferToLayingDetailModal = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
errorMessage={
|
errorMessage={
|
||||||
totalAvailableChickenForTransfer < 0
|
totalAvailableChickenForTransfer < 0
|
||||||
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
|
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+21
-10
@@ -13,6 +13,7 @@ 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,
|
||||||
@@ -20,14 +21,12 @@ import {
|
|||||||
|
|
||||||
interface TransferToLayingFilterModal {
|
interface TransferToLayingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
initialValues?: Partial<TransferToLayingFilterValues>;
|
onSubmit?: (values: TransferToLayingFilter) => void;
|
||||||
onSubmit?: (values: TransferToLayingFilterValues) => void;
|
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransferToLayingFilterModal = ({
|
const TransferToLayingFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
initialValues: initialValuesProp,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: TransferToLayingFilterModal) => {
|
}: TransferToLayingFilterModal) => {
|
||||||
@@ -87,16 +86,28 @@ const TransferToLayingFilterModal = ({
|
|||||||
|
|
||||||
const formik = useFormik<TransferToLayingFilterValues>({
|
const formik = useFormik<TransferToLayingFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: initialValuesProp?.startDate ?? '',
|
startDate: '',
|
||||||
endDate: initialValuesProp?.endDate ?? '',
|
endDate: '',
|
||||||
flockSource: initialValuesProp?.flockSource ?? [],
|
flockSource: [],
|
||||||
flockDestination: initialValuesProp?.flockDestination ?? [],
|
flockDestination: [],
|
||||||
status: initialValuesProp?.status ?? [],
|
status: [],
|
||||||
},
|
},
|
||||||
enableReinitialize: true,
|
|
||||||
validationSchema: TransferToLayingFilterSchema,
|
validationSchema: TransferToLayingFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
onSubmit?.(values);
|
const formattedValues = {
|
||||||
|
...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,8 +223,6 @@ 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<
|
||||||
@@ -457,13 +455,13 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(flockSourceRawData)) {
|
if (isResponseSuccess(flockSourceRawData)) {
|
||||||
const currentSelectedFlockSourceRawData = flockSourceRawData.data.find(
|
const selectedFlockSourceRawData = flockSourceRawData.data.find(
|
||||||
(item) => item.id === formik.values.flockSource?.value
|
(item) => item.id === formik.values.flockSource?.value
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedFlockSourceRawData(currentSelectedFlockSourceRawData);
|
setSelectedFlockSourceRawData(selectedFlockSourceRawData);
|
||||||
}
|
}
|
||||||
}, [flockSourceRawData, formikFlockSource]);
|
}, [flockSourceRawData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
||||||
@@ -627,7 +625,6 @@ 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}
|
||||||
@@ -640,14 +637,13 @@ 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>
|
||||||
|
|
||||||
@@ -822,33 +818,11 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
<span className='w-full py-2 text-xs font-semibold'>
|
||||||
<span className='text-nowrap'>
|
Kandang Asal{' '}
|
||||||
Kandang Asal{' '}
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
<span
|
<span className='text-error'> *</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 && (
|
||||||
@@ -928,6 +902,23 @@ 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, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, 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,9 +26,10 @@ 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 { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
import {
|
||||||
import { TransferToLayingFilterValues } from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter';
|
TransferToLaying,
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
TransferToLayingFilter,
|
||||||
|
} 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';
|
||||||
@@ -141,8 +142,6 @@ const TransferToLayingsTable = () => {
|
|||||||
status: '',
|
status: '',
|
||||||
filter_by: '',
|
filter_by: '',
|
||||||
sort_by: '',
|
sort_by: '',
|
||||||
flockSourceNames: '',
|
|
||||||
flockDestinationNames: '',
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -155,9 +154,6 @@ 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 {
|
||||||
@@ -435,84 +431,12 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_FILTER_OPTIONS = [
|
const filterSubmitHandler = (values: TransferToLayingFilter) => {
|
||||||
{ value: 'PENDING', label: 'Pengajuan' },
|
updateFilter('startDate', values.startDate);
|
||||||
{ value: 'APPROVED', label: 'Disetujui' },
|
updateFilter('endDate', values.endDate);
|
||||||
{ value: 'REJECTED', label: 'Ditolak' },
|
updateFilter('flockSource', values.flockSource.join(','));
|
||||||
];
|
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 = () => {
|
||||||
@@ -521,8 +445,6 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('flockSource', '');
|
updateFilter('flockSource', '');
|
||||||
updateFilter('flockDestination', '');
|
updateFilter('flockDestination', '');
|
||||||
updateFilter('status', '');
|
updateFilter('status', '');
|
||||||
updateFilter('flockSourceNames', '');
|
|
||||||
updateFilter('flockDestinationNames', '');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
const exportToExcelHandler = async () => {
|
||||||
@@ -636,8 +558,6 @@ 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}
|
||||||
@@ -750,7 +670,6 @@ 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, useMemo, useCallback } from 'react';
|
import { RefObject, useState, useEffect } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -9,61 +9,31 @@ 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 SelectInputRadio from '@/components/input/SelectInputRadio';
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
const filterByOptions: OptionType<string>[] = [
|
|
||||||
{ value: 'po_date', label: 'Tanggal PO' },
|
|
||||||
{ value: 'received_date', label: 'Tanggal Terima' },
|
|
||||||
{ value: 'due_date', label: 'Tanggal Jatuh Tempo' },
|
|
||||||
{ value: 'created_at', label: 'Tanggal Dibuat' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface PurchaseFilterModalProps {
|
interface PurchaseFilterModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
initialValues?: {
|
|
||||||
poDate: string;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
filterBy: OptionType<string> | undefined;
|
|
||||||
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 = useCallback(() => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
}, [ref]);
|
};
|
||||||
|
|
||||||
// ===== DATE ERROR STATE =====
|
// ===== DATE ERROR STATE =====
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
|
||||||
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(() => {
|
||||||
@@ -103,140 +73,32 @@ 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;
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
filterBy: OptionType<string> | undefined;
|
|
||||||
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;
|
|
||||||
}>({
|
}>({
|
||||||
// enableReinitialize: true,
|
initialValues: {
|
||||||
initialValues: initialValues || {
|
|
||||||
poDate: '',
|
poDate: '',
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
filterBy: undefined,
|
|
||||||
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
|
||||||
) => {
|
) => {
|
||||||
@@ -247,86 +109,6 @@ const PurchaseFilterModal = ({
|
|||||||
formik.setFieldValue('status', val);
|
formik.setFieldValue('status', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formikResetHandler = useCallback(() => {
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
resetForm({
|
|
||||||
values: {
|
|
||||||
poDate: '',
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
filterBy: undefined,
|
|
||||||
category: [],
|
|
||||||
status: [],
|
|
||||||
supplier: null,
|
|
||||||
area: null,
|
|
||||||
location: null,
|
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setSelectedAreaId('');
|
|
||||||
setSelectedLocationId('');
|
|
||||||
onReset?.();
|
|
||||||
closeModalHandler();
|
|
||||||
}, [resetForm, onReset, closeModalHandler, dateErrorShown]);
|
|
||||||
|
|
||||||
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 sebelum tanggal mulai', {
|
|
||||||
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 sebelum tanggal mulai', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formikSubmitHandler = useCallback(async () => {
|
|
||||||
await submitForm();
|
|
||||||
}, [submitForm]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -336,7 +118,7 @@ const PurchaseFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formikResetHandler}
|
onReset={formik.handleReset}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -350,9 +132,7 @@ const PurchaseFilterModal = ({
|
|||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={() => {
|
onClick={closeModalHandler}
|
||||||
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} />
|
||||||
@@ -362,44 +142,6 @@ const PurchaseFilterModal = ({
|
|||||||
{/* 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'>
|
<div className='flex flex-col'>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
label='PO Date'
|
label='PO Date'
|
||||||
name='poDate'
|
name='poDate'
|
||||||
@@ -430,108 +172,6 @@ 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>
|
||||||
|
|
||||||
@@ -547,9 +187,7 @@ const PurchaseFilterModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='submit'
|
||||||
onClick={formikSubmitHandler}
|
|
||||||
disabled={hasDateError}
|
|
||||||
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,22 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import {
|
import {
|
||||||
CellContext,
|
ChangeEventHandler,
|
||||||
ColumnDef,
|
useCallback,
|
||||||
SortingState,
|
useEffect,
|
||||||
Updater,
|
useMemo,
|
||||||
} from '@tanstack/react-table';
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { CellContext, ColumnDef, SortingState } 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 Modal, { useModal } from '@/components/Modal';
|
import { 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';
|
||||||
@@ -25,40 +28,17 @@ 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, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
import { 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;
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
filter_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> = {
|
||||||
@@ -167,100 +147,42 @@ 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<PurchaseTableFilters>({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
sort_by: '',
|
|
||||||
order_by: '',
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
filter_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',
|
|
||||||
start_date: 'start_date',
|
|
||||||
end_date: 'end_date',
|
|
||||||
filter_by: 'filter_by',
|
|
||||||
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 {
|
||||||
@@ -272,10 +194,36 @@ 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;
|
||||||
@@ -291,91 +239,37 @@ const PurchaseTable = () => {
|
|||||||
return (
|
return (
|
||||||
<ul className='list-disc pl-4'>
|
<ul className='list-disc pl-4'>
|
||||||
{poExpedition.map((exp, index) => {
|
{poExpedition.map((exp, index) => {
|
||||||
return (
|
const expenseId = expenseMap.get(exp.refrence);
|
||||||
<li key={index}>
|
if (expenseId) {
|
||||||
<Link
|
return (
|
||||||
href={`/expense/detail/?expenseId=${exp.id}`}
|
<li key={index}>
|
||||||
className='p-0 h-auto text-primary underline'
|
<Link
|
||||||
>
|
href={`/expense/detail/?expenseId=${expenseId}`}
|
||||||
{exp.refrence}
|
className='p-0 h-auto text-primary underline'
|
||||||
</Link>
|
>
|
||||||
</li>
|
{exp.refrence}
|
||||||
);
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <li key={index}>{exp.refrence}</li>;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'requester_name',
|
|
||||||
header: 'Nama Pengaju',
|
|
||||||
cell: (props) => props.row.original.requester_name || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'po_date',
|
|
||||||
header: 'Tgl. PO',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.po_date
|
|
||||||
? 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',
|
|
||||||
header: 'Jatuh Tempo',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.due_date
|
|
||||||
? formatDate(props.row.original.due_date, 'DD MMM YYYY')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aging',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const purchase = props.row.original;
|
|
||||||
if (!purchase.po_date) return '-';
|
|
||||||
const poDate = new Date(purchase.po_date);
|
|
||||||
const today = new Date();
|
|
||||||
const diffTime = Math.abs(today.getTime() - poDate.getTime());
|
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
||||||
return `${diffDays} hari`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'supplier',
|
accessorKey: 'supplier',
|
||||||
header: 'Vendor',
|
header: 'Vendor',
|
||||||
cell: (props) => props.row.original.supplier.name,
|
cell: (props) => props.row.original.supplier.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'location',
|
accessorKey: 'requester_name',
|
||||||
header: 'Lokasi',
|
header: 'Nama Pengaju',
|
||||||
cell: (props) => props.row.original.location?.name || '-',
|
cell: (props) => props.row.original.requester_name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'warehouse',
|
accessorKey: 'products.name',
|
||||||
header: 'Gudang',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>{item.warehouse?.name ?? '-'}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'products',
|
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const products = props.row.original.products;
|
const products = props.row.original.products;
|
||||||
@@ -390,162 +284,39 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'total_qty',
|
accessorKey: 'location.name',
|
||||||
header: 'Kuantitas',
|
header: 'Lokasi',
|
||||||
enableSorting: false,
|
cell: (props) => props.row.original.location?.name || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'po_date',
|
||||||
|
header: 'Tgl. PO',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.po_date
|
||||||
|
? formatDate(props.row.original.po_date, 'DD MMM YYYY')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'due_date',
|
||||||
|
header: 'Jatuh Tempo',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.due_date
|
||||||
|
? formatDate(props.row.original.due_date, 'DD MMM YYYY')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Aging',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const items = props.row.original.items;
|
const purchase = props.row.original;
|
||||||
if (!items || items.length === 0) return '-';
|
if (!purchase.po_date) return '-';
|
||||||
return (
|
const poDate = new Date(purchase.po_date);
|
||||||
<ul className='list-disc pl-4'>
|
const today = new Date();
|
||||||
{items.map((item, index) => (
|
const diffTime = Math.abs(today.getTime() - poDate.getTime());
|
||||||
<li key={index}>{formatNumber(item.total_qty ?? 0)}</li>
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
))}
|
return `${diffDays} hari`;
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'uom',
|
|
||||||
header: 'Satuan',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>{item.product?.uom?.name ?? '-'}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'price',
|
|
||||||
header: 'Harga',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>{formatCurrency(item.price ?? 0)}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'total_price',
|
|
||||||
header: 'Total Harga',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>{formatCurrency(item.total_price ?? 0)}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'products_total',
|
|
||||||
header: 'Total Harga Produk',
|
|
||||||
cell: (props) => formatCurrency(props.row.original.products_total ?? 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'expedition_vendor',
|
|
||||||
header: 'Vendor Ekspedisi',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>{item.expedition_vendor?.name ?? '-'}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'expedition_qty',
|
|
||||||
header: 'Qty Ekspedisi',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
{item.expedition_qty != null
|
|
||||||
? formatNumber(item.expedition_qty)
|
|
||||||
: '-'}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transport_per_item',
|
|
||||||
header: 'Harga Ekspedisi',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
{item.transport_per_item != null
|
|
||||||
? formatCurrency(item.transport_per_item)
|
|
||||||
: '-'}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'item_expedition_total',
|
|
||||||
header: 'Total Ekspedisi',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
|
||||||
const items = props.row.original.items;
|
|
||||||
if (!items || items.length === 0) return '-';
|
|
||||||
return (
|
|
||||||
<ul className='list-disc pl-4'>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
{item.expedition_total != null
|
|
||||||
? formatCurrency(item.expedition_total)
|
|
||||||
: '-'}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'expedition_total',
|
|
||||||
header: 'Total Ekspedisi Semua Produk',
|
|
||||||
cell: (props) => formatCurrency(props.row.original.expedition_total ?? 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'grand_total_all',
|
|
||||||
header: 'Grand Total All',
|
|
||||||
cell: (props) => formatCurrency(props.row.original.grand_total_all ?? 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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;
|
||||||
@@ -590,19 +361,6 @@ const PurchaseTable = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'notes',
|
|
||||||
header: 'Notes',
|
|
||||||
cell: (props) => props.row.original.notes || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => {
|
||||||
@@ -634,17 +392,10 @@ const PurchaseTable = () => {
|
|||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deleteResponse = await PurchaseApi.delete(
|
await PurchaseApi.delete(selectedPurchase?.id as number);
|
||||||
selectedPurchase?.id as number
|
refreshPurchaseRequests();
|
||||||
);
|
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!');
|
||||||
}
|
}
|
||||||
@@ -652,214 +403,34 @@ 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]
|
[updateFilter, setSearchValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterSubmitHandler = (values: PurchaseFilter) => {
|
const filterSubmitHandler = (values: PurchaseFilter) => {
|
||||||
setFilters({
|
updateFilter('po_date', values.poDate);
|
||||||
start_date: values.start_date || '',
|
updateFilter('product_category_id', values.category.join(','));
|
||||||
end_date: values.end_date || '',
|
updateFilter('approval_status', values.status.join(','));
|
||||||
filter_by: values.filterBy?.value || '',
|
|
||||||
po_date: values.poDate,
|
|
||||||
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 = () => {
|
||||||
setFilters({
|
updateFilter('po_date', '');
|
||||||
start_date: '',
|
updateFilter('product_category_id', '');
|
||||||
end_date: '',
|
updateFilter('approval_status', '');
|
||||||
filter_by: '',
|
|
||||||
po_date: '',
|
|
||||||
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 filterByLabelMap: Record<string, string> = {
|
|
||||||
po_date: 'Tanggal PO',
|
|
||||||
received_date: 'Tanggal Terima',
|
|
||||||
due_date: 'Tanggal Jatuh Tempo',
|
|
||||||
created_at: 'Tanggal Dibuat',
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
filterBy: tableFilterState.filter_by
|
|
||||||
? {
|
|
||||||
value: tableFilterState.filter_by,
|
|
||||||
label:
|
|
||||||
filterByLabelMap[tableFilterState.filter_by] ||
|
|
||||||
tableFilterState.filter_by,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
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'>
|
||||||
@@ -906,73 +477,11 @@ 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={[['start_date', 'end_date']]}
|
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>
|
||||||
|
|
||||||
@@ -1020,8 +529,7 @@ const PurchaseTable = () => {
|
|||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={handleSortingChange}
|
setSorting={setSorting}
|
||||||
manualSorting
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
headerColumnClassName: 'text-nowrap',
|
||||||
@@ -1035,7 +543,6 @@ const PurchaseTable = () => {
|
|||||||
|
|
||||||
<PurchaseFilterModal
|
<PurchaseFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
initialValues={purchaseFilterInitialValues}
|
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
/>
|
/>
|
||||||
@@ -1055,76 +562,6 @@ 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,6 +55,7 @@ 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[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -162,7 +163,6 @@ 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,8 +26,6 @@ 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,
|
||||||
@@ -98,7 +96,6 @@ 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();
|
||||||
|
|
||||||
@@ -108,9 +105,6 @@ 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)
|
||||||
@@ -218,8 +212,6 @@ const PurchaseOrderDetail = ({
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
setManagerApprovalNotes('');
|
|
||||||
setManagerApprovalPoDate('');
|
|
||||||
confirmationModalWithNotes.openModal();
|
confirmationModalWithNotes.openModal();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
@@ -422,50 +414,17 @@ 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 () => {
|
const managerApprovalHandler = async (notes: string) => {
|
||||||
const payload: CreateManagerApprovalRequestPayload = {
|
const payload: CreateManagerApprovalRequestPayload = {
|
||||||
action: 'APPROVED',
|
action: 'APPROVED',
|
||||||
notes: managerApprovalNotes || null,
|
notes: notes || null,
|
||||||
po_date: managerApprovalPoDate || null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await createManagerApprovalHandler(payload);
|
await createManagerApprovalHandler(payload);
|
||||||
await refreshApprovals();
|
await refreshApprovals();
|
||||||
await refetchData?.();
|
await refetchData?.();
|
||||||
setManagerApprovalNotes('');
|
setApprovalNotes('');
|
||||||
setManagerApprovalPoDate('');
|
|
||||||
confirmationModalWithNotes.closeModal();
|
confirmationModalWithNotes.closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -870,41 +829,6 @@ 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>
|
||||||
@@ -1092,79 +1016,27 @@ const PurchaseOrderDetail = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Manager Approval Modal */}
|
{/* Confirmation Modal with Notes */}
|
||||||
<Modal
|
<ConfirmationModalWithNotes
|
||||||
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
|
||||||
className={{
|
primaryButton={{
|
||||||
modalBox: 'max-w-lg rounded-lg p-0',
|
text: 'Ya, Lanjutkan',
|
||||||
|
color: 'success',
|
||||||
|
onClick: managerApprovalHandler,
|
||||||
}}
|
}}
|
||||||
>
|
secondaryButton={{
|
||||||
<div className='flex flex-col'>
|
text: 'Batal',
|
||||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
onClick: () => {
|
||||||
<h4 className='text-sm font-semibold text-base-content'>
|
setApprovalNotes('');
|
||||||
Konfirmasi Approval Manager
|
confirmationModalWithNotes.closeModal();
|
||||||
</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
|
||||||
@@ -1240,66 +1112,6 @@ 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}
|
||||||
|
|||||||
@@ -1,38 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { useState } from 'react';
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import ReportExpenseTab from '@/components/pages/report/expense/tab/ReportExpenseTab';
|
import ReportExpenseTab from './tab/ReportExpenseTab';
|
||||||
import ReportDepreciationTab from '@/components/pages/report/expense/tab/ReportDepreciationTab';
|
|
||||||
|
|
||||||
const VALID_TAB_IDS = ['operational-expense', 'depreciation'];
|
|
||||||
|
|
||||||
const ReportExpenseTabs = () => {
|
const ReportExpenseTabs = () => {
|
||||||
const router = useRouter();
|
const [activeTabId, setActiveTabId] = useState<string>('1');
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const tabParam = searchParams.get('tab') ?? 'operational-expense';
|
|
||||||
const activeTabId = VALID_TAB_IDS.includes(tabParam)
|
|
||||||
? tabParam
|
|
||||||
: 'operational-expense';
|
|
||||||
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: 'operational-expense',
|
id: '1',
|
||||||
label: 'Laporan Biaya Operasional',
|
label: 'Laporan Biaya Operasional',
|
||||||
content: <ReportExpenseTab tabId={'operational-expense'} />,
|
content: <ReportExpenseTab tabId={'1'} />,
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'depreciation',
|
|
||||||
label: 'Laporan Depresiasi',
|
|
||||||
content: <ReportDepreciationTab tabId={'depreciation'} />,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -42,7 +24,7 @@ const ReportExpenseTabs = () => {
|
|||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
variant='boxed'
|
variant='boxed'
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={setActiveTabId}
|
||||||
className={{
|
className={{
|
||||||
tabHeaderWrapper:
|
tabHeaderWrapper:
|
||||||
'justify-between items-center p-3 border-b border-base-content/10',
|
'justify-between items-center p-3 border-b border-base-content/10',
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
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 ReportSkeletonColumn<TData extends object> =
|
type ReportExpenseColumn =
|
||||||
| ColumnDef<TData>
|
| ColumnDef<ReportExpense>
|
||||||
| {
|
| {
|
||||||
header: string;
|
header: string;
|
||||||
columns: Array<{
|
columns: Array<{
|
||||||
header: string;
|
header: string;
|
||||||
accessorKey?: string;
|
accessorKey?: string;
|
||||||
cell?: (props: { row: { original: TData } }) => React.ReactNode;
|
cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReportExpenseSkeleton = <TData extends object>({
|
const ReportExpenseSkeleton = ({
|
||||||
columns,
|
columns,
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
}: {
|
}: {
|
||||||
columns: ReportSkeletonColumn<TData>[];
|
columns: ReportExpenseColumn[];
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { RefObject } 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?: OptionType<string>;
|
|
||||||
location?: OptionType<string>;
|
|
||||||
projectFlock?: OptionType<string>;
|
|
||||||
period: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ReportDepreciationFilterSchema = yup.object({
|
|
||||||
area: yup.mixed<OptionType<string>>().optional(),
|
|
||||||
location: yup.mixed<OptionType<string>>().optional(),
|
|
||||||
projectFlock: yup.mixed<OptionType<string>>().optional(),
|
|
||||||
period: yup.string().nullable().required('Periode wajib dipilih'),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ReportDepreciationFilterModalProps {
|
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
|
||||||
initialValues?: Partial<ReportDepreciationFilterValues>;
|
|
||||||
onSubmit?: (values: Partial<ReportDepreciationFilterValues>) => void;
|
|
||||||
onReset?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultInitialValues: (
|
|
||||||
initialValues?: Partial<ReportDepreciationFilterValues>
|
|
||||||
) => ReportDepreciationFilterValues = (initialValues) => ({
|
|
||||||
area: undefined,
|
|
||||||
location: undefined,
|
|
||||||
projectFlock: undefined,
|
|
||||||
period: initialValues?.period ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ReportDepreciationFilterModal = ({
|
|
||||||
ref,
|
|
||||||
initialValues,
|
|
||||||
onSubmit,
|
|
||||||
onReset,
|
|
||||||
}: ReportDepreciationFilterModalProps) => {
|
|
||||||
const closeModalHandler = () => {
|
|
||||||
ref.current?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const formik = useFormik<ReportDepreciationFilterValues>({
|
|
||||||
initialValues: { ...defaultInitialValues(initialValues), ...initialValues },
|
|
||||||
validationSchema: ReportDepreciationFilterSchema,
|
|
||||||
onSubmit: async (values) => {
|
|
||||||
onSubmit?.(values);
|
|
||||||
closeModalHandler();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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: String(formik.values.area?.value ?? ''),
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setProjectFlockInputValue,
|
|
||||||
options: projectFlockOptions,
|
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
|
||||||
loadMore: loadMoreProjectFlocks,
|
|
||||||
} = useSelect<ProjectFlock>(
|
|
||||||
ProjectFlockApi.basePath,
|
|
||||||
'id',
|
|
||||||
'flock_name',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
location_id: String(formik.values.location?.value ?? ''),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const formikResetHandler = () => {
|
|
||||||
onReset?.();
|
|
||||||
formik.resetForm({ values: defaultInitialValues(initialValues) });
|
|
||||||
closeModalHandler();
|
|
||||||
};
|
|
||||||
|
|
||||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const area =
|
|
||||||
val && !Array.isArray(val) ? (val as OptionType<string>) : undefined;
|
|
||||||
formik.setFieldValue('area', area);
|
|
||||||
formik.setFieldValue('location', undefined);
|
|
||||||
formik.setFieldValue('projectFlock', undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const location =
|
|
||||||
val && !Array.isArray(val) ? (val as OptionType<string>) : undefined;
|
|
||||||
formik.setFieldValue('location', location);
|
|
||||||
formik.setFieldValue('projectFlock', undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const projectFlock =
|
|
||||||
val && !Array.isArray(val) ? (val as OptionType<string>) : undefined;
|
|
||||||
formik.setFieldValue('projectFlock', projectFlock);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={ref}
|
|
||||||
className={{
|
|
||||||
modalBox: 'p-0 rounded-xl xl:max-w-4/12 max-w-sm',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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-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={formik.values.area ?? null}
|
|
||||||
onChange={areaChangeHandler}
|
|
||||||
onInputChange={setAreaInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreAreas}
|
|
||||||
isLoading={isLoadingAreaOptions}
|
|
||||||
isClearable
|
|
||||||
isSearchable={true}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Lokasi'
|
|
||||||
placeholder='Pilih Lokasi'
|
|
||||||
options={locationOptions}
|
|
||||||
value={formik.values.location ?? null}
|
|
||||||
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={formik.values.projectFlock ?? null}
|
|
||||||
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;
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
'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 { OptionType } from '@/components/input/SelectInput';
|
|
||||||
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<{
|
|
||||||
area?: OptionType<string>;
|
|
||||||
location?: OptionType<string>;
|
|
||||||
projectFlock?: OptionType<string>;
|
|
||||||
period: string;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
area: undefined,
|
|
||||||
location: undefined,
|
|
||||||
projectFlock: undefined,
|
|
||||||
period: formatDate(Date.now(), 'YYYY-MM-DD'),
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
pageSize: 'limit',
|
|
||||||
area: 'area_id',
|
|
||||||
location: 'location_id',
|
|
||||||
projectFlock: 'project_flock_id',
|
|
||||||
period: 'period',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'report-depreciation-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
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', values.area, true);
|
|
||||||
updateFilter('location', values.location, true);
|
|
||||||
updateFilter('projectFlock', values.projectFlock, true);
|
|
||||||
updateFilter(
|
|
||||||
'period',
|
|
||||||
values.period ? formatDate(values.period, 'YYYY-MM-DD') : '',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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/expense-report';
|
import { ReportExpenseApi } from '@/services/api/report';
|
||||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
import { 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, SortingState, Updater } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { httpClient } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
@@ -73,25 +73,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
// ===== SORTING STATE =====
|
|
||||||
const [sortBy, setSortBy] = useState('');
|
|
||||||
const [orderBy, setOrderBy] = useState('');
|
|
||||||
|
|
||||||
const sorting: SortingState = sortBy
|
|
||||||
? [{ id: sortBy, desc: orderBy === 'desc' }]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
||||||
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
||||||
if (next.length > 0) {
|
|
||||||
setSortBy(next[0].id);
|
|
||||||
setOrderBy(next[0].desc ? 'desc' : 'asc');
|
|
||||||
} else {
|
|
||||||
setSortBy('');
|
|
||||||
setOrderBy('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterModalOpenRef = useRef(() => {});
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
@@ -145,49 +126,8 @@ 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 =====
|
||||||
@@ -249,49 +189,26 @@ 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 queryString = buildReportExpenseQueryString({
|
const params = new URLSearchParams();
|
||||||
page: String(page),
|
if (filterParams.location_id)
|
||||||
limit: String(pageSize),
|
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);
|
||||||
|
params.append('page', String(page));
|
||||||
|
params.append('limit', String(pageSize));
|
||||||
|
|
||||||
return [`${ReportExpenseApi.basePath}?${queryString}`];
|
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
|
||||||
},
|
},
|
||||||
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
||||||
);
|
);
|
||||||
@@ -316,31 +233,47 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const reportExpenseExport = useCallback(async (): Promise<
|
const reportExpenseExport = useCallback(async (): Promise<
|
||||||
ReportExpense[] | null
|
ReportExpense[] | null
|
||||||
> => {
|
> => {
|
||||||
const queryString = buildReportExpenseQueryString({
|
const params = new URLSearchParams();
|
||||||
page: '1',
|
if (filterParams.location_id)
|
||||||
limit: '100',
|
params.append('location_id', filterParams.location_id);
|
||||||
});
|
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}?${queryString}`
|
`${ReportExpenseApi.basePath}?${params.toString()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
}, [buildReportExpenseQueryString]);
|
}, [filterParams]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
await ReportExpenseApi.exportToExcel(buildReportExpenseQueryString());
|
const allDataForExport = await reportExpenseExport();
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
if (!allDataForExport || allDataForExport.length === 0) {
|
||||||
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
);
|
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);
|
||||||
}
|
}
|
||||||
}, [buildReportExpenseQueryString]);
|
}, [reportExpenseExport]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -464,23 +397,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: 'No',
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. PO',
|
header: 'No. PO',
|
||||||
accessorKey: 'po_number',
|
accessorKey: 'po_number',
|
||||||
enableSorting: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. Referensi',
|
header: 'No. Referensi',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
enableSorting: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Realisasi',
|
header: 'Tanggal Realisasi',
|
||||||
accessorKey: 'realization_date',
|
accessorKey: 'realization_date',
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -488,7 +417,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Tanggal Transaksi',
|
header: 'Tanggal Transaksi',
|
||||||
accessorKey: 'transaction_date',
|
accessorKey: 'transaction_date',
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -496,30 +424,21 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
enableSorting: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
accessorKey: 'product',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Supplier',
|
header: 'Supplier',
|
||||||
accessorKey: 'supplier',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.supplier?.name,
|
accessorFn: (row) => row.supplier?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
accessorKey: 'location',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.kandang?.location?.name,
|
accessorFn: (row) => row.kandang?.location?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Kandang',
|
header: 'Kandang',
|
||||||
accessorKey: 'kandang',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.kandang?.name,
|
accessorFn: (row) => row.kandang?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -527,19 +446,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
accessorKey: 'qty_pengajuan',
|
id: 'qty_pengajuan',
|
||||||
|
accessorFn: (row) => row.pengajuan?.qty,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
accessorKey: 'price_pengajuan',
|
id: 'harga_pengajuan',
|
||||||
|
accessorFn: (row) => row.pengajuan?.price,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.pengajuan?.price || 0),
|
formatCurrency(row.original.pengajuan?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
accessorKey: 'total_pengajuan',
|
id: 'total_pengajuan',
|
||||||
|
accessorFn: (row) =>
|
||||||
|
(row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.pengajuan?.qty || 0) *
|
(row.original.pengajuan?.qty || 0) *
|
||||||
@@ -554,19 +477,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
accessorKey: 'qty_realisasi',
|
id: 'qty_realisasi',
|
||||||
|
accessorFn: (row) => row.realisasi?.qty,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
accessorKey: 'price_realisasi',
|
id: 'harga_realisasi',
|
||||||
|
accessorFn: (row) => row.realisasi?.price,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.realisasi?.price || 0),
|
formatCurrency(row.original.realisasi?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
accessorKey: 'total_realisasi',
|
id: 'total_realisasi',
|
||||||
|
accessorFn: (row) =>
|
||||||
|
(row.realisasi?.qty || 0) * (row.realisasi?.price || 0),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.realisasi?.qty || 0) *
|
(row.original.realisasi?.qty || 0) *
|
||||||
@@ -577,7 +504,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'realization_status',
|
|
||||||
header: 'Status Pencairan',
|
header: 'Status Pencairan',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RealizationStatusBadge
|
<RealizationStatusBadge
|
||||||
@@ -586,7 +512,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bop_status',
|
|
||||||
header: 'Status BOP',
|
header: 'Status BOP',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
||||||
@@ -631,9 +556,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
totalItems={meta?.total_results || 0}
|
totalItems={meta?.total_results || 0}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
sorting={sorting}
|
|
||||||
setSorting={handleSortingChange}
|
|
||||||
manualSorting
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-0!',
|
containerClassName: 'w-full mb-0!',
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
|
|||||||
@@ -1,47 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { useState } from 'react';
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab';
|
import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab';
|
||||||
import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab';
|
import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab';
|
||||||
import BalanceMonitoringTab from '@/components/pages/report/finance/tab/BalanceMonitoringTab';
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
|
|
||||||
const VALID_TAB_IDS = [
|
|
||||||
'debt-supplier',
|
|
||||||
'customer-payment',
|
|
||||||
'balance-monitoring',
|
|
||||||
];
|
|
||||||
|
|
||||||
const FinanceTabs = () => {
|
const FinanceTabs = () => {
|
||||||
const router = useRouter();
|
const [activeTabId, setActiveTabId] = useState<string>('1');
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const tabParam = searchParams.get('tab') ?? 'debt-supplier';
|
|
||||||
const activeTabId = VALID_TAB_IDS.includes(tabParam)
|
|
||||||
? tabParam
|
|
||||||
: 'debt-supplier';
|
|
||||||
const tabActions = useTabActionsStore((state) => state.tabActions);
|
const tabActions = useTabActionsStore((state) => state.tabActions);
|
||||||
|
|
||||||
const handleTabChange = (tabId: string) => {
|
|
||||||
router.push(`${pathname}?tab=${tabId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
id: 'debt-supplier',
|
id: '1',
|
||||||
label: 'Rekapitulasi Hutang Ke Supplier',
|
label: 'Rekapitulasi Hutang Ke Supplier',
|
||||||
content: <DebtSupplierTab tabId={'debt-supplier'} />,
|
content: <DebtSupplierTab tabId={'1'} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'customer-payment',
|
id: '2',
|
||||||
label: 'Kontrol Pembayaran Customer',
|
label: 'Kontrol Pembayaran Customer',
|
||||||
content: <CustomerPaymentTab tabId={'customer-payment'} />,
|
content: <CustomerPaymentTab tabId={'2'} />,
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'balance-monitoring',
|
|
||||||
label: 'Monitoring Saldo',
|
|
||||||
content: <BalanceMonitoringTab tabId={'balance-monitoring'} />,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -51,7 +29,7 @@ const FinanceTabs = () => {
|
|||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
variant='boxed'
|
variant='boxed'
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={setActiveTabId}
|
||||||
className={{
|
className={{
|
||||||
tabHeaderWrapper:
|
tabHeaderWrapper:
|
||||||
'justify-between items-center p-3 border-b border-base-content/10',
|
'justify-between items-center p-3 border-b border-base-content/10',
|
||||||
|
|||||||
@@ -1,602 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useFormik } from 'formik';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { FinanceApi } from '@/services/api/report/finance-report';
|
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
|
||||||
import { UserApi } from '@/services/api/user';
|
|
||||||
import { useSelect, OptionType } from '@/components/input/SelectInput';
|
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
|
||||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { BalanceMonitoringRow } from '@/types/api/report/balance-monitoring';
|
|
||||||
import { CustomerPaymentRow } from '@/types/api/report/customer-payment';
|
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
|
||||||
import Button from '@/components/Button';
|
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
|
||||||
|
|
||||||
interface BalanceMonitoringTabProps {
|
|
||||||
tabId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterByOptions: OptionType<string>[] = [
|
|
||||||
{ label: 'Tanggal Penjualan (SO Date)', value: 'sold_at' },
|
|
||||||
{ label: 'Tanggal Realisasi (Delivery Date)', value: 'realized_at' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const BalanceMonitoringTab = ({ tabId }: BalanceMonitoringTabProps) => {
|
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
|
||||||
|
|
||||||
const filterModal = useModal();
|
|
||||||
|
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
|
||||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
|
||||||
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
updateFilter,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
reset: resetFilter,
|
|
||||||
} = useTableFilter<{
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
customers: OptionType<number>[];
|
|
||||||
salesPersons: OptionType<number>[];
|
|
||||||
filterBy?: OptionType<string>;
|
|
||||||
sort_by: string;
|
|
||||||
order_by: string;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
customers: [],
|
|
||||||
salesPersons: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
sort_by: '',
|
|
||||||
order_by: '',
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
start_date: 'start_date',
|
|
||||||
end_date: 'end_date',
|
|
||||||
customers: 'customer_ids',
|
|
||||||
salesPersons: 'sales_ids',
|
|
||||||
filterBy: 'filter_by',
|
|
||||||
sort_by: 'sort_by',
|
|
||||||
order_by: 'sort_order',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'balance-monitoring-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
// const sorting: SortingState = tableFilterState.sort_by
|
|
||||||
// ? [
|
|
||||||
// {
|
|
||||||
// id: tableFilterState.sort_by,
|
|
||||||
// desc: tableFilterState.order_by === 'desc',
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// : [];
|
|
||||||
|
|
||||||
// const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
||||||
// const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
||||||
// if (next.length > 0) {
|
|
||||||
// updateFilter('sort_by', next[0].id, true);
|
|
||||||
// updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
|
|
||||||
// } else {
|
|
||||||
// updateFilter('sort_by', '', true);
|
|
||||||
// updateFilter('order_by', '', true);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const {
|
|
||||||
options: customerOptions,
|
|
||||||
setInputValue: setCustomerInput,
|
|
||||||
isLoadingOptions: isLoadingCustomers,
|
|
||||||
loadMore: loadMoreCustomers,
|
|
||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
|
||||||
|
|
||||||
const {
|
|
||||||
options: salesOptions,
|
|
||||||
setInputValue: setSalesInput,
|
|
||||||
isLoadingOptions: isLoadingSales,
|
|
||||||
loadMore: loadMoreSales,
|
|
||||||
} = useSelect(UserApi.basePath, 'id', 'name', 'search');
|
|
||||||
|
|
||||||
const formik = useFormik({
|
|
||||||
initialValues: {
|
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
customers: tableFilterState.customers,
|
|
||||||
salesPersons: tableFilterState.salesPersons,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
},
|
|
||||||
onSubmit: (values) => {
|
|
||||||
updateFilter('start_date', values.start_date, true);
|
|
||||||
updateFilter('end_date', values.end_date, true);
|
|
||||||
updateFilter('customers', values.customers, true);
|
|
||||||
updateFilter('salesPersons', values.salesPersons, true);
|
|
||||||
updateFilter('filterBy', values.filterBy, true);
|
|
||||||
filterModal.closeModal();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const formikResetHandler = () => {
|
|
||||||
resetFilter();
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
customers: [],
|
|
||||||
salesPersons: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('start_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.end_date) {
|
|
||||||
if (new Date(formik.values.end_date) < new Date(value)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasDateError(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('end_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.start_date) {
|
|
||||||
if (new Date(value) < new Date(formik.values.start_date)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: balanceMonitoringsResponse, isLoading } = useSWR<
|
|
||||||
BaseApiResponse<BalanceMonitoringRow[]>,
|
|
||||||
AxiosError<BaseApiResponse>,
|
|
||||||
SWRHttpKey
|
|
||||||
>(
|
|
||||||
`${FinanceApi.basePath}/balance-monitoring${getTableFilterQueryString()}`,
|
|
||||||
httpClientFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const balanceMonitorings: BalanceMonitoringRow[] = isResponseSuccess(
|
|
||||||
balanceMonitoringsResponse
|
|
||||||
)
|
|
||||||
? ((balanceMonitoringsResponse.data as BalanceMonitoringRow[]) ?? [])
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const meta =
|
|
||||||
isResponseSuccess(balanceMonitoringsResponse) &&
|
|
||||||
balanceMonitoringsResponse.meta
|
|
||||||
? balanceMonitoringsResponse.meta
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Inject tab actions directly — no nested component, no remount cycle
|
|
||||||
useEffect(() => {
|
|
||||||
setTabActions(
|
|
||||||
tabId,
|
|
||||||
<div className='flex flex-row gap-3'>
|
|
||||||
<ButtonFilter
|
|
||||||
values={{
|
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
customers: tableFilterState.customers,
|
|
||||||
salesPersons: tableFilterState.salesPersons,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
}}
|
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
|
||||||
onClick={filterModal.openModal}
|
|
||||||
variant='outline'
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [tabId, setTabActions, tableFilterState, filterModal.openModal]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => clearTabActions(tabId);
|
|
||||||
}, [tabId, clearTabActions]);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
(): ColumnDef<BalanceMonitoringRow>[] => [
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) =>
|
|
||||||
(tableFilterState.page - 1) * tableFilterState.pageSize +
|
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Customer',
|
|
||||||
accessorKey: 'customer.name',
|
|
||||||
enableSorting: true,
|
|
||||||
id: 'customer_name',
|
|
||||||
cell: ({ row }) => row.original.customer.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Saldo Awal',
|
|
||||||
accessorKey: 'saldo_awal',
|
|
||||||
id: 'saldo_awal',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.saldo_awal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penjualan Ayam',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: 'Ekor',
|
|
||||||
accessorKey: 'penjualan_ayam.ekor',
|
|
||||||
id: 'penjualan_ayam_ekor',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_ayam.ekor)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Kg',
|
|
||||||
accessorKey: 'penjualan_ayam.kg',
|
|
||||||
id: 'penjualan_ayam_kg',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_ayam.kg)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nominal',
|
|
||||||
accessorKey: 'penjualan_ayam.nominal',
|
|
||||||
id: 'penjualan_ayam_nominal',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.penjualan_ayam.nominal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penjualan Telur',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: 'Butir',
|
|
||||||
accessorKey: 'penjualan_telur.butir',
|
|
||||||
id: 'penjualan_telur_butir',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_telur.butir)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Kg',
|
|
||||||
accessorKey: 'penjualan_telur.kg',
|
|
||||||
id: 'penjualan_telur_kg',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatNumber(row.original.penjualan_telur.kg)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nominal',
|
|
||||||
accessorKey: 'penjualan_telur.nominal',
|
|
||||||
id: 'penjualan_telur_nominal',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.penjualan_telur.nominal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penjualan Trading',
|
|
||||||
accessorKey: 'penjualan_trading.nominal',
|
|
||||||
id: 'penjualan_trading',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.penjualan_trading.nominal)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Pembayaran',
|
|
||||||
accessorKey: 'pembayaran',
|
|
||||||
id: 'pembayaran',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-right'>
|
|
||||||
{formatCurrency(row.original.pembayaran)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aging',
|
|
||||||
accessorKey: 'aging',
|
|
||||||
id: 'aging',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-center'>
|
|
||||||
{formatNumber(row.original.aging)} hari
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aging Rata-Rata',
|
|
||||||
accessorKey: 'aging_rata_rata',
|
|
||||||
id: 'aging_rata_rata',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className='text-center'>
|
|
||||||
{formatNumber(row.original.aging_rata_rata)} hari
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Saldo Akhir',
|
|
||||||
accessorKey: 'saldo_akhir',
|
|
||||||
id: 'saldo_akhir',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div
|
|
||||||
className={`text-right font-semibold ${row.original.saldo_akhir < 0 ? 'text-error' : ''}`}
|
|
||||||
>
|
|
||||||
{formatCurrency(row.original.saldo_akhir)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[tableFilterState.page, tableFilterState.pageSize]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
|
||||||
{isLoading && (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && balanceMonitorings.length === 0 && (
|
|
||||||
<CustomerSupplierSkeleton
|
|
||||||
columns={columns as unknown as ColumnDef<CustomerPaymentRow>[]}
|
|
||||||
icon={
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:chart-bar'
|
|
||||||
className='text-white'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title='Data Not Yet Available'
|
|
||||||
subtitle='Please change your filters to get the data.'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && balanceMonitorings.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className='w-full overflow-x-auto'>
|
|
||||||
<Table
|
|
||||||
data={balanceMonitorings}
|
|
||||||
columns={columns}
|
|
||||||
pageSize={tableFilterState.pageSize || 10}
|
|
||||||
page={tableFilterState.page || 1}
|
|
||||||
totalItems={meta?.total_results || 0}
|
|
||||||
onPageChange={setPage}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
// sorting={sorting}
|
|
||||||
// setSorting={handleSortingChange}
|
|
||||||
// manualSorting
|
|
||||||
className={{
|
|
||||||
containerClassName: 'w-full mb-0!',
|
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200 text-nowrap',
|
|
||||||
bodyRowClassName:
|
|
||||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Modal */}
|
|
||||||
<Modal
|
|
||||||
ref={filterModal.ref}
|
|
||||||
className={{
|
|
||||||
modal: 'p-0',
|
|
||||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Modal Header */}
|
|
||||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
|
||||||
<div className='flex items-center gap-2 text-primary'>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant='link'
|
|
||||||
onClick={filterModal.closeModal}
|
|
||||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
|
||||||
<div className='p-4 flex flex-col gap-3'>
|
|
||||||
<div>
|
|
||||||
<label className='block text-xs font-semibold text-base-content py-2'>
|
|
||||||
Tanggal
|
|
||||||
</label>
|
|
||||||
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
|
||||||
<DateInput
|
|
||||||
name='start_date'
|
|
||||||
value={formik.values.start_date || ''}
|
|
||||||
onChange={handleStartDateChange}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
isNestedModal
|
|
||||||
/>
|
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
|
||||||
<DateInput
|
|
||||||
name='end_date'
|
|
||||||
value={formik.values.end_date || ''}
|
|
||||||
onChange={handleEndDateChange}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
isNestedModal
|
|
||||||
isError={hasDateError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SelectInputCheckbox
|
|
||||||
label='Customer'
|
|
||||||
placeholder='Pilih Customer'
|
|
||||||
options={customerOptions}
|
|
||||||
value={formik.values.customers}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue('customers', Array.isArray(val) ? val : [])
|
|
||||||
}
|
|
||||||
onInputChange={setCustomerInput}
|
|
||||||
isLoading={isLoadingCustomers}
|
|
||||||
isClearable
|
|
||||||
onMenuScrollToBottom={loadMoreCustomers}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInputCheckbox
|
|
||||||
label='Sales'
|
|
||||||
placeholder='Pilih Sales'
|
|
||||||
options={salesOptions}
|
|
||||||
value={formik.values.salesPersons}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'salesPersons',
|
|
||||||
Array.isArray(val) ? val : []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onInputChange={setSalesInput}
|
|
||||||
isLoading={isLoadingSales}
|
|
||||||
isClearable
|
|
||||||
onMenuScrollToBottom={loadMoreSales}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInputRadio
|
|
||||||
label='Filter Berdasarkan'
|
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
|
||||||
options={filterByOptions}
|
|
||||||
value={formik.values.filterBy ?? null}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'filterBy',
|
|
||||||
!Array.isArray(val) ? (val ?? undefined) : undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isClearable
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modal Footer */}
|
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
|
||||||
<Button
|
|
||||||
type='reset'
|
|
||||||
variant='soft'
|
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
|
||||||
>
|
|
||||||
Reset Filter
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
|
||||||
disabled={hasDateError}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BalanceMonitoringTab;
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import { useSelect, OptionType } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { FinanceApi } from '@/services/api/report/finance-report';
|
import { FinanceApi } from '@/services/api/report/finance-report';
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import {
|
import {
|
||||||
@@ -30,70 +27,55 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataTypeOptions: OptionType<string>[] = [
|
interface FilterParams {
|
||||||
{ value: 'trans_date', label: 'Tanggal Jual/Bayar' },
|
customer_ids?: string;
|
||||||
{ value: 'realization_date', label: 'Tanggal Realisasi' },
|
start_date?: string;
|
||||||
];
|
end_date?: string;
|
||||||
|
filter_by?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
useState(false);
|
|
||||||
const isAnyExportLoading =
|
|
||||||
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
|
||||||
|
|
||||||
|
// ===== PAGINATION STATE =====
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize] = useState(10);
|
||||||
|
|
||||||
|
// ===== SUBMISSION STATE =====
|
||||||
|
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
const dataTypeOptions = useMemo(
|
||||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
() => [
|
||||||
|
{ value: 'trans_date', label: 'Tanggal Jual/Bayar' },
|
||||||
const {
|
{ value: 'realization_date', label: 'Tanggal Realisasi' },
|
||||||
state: tableFilterState,
|
],
|
||||||
updateFilter,
|
[]
|
||||||
setPage,
|
);
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
reset: resetFilter,
|
|
||||||
} = useTableFilter<{
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
customers: OptionType<number>[];
|
|
||||||
filterBy?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
customers: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
start_date: 'start_date',
|
|
||||||
end_date: 'end_date',
|
|
||||||
customers: 'customer_ids',
|
|
||||||
filterBy: 'filter_by',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'customer-payment-report-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: customerOptions,
|
options: customerOptions,
|
||||||
@@ -103,188 +85,223 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik({
|
const formik = useFormik<CustomerPaymentFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
start_date: tableFilterState.start_date,
|
start_date: null,
|
||||||
end_date: tableFilterState.end_date,
|
end_date: null,
|
||||||
customers: tableFilterState.customers,
|
customer_ids: null,
|
||||||
filterBy: tableFilterState.filterBy,
|
filter_by: null,
|
||||||
},
|
},
|
||||||
onSubmit: (values) => {
|
validationSchema: CustomerPaymentFilterSchema,
|
||||||
updateFilter('start_date', values.start_date, true);
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('end_date', values.end_date, true);
|
setFilterParams({
|
||||||
updateFilter('customers', values.customers, true);
|
start_date: values.start_date || undefined,
|
||||||
updateFilter('filterBy', values.filterBy, true);
|
end_date: values.end_date || undefined,
|
||||||
|
customer_ids: values.customer_ids || undefined,
|
||||||
|
filter_by: values.filter_by || undefined,
|
||||||
|
});
|
||||||
|
filterModal.closeModal();
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
setFilterParams({});
|
||||||
|
setCurrentPage(1);
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const formikResetHandler = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
resetFilter();
|
filterModal.openModal();
|
||||||
|
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.includes('belum')) return 'warning';
|
if (normalizedValue === 'lunas') {
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedValue.includes('belum')) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
return 'neutral';
|
return 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== DATE CHANGE HANDLERS =====
|
// ===== DATE CHANGE HANDLERS =====
|
||||||
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleStartDateChange = useCallback(
|
||||||
const value = e.target.value;
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
formik.setFieldValue('start_date', value);
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('start_date', value || null);
|
||||||
|
|
||||||
if (value && formik.values.end_date) {
|
if (value && formik.values.end_date) {
|
||||||
if (new Date(formik.values.end_date) < new Date(value)) {
|
const startDate = new Date(value);
|
||||||
setHasDateError(true);
|
const endDateObj = new Date(formik.values.end_date);
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
if (endDateObj < startDate) {
|
||||||
duration: Infinity,
|
setHasDateError(true);
|
||||||
});
|
if (!dateErrorShown) {
|
||||||
setDateErrorShown(true);
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setHasDateError(false);
|
setHasDateError(false);
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
setHasDateError(false);
|
[formik, dateErrorShown]
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
formik.setFieldValue('end_date', value);
|
|
||||||
|
|
||||||
if (value && formik.values.start_date) {
|
|
||||||
if (new Date(value) < new Date(formik.values.start_date)) {
|
|
||||||
setHasDateError(true);
|
|
||||||
if (!dateErrorShown) {
|
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
|
||||||
duration: Infinity,
|
|
||||||
});
|
|
||||||
setDateErrorShown(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasDateError(false);
|
|
||||||
if (dateErrorShown) {
|
|
||||||
toast.dismiss();
|
|
||||||
setDateErrorShown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
|
||||||
const { data: customerPayment, isLoading } = useSWR<
|
|
||||||
BaseApiResponse<CustomerPaymentReport>,
|
|
||||||
AxiosError<BaseApiResponse>,
|
|
||||||
SWRHttpKey
|
|
||||||
>(
|
|
||||||
`${FinanceApi.basePath}/customer-payment${getTableFilterQueryString()}`,
|
|
||||||
httpClientFetcher
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const data: CustomerPaymentReport[] = isResponseSuccess(customerPayment)
|
const handleEndDateChange = useCallback(
|
||||||
? (customerPayment?.data as unknown as CustomerPaymentReport[]) || []
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
: [];
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('end_date', value || null);
|
||||||
|
|
||||||
const meta =
|
if (value && formik.values.start_date) {
|
||||||
isResponseSuccess(customerPayment) && customerPayment.meta
|
const startDateObj = new Date(formik.values.start_date);
|
||||||
? customerPayment.meta
|
const endDate = new Date(value);
|
||||||
: null;
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[formik, dateErrorShown]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const customerIdsValue = useMemo(() => {
|
||||||
|
if (!formik.values.customer_ids) return [];
|
||||||
|
return customerOptions.filter((opt) =>
|
||||||
|
formik.values.customer_ids?.split(',').includes(String(opt.value))
|
||||||
|
);
|
||||||
|
}, [formik.values.customer_ids, customerOptions]);
|
||||||
|
|
||||||
|
const filterByValue = useMemo(() => {
|
||||||
|
if (!formik.values.filter_by) return null;
|
||||||
|
return (
|
||||||
|
dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.filter_by, dataTypeOptions]);
|
||||||
|
|
||||||
|
// ===== DATA FETCHING =====
|
||||||
|
const { data: customerPayment, isLoading } = useSWR(
|
||||||
|
() => {
|
||||||
|
const params = {
|
||||||
|
customer_ids: filterParams.customer_ids,
|
||||||
|
filter_by: filterParams.filter_by as
|
||||||
|
| 'trans_date'
|
||||||
|
| 'realization_date'
|
||||||
|
| undefined,
|
||||||
|
start_date: filterParams.start_date,
|
||||||
|
end_date: filterParams.end_date,
|
||||||
|
page: currentPage,
|
||||||
|
limit: pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ['customer-payment-report', params];
|
||||||
|
},
|
||||||
|
([, params]) =>
|
||||||
|
FinanceApi.getCustomerPaymentReport(
|
||||||
|
params.customer_ids,
|
||||||
|
params.filter_by,
|
||||||
|
params.start_date,
|
||||||
|
params.end_date,
|
||||||
|
params.page,
|
||||||
|
params.limit
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: CustomerPaymentReport[] = useMemo(
|
||||||
|
() =>
|
||||||
|
isResponseSuccess(customerPayment)
|
||||||
|
? (customerPayment?.data as unknown as CustomerPaymentReport[]) || []
|
||||||
|
: [],
|
||||||
|
[customerPayment]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const customerPaymentExport = useCallback(async (): Promise<
|
const customerPaymentExport = useCallback(async (): Promise<
|
||||||
CustomerPaymentReport[] | null
|
CustomerPaymentReport[] | null
|
||||||
> => {
|
> => {
|
||||||
const customer_ids =
|
const params = {
|
||||||
tableFilterState.customers.length > 0
|
customer_ids: filterParams.customer_ids,
|
||||||
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
filter_by: filterParams.filter_by as
|
||||||
: undefined;
|
| 'trans_date'
|
||||||
const filter_by = tableFilterState.filterBy?.value as
|
| 'realization_date'
|
||||||
| 'trans_date'
|
| undefined,
|
||||||
| 'realization_date'
|
start_date: filterParams.start_date,
|
||||||
| undefined;
|
end_date: filterParams.end_date,
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await FinanceApi.getCustomerPaymentReport(
|
const response = await FinanceApi.getCustomerPaymentReport(
|
||||||
customer_ids,
|
params.customer_ids,
|
||||||
filter_by,
|
params.filter_by,
|
||||||
tableFilterState.start_date || undefined,
|
params.start_date,
|
||||||
tableFilterState.end_date || undefined,
|
params.end_date,
|
||||||
1,
|
params.page,
|
||||||
100
|
params.limit
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response)
|
return isResponseSuccess(response)
|
||||||
? (response.data as unknown as CustomerPaymentReport[])
|
? (response.data as unknown as CustomerPaymentReport[])
|
||||||
: null;
|
: null;
|
||||||
}, [tableFilterState]);
|
}, [filterParams]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcelGeneral = useCallback(async () => {
|
|
||||||
setIsExcelGeneralExportLoading(true);
|
|
||||||
try {
|
|
||||||
const customer_ids =
|
|
||||||
tableFilterState.customers.length > 0
|
|
||||||
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
|
||||||
: undefined;
|
|
||||||
await FinanceApi.exportCustomerPaymentToExcelGeneral(
|
|
||||||
customer_ids,
|
|
||||||
tableFilterState.filterBy?.value,
|
|
||||||
tableFilterState.start_date || undefined,
|
|
||||||
tableFilterState.end_date || undefined
|
|
||||||
);
|
|
||||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExcelGeneralExportLoading(false);
|
|
||||||
}
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const customer_ids =
|
const allDataForExport = await customerPaymentExport();
|
||||||
tableFilterState.customers.length > 0
|
|
||||||
? tableFilterState.customers.map((o) => String(o.value)).join(',')
|
if (
|
||||||
: undefined;
|
!allDataForExport ||
|
||||||
await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet(
|
!Array.isArray(allDataForExport) ||
|
||||||
customer_ids,
|
allDataForExport.length === 0
|
||||||
tableFilterState.filterBy?.value,
|
) {
|
||||||
tableFilterState.start_date || undefined,
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
tableFilterState.end_date || undefined
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
await generateCustomerPaymentExcel({ data: allDataForExport });
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [tableFilterState]);
|
}, [customerPaymentExport]);
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -300,18 +317,22 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerName =
|
const customerName = filterParams.customer_ids
|
||||||
tableFilterState.customers.length > 0
|
? customerOptions
|
||||||
? tableFilterState.customers.map((o) => o.label).join(', ')
|
.filter((opt) =>
|
||||||
: 'Semua Customer';
|
filterParams.customer_ids?.split(',').includes(String(opt.value))
|
||||||
|
)
|
||||||
|
.map((opt) => opt.label)
|
||||||
|
.join(', ') || 'Semua Customer'
|
||||||
|
: 'Semua Customer';
|
||||||
|
|
||||||
await generateCustomerPaymentPDF({
|
await generateCustomerPaymentPDF({
|
||||||
data: allDataForExport,
|
data: allDataForExport,
|
||||||
params: {
|
params: {
|
||||||
customer_name: customerName,
|
customer_name: customerName,
|
||||||
start_date: tableFilterState.start_date || undefined,
|
start_date: filterParams.start_date,
|
||||||
end_date: tableFilterState.end_date || undefined,
|
end_date: filterParams.end_date,
|
||||||
filter_by: tableFilterState.filterBy?.value as
|
filter_by: filterParams.filter_by as
|
||||||
| 'trans_date'
|
| 'trans_date'
|
||||||
| 'realization_date'
|
| 'realization_date'
|
||||||
| undefined,
|
| undefined,
|
||||||
@@ -323,103 +344,106 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsPdfExportLoading(false);
|
setIsPdfExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [customerPaymentExport, tableFilterState]);
|
}, [customerPaymentExport, filterParams, customerOptions]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
useEffect(() => {
|
const TabActions = useMemo(() => {
|
||||||
setTabActions(
|
return function TabActionsComponent() {
|
||||||
tabId,
|
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||||
<div className='flex flex-row gap-3'>
|
const clearTabActions = useTabActionsStore(
|
||||||
<ButtonFilter
|
(state) => state.clearTabActions
|
||||||
values={{
|
);
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
customers: tableFilterState.customers,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
}}
|
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
|
||||||
onClick={filterModal.openModal}
|
|
||||||
variant='outline'
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
useEffect(() => {
|
||||||
align='end'
|
setTabActions(
|
||||||
direction='bottom'
|
tabId,
|
||||||
className={{
|
<div className='flex flex-row gap-3'>
|
||||||
content:
|
<ButtonFilter
|
||||||
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
values={filterParams}
|
||||||
}}
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
trigger={
|
onClick={() => handleFilterModalOpenRef.current()}
|
||||||
<Button
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
className='px-3 py-2.5'
|
||||||
isLoading={isAnyExportLoading}
|
/>
|
||||||
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
|
||||||
|
<Dropdown
|
||||||
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
|
className={{
|
||||||
|
content:
|
||||||
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
isLoading={isAnyExportLoading}
|
||||||
|
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>Export</span>
|
||||||
|
|
||||||
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-down'
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
<Button
|
||||||
<Icon
|
variant='ghost'
|
||||||
icon='heroicons:cloud-arrow-down'
|
color='none'
|
||||||
width={20}
|
onClick={handleExportExcel}
|
||||||
height={20}
|
isLoading={isExcelExportLoading}
|
||||||
/>
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
<span>Export</span>
|
>
|
||||||
<div className='w-px self-stretch bg-base-content/10' />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
<Icon icon='heroicons:chevron-down' width={14} height={14} />
|
Export to Excel
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
}
|
variant='ghost'
|
||||||
>
|
color='none'
|
||||||
<Button
|
onClick={handleExportPdf}
|
||||||
variant='ghost'
|
isLoading={isPdfExportLoading}
|
||||||
color='none'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
onClick={handleExportExcel}
|
>
|
||||||
isLoading={isExcelExportLoading}
|
<Icon icon='heroicons:document' width={20} height={20} />
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
Export to PDF
|
||||||
>
|
</Button>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
</Dropdown>
|
||||||
Export to Excel - Customer Per Sheet
|
</div>
|
||||||
</Button>
|
);
|
||||||
<Button
|
}, [setTabActions]);
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
useEffect(() => {
|
||||||
onClick={handleExportExcelGeneral}
|
return () => {
|
||||||
isLoading={isExcelGeneralExportLoading}
|
clearTabActions(tabId);
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
};
|
||||||
>
|
}, [clearTabActions]);
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Export to Excel - General
|
return null;
|
||||||
</Button>
|
};
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={handleExportPdf}
|
|
||||||
isLoading={isPdfExportLoading}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:document' width={20} height={20} />
|
|
||||||
Export to PDF
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
setTabActions,
|
|
||||||
tableFilterState,
|
|
||||||
filterModal.openModal,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportExcelGeneral,
|
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
isExcelExportLoading,
|
isExcelExportLoading,
|
||||||
isExcelGeneralExportLoading,
|
|
||||||
isPdfExportLoading,
|
isPdfExportLoading,
|
||||||
|
filterParams,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
||||||
return () => clearTabActions(tabId);
|
|
||||||
}, [tabId, clearTabActions]);
|
|
||||||
|
|
||||||
const getTableColumns = (
|
const getTableColumns = (
|
||||||
summary: CustomerPaymentSummary
|
summary: CustomerPaymentSummary
|
||||||
@@ -626,7 +650,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.status;
|
const value = props.row.original.status;
|
||||||
if (!value) return '-';
|
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
color={getPaymentStatusBadgeColor(value)}
|
color={getPaymentStatusBadgeColor(value)}
|
||||||
@@ -665,6 +693,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{TabActionsElement}
|
||||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
@@ -688,27 +717,6 @@ 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) => {
|
||||||
@@ -803,27 +811,6 @@ 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 */}
|
||||||
@@ -848,7 +835,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<div>
|
<div>
|
||||||
<label className='block text-xs font-semibold text-base-content py-2'>
|
<label className='block text-xs font-semibold text-base-content py-2'>
|
||||||
@@ -858,18 +845,29 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<DateInput
|
<DateInput
|
||||||
name='start_date'
|
name='start_date'
|
||||||
value={formik.values.start_date || ''}
|
value={formik.values.start_date || ''}
|
||||||
|
errorMessage={formik.errors.start_date}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
|
isError={
|
||||||
|
formik.touched.start_date &&
|
||||||
|
Boolean(formik.errors.start_date)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='end_date'
|
||||||
value={formik.values.end_date || ''}
|
value={formik.values.end_date || ''}
|
||||||
|
errorMessage={formik.errors.end_date}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
isError={hasDateError}
|
isError={
|
||||||
|
(formik.touched.end_date &&
|
||||||
|
Boolean(formik.errors.end_date)) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -878,10 +876,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
label='Customer'
|
label='Customer'
|
||||||
placeholder='Pilih Customer'
|
placeholder='Pilih Customer'
|
||||||
options={customerOptions}
|
options={customerOptions}
|
||||||
value={formik.values.customers}
|
value={customerIdsValue}
|
||||||
onChange={(val) =>
|
onChange={(val) => {
|
||||||
formik.setFieldValue('customers', Array.isArray(val) ? val : [])
|
formik.setFieldValue(
|
||||||
}
|
'customer_ids',
|
||||||
|
Array.isArray(val) && val.length > 0
|
||||||
|
? val.map((v: OptionType) => String(v.value)).join(',')
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}}
|
||||||
onInputChange={setCustomerInputValue}
|
onInputChange={setCustomerInputValue}
|
||||||
isLoading={isLoadingCustomers}
|
isLoading={isLoadingCustomers}
|
||||||
isClearable
|
isClearable
|
||||||
@@ -893,15 +896,14 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
label='Filter Berdasarkan'
|
label='Filter Berdasarkan'
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
options={dataTypeOptions}
|
options={dataTypeOptions}
|
||||||
value={formik.values.filterBy ?? null}
|
value={filterByValue}
|
||||||
onChange={(val) =>
|
onChange={(val) => {
|
||||||
formik.setFieldValue(
|
if (!Array.isArray(val)) {
|
||||||
'filterBy',
|
formik.setFieldValue('filter_by', val?.value || null);
|
||||||
!Array.isArray(val) ? (val ?? undefined) : undefined
|
}
|
||||||
)
|
}}
|
||||||
}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isClearable
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -917,7 +919,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={hasDateError}
|
disabled={hasDateError || !formik.isValid || formik.isSubmitting}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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';
|
||||||
@@ -9,15 +8,24 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
|
import {
|
||||||
|
DebtRow,
|
||||||
|
DebtSupplier,
|
||||||
|
DebtSupplierFilter,
|
||||||
|
} from '@/types/api/report/debt-supplier';
|
||||||
|
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
|
||||||
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
|
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
import {
|
||||||
|
DebtSupplierFilterSchema,
|
||||||
|
DebtSupplierFilterType,
|
||||||
|
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
@@ -26,10 +34,6 @@ import SelectInputRadio from '@/components/input/SelectInputRadio';
|
|||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton';
|
import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
const dueStatus: Record<string, Color> = {
|
const dueStatus: Record<string, Color> = {
|
||||||
'Sudah Jatuh Tempo': 'error',
|
'Sudah Jatuh Tempo': 'error',
|
||||||
@@ -47,6 +51,7 @@ const getPillBadge = (
|
|||||||
statusText: string,
|
statusText: string,
|
||||||
type: 'due' | 'payment' = 'payment'
|
type: 'due' | 'payment' = 'payment'
|
||||||
) => {
|
) => {
|
||||||
|
// Get color based on type
|
||||||
const color =
|
const color =
|
||||||
type === 'due'
|
type === 'due'
|
||||||
? dueStatus[statusText] || 'neutral'
|
? dueStatus[statusText] || 'neutral'
|
||||||
@@ -63,11 +68,6 @@ const getPillBadge = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataTypeOptions: OptionType<string>[] = [
|
|
||||||
{ value: 'received_date', label: 'Tanggal Terima' },
|
|
||||||
{ value: 'po_date', label: 'Tanggal PO' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface DebtSupplierTabProps {
|
interface DebtSupplierTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
}
|
}
|
||||||
@@ -76,50 +76,24 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
useState(false);
|
|
||||||
const isAnyExportLoading =
|
|
||||||
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
|
||||||
|
|
||||||
|
// ===== SUBMISSION STATE =====
|
||||||
|
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
|
||||||
|
start_date: undefined,
|
||||||
|
end_date: undefined,
|
||||||
|
supplier_ids: undefined,
|
||||||
|
filter_by: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== DATE ERROR STATE =====
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
const [hasDateError, setHasDateError] = useState(false);
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
|
||||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
|
||||||
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
updateFilter,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
reset: resetFilter,
|
|
||||||
} = useTableFilter<{
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
suppliers: OptionType<number>[];
|
|
||||||
filterBy?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
start_date: '',
|
|
||||||
end_date: '',
|
|
||||||
suppliers: [],
|
|
||||||
filterBy: undefined,
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
start_date: 'start_date',
|
|
||||||
end_date: 'end_date',
|
|
||||||
suppliers: 'supplier_ids',
|
|
||||||
filterBy: 'filter_by',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'debt-supplier-report-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setSupplierInputValue,
|
setInputValue: setSupplierInputValue,
|
||||||
options: supplierOptions,
|
options: supplierOptions,
|
||||||
@@ -127,180 +101,140 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
loadMore: loadMoreSuppliers,
|
loadMore: loadMoreSuppliers,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const dataTypeOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'received_date', label: 'Tanggal Terima' },
|
||||||
|
{ value: 'po_date', label: 'Tanggal PO' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik({
|
const formik = useFormik<DebtSupplierFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
start_date: tableFilterState.start_date,
|
startDate: null,
|
||||||
end_date: tableFilterState.end_date,
|
endDate: null,
|
||||||
suppliers: tableFilterState.suppliers,
|
supplierIds: null,
|
||||||
filterBy: tableFilterState.filterBy,
|
filterBy: null,
|
||||||
},
|
},
|
||||||
|
validationSchema: DebtSupplierFilterSchema,
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
updateFilter('start_date', values.start_date, true);
|
setFilterParams({
|
||||||
updateFilter('end_date', values.end_date, true);
|
start_date: values.startDate?.toString() || undefined,
|
||||||
updateFilter('suppliers', values.suppliers, true);
|
end_date: values.endDate?.toString() || undefined,
|
||||||
updateFilter('filterBy', values.filterBy, true);
|
supplier_ids:
|
||||||
|
values.supplierIds?.map((v) => String(v.value)).join(',') ||
|
||||||
|
undefined,
|
||||||
|
filter_by: values.filterBy?.value?.toString() || undefined,
|
||||||
|
});
|
||||||
|
filterModal.closeModal();
|
||||||
|
// setIsSubmitted(true);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
setFilterParams({
|
||||||
|
start_date: undefined,
|
||||||
|
end_date: undefined,
|
||||||
|
supplier_ids: undefined,
|
||||||
|
filter_by: undefined,
|
||||||
|
});
|
||||||
|
// setIsSubmitted(false);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const formikResetHandler = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
resetFilter();
|
filterModal.openModal();
|
||||||
|
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: debtSupplierResponse, isLoading } = useSWR<
|
const { data: debtSupplier, isLoading } = useSWR(
|
||||||
BaseApiResponse<DebtSupplier[]>,
|
() => {
|
||||||
AxiosError<BaseApiResponse>,
|
const params = {
|
||||||
SWRHttpKey
|
supplier_ids: filterParams.supplier_ids,
|
||||||
>(
|
filter_by: filterParams.filter_by,
|
||||||
`${DebtSupplierApi.basePath}/debt-supplier${getTableFilterQueryString()}`,
|
start_date: filterParams.start_date,
|
||||||
httpClientFetcher
|
end_date: filterParams.end_date,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ['debt-supplier-report', params];
|
||||||
|
},
|
||||||
|
([, params]) =>
|
||||||
|
DebtSupplierApi.getDebtSupplierReport(
|
||||||
|
params.supplier_ids,
|
||||||
|
params.filter_by,
|
||||||
|
params.start_date,
|
||||||
|
params.end_date
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const data: DebtSupplier[] = isResponseSuccess(debtSupplierResponse)
|
const data: DebtSupplier[] = useMemo(
|
||||||
? ((debtSupplierResponse?.data as unknown as DebtSupplier[]) ?? [])
|
() =>
|
||||||
: [];
|
isResponseSuccess(debtSupplier)
|
||||||
|
? (debtSupplier?.data as unknown as DebtSupplier[]) || []
|
||||||
const meta =
|
: [],
|
||||||
isResponseSuccess(debtSupplierResponse) && debtSupplierResponse.meta
|
[debtSupplier]
|
||||||
? debtSupplierResponse.meta
|
);
|
||||||
: null;
|
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const debtSupplierExport = useCallback(async (): Promise<
|
const debtSupplierExport = useCallback(async (): Promise<
|
||||||
DebtSupplier[] | null
|
DebtSupplier[] | null
|
||||||
> => {
|
> => {
|
||||||
const supplier_ids =
|
const params = {
|
||||||
tableFilterState.suppliers.length > 0
|
supplier_ids:
|
||||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
formik.values.supplierIds && formik.values.supplierIds.length > 0
|
||||||
: undefined;
|
? formik.values.supplierIds.map((v) => String(v.value)).join(',')
|
||||||
|
: undefined,
|
||||||
|
filter_by: formik.values.filterBy?.value?.toString() || undefined,
|
||||||
|
start_date: formik.values.startDate || undefined,
|
||||||
|
end_date: formik.values.endDate || undefined,
|
||||||
|
date_type: formik.values.filterBy
|
||||||
|
? formik.values.filterBy.value
|
||||||
|
: undefined,
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await DebtSupplierApi.getDebtSupplierReport(
|
const response = await DebtSupplierApi.getDebtSupplierReport(
|
||||||
supplier_ids,
|
params.supplier_ids,
|
||||||
tableFilterState.filterBy?.value,
|
params.filter_by,
|
||||||
tableFilterState.start_date || undefined,
|
params.start_date,
|
||||||
tableFilterState.end_date || undefined,
|
params.end_date
|
||||||
1,
|
|
||||||
100
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response)
|
return isResponseSuccess(response)
|
||||||
? (response.data as unknown as DebtSupplier[])
|
? (response.data as unknown as DebtSupplier[])
|
||||||
: null;
|
: null;
|
||||||
}, [tableFilterState]);
|
}, [
|
||||||
|
formik.values.supplierIds,
|
||||||
|
formik.values.startDate,
|
||||||
|
formik.values.endDate,
|
||||||
|
formik.values.filterBy,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const supplier_ids =
|
const allDataForExport = await debtSupplierExport();
|
||||||
tableFilterState.suppliers.length > 0
|
|
||||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
if (
|
||||||
: undefined;
|
!allDataForExport ||
|
||||||
await DebtSupplierApi.exportToExcelSupplierPerSheet(
|
!Array.isArray(allDataForExport) ||
|
||||||
supplier_ids,
|
allDataForExport.length === 0
|
||||||
tableFilterState.filterBy?.value,
|
) {
|
||||||
tableFilterState.start_date || undefined,
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
tableFilterState.end_date || undefined
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
generateDebtSupplierExcel({ data: allDataForExport });
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [tableFilterState]);
|
}, [debtSupplierExport]);
|
||||||
|
|
||||||
const handleExportExcelGeneral = useCallback(async () => {
|
|
||||||
setIsExcelGeneralExportLoading(true);
|
|
||||||
try {
|
|
||||||
const supplier_ids =
|
|
||||||
tableFilterState.suppliers.length > 0
|
|
||||||
? tableFilterState.suppliers.map((o) => String(o.value)).join(',')
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await DebtSupplierApi.exportToExcelGeneral(
|
|
||||||
supplier_ids,
|
|
||||||
tableFilterState.filterBy?.value,
|
|
||||||
tableFilterState.start_date || undefined,
|
|
||||||
tableFilterState.end_date || undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExcelGeneralExportLoading(false);
|
|
||||||
}
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -316,18 +250,15 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supplierName =
|
|
||||||
tableFilterState.suppliers.length > 0
|
|
||||||
? tableFilterState.suppliers.map((o) => o.label).join(', ')
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await generateDebtSupplierPDF({
|
await generateDebtSupplierPDF({
|
||||||
data: allDataForExport,
|
data: allDataForExport,
|
||||||
params: {
|
params: {
|
||||||
supplier_name: supplierName,
|
supplier_name: formik.values.supplierIds
|
||||||
filter_by: tableFilterState.filterBy?.label,
|
?.map((v) => v.label)
|
||||||
start_date: tableFilterState.start_date || undefined,
|
.join(', '),
|
||||||
end_date: tableFilterState.end_date || undefined,
|
filter_by: formik.values.filterBy?.label,
|
||||||
|
start_date: formik.values.startDate || undefined,
|
||||||
|
end_date: formik.values.endDate || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
@@ -336,103 +267,129 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsPdfExportLoading(false);
|
setIsPdfExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [debtSupplierExport, tableFilterState]);
|
}, [
|
||||||
|
debtSupplierExport,
|
||||||
|
formik.values.supplierIds,
|
||||||
|
formik.values.filterBy,
|
||||||
|
formik.values.startDate,
|
||||||
|
formik.values.endDate,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
useEffect(() => {
|
const TabActions = useMemo(() => {
|
||||||
setTabActions(
|
return function TabActionsComponent() {
|
||||||
tabId,
|
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
||||||
<div className='flex flex-row gap-3'>
|
const clearTabActions = useTabActionsStore(
|
||||||
<ButtonFilter
|
(state) => state.clearTabActions
|
||||||
values={{
|
);
|
||||||
start_date: tableFilterState.start_date,
|
|
||||||
end_date: tableFilterState.end_date,
|
|
||||||
suppliers: tableFilterState.suppliers,
|
|
||||||
filterBy: tableFilterState.filterBy,
|
|
||||||
}}
|
|
||||||
fieldGroups={[['start_date', 'end_date']]}
|
|
||||||
onClick={filterModal.openModal}
|
|
||||||
variant='outline'
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
useEffect(() => {
|
||||||
align='end'
|
setTabActions(
|
||||||
direction='bottom'
|
tabId,
|
||||||
className={{
|
<div className='flex flex-row gap-3'>
|
||||||
content:
|
<ButtonFilter
|
||||||
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
values={filterParams}
|
||||||
}}
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
trigger={
|
onClick={() => handleFilterModalOpenRef.current()}
|
||||||
<Button
|
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
className='px-3 py-2.5'
|
||||||
isLoading={isAnyExportLoading}
|
/>
|
||||||
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
|
||||||
|
<Dropdown
|
||||||
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
|
className={{
|
||||||
|
content:
|
||||||
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
isLoading={isAnyExportLoading}
|
||||||
|
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>Export</span>
|
||||||
|
|
||||||
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-down'
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
<Button
|
||||||
<Icon
|
variant='ghost'
|
||||||
icon='heroicons:cloud-arrow-down'
|
color='none'
|
||||||
width={20}
|
onClick={handleExportExcel}
|
||||||
height={20}
|
isLoading={isExcelExportLoading}
|
||||||
/>
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
<span>Export</span>
|
>
|
||||||
<div className='w-px self-stretch bg-base-content/10' />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
<Icon icon='heroicons:chevron-down' width={14} height={14} />
|
Export to Excel
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
}
|
variant='ghost'
|
||||||
>
|
color='none'
|
||||||
<Button
|
onClick={handleExportPdf}
|
||||||
variant='ghost'
|
isLoading={isPdfExportLoading}
|
||||||
color='none'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
onClick={handleExportExcel}
|
>
|
||||||
isLoading={isExcelExportLoading}
|
<Icon icon='heroicons:document' width={20} height={20} />
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
Export to PDF
|
||||||
>
|
</Button>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
</Dropdown>
|
||||||
Export to Excel - Supplier Per Sheet
|
</div>
|
||||||
</Button>
|
);
|
||||||
<Button
|
}, [setTabActions]);
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
useEffect(() => {
|
||||||
onClick={handleExportExcelGeneral}
|
return () => {
|
||||||
isLoading={isExcelGeneralExportLoading}
|
clearTabActions(tabId);
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
};
|
||||||
>
|
}, [clearTabActions]);
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Export to Excel - General
|
return null;
|
||||||
</Button>
|
};
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={handleExportPdf}
|
|
||||||
isLoading={isPdfExportLoading}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:document' width={20} height={20} />
|
|
||||||
Export to PDF
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
setTabActions,
|
filterParams,
|
||||||
tableFilterState,
|
|
||||||
filterModal.openModal,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportExcelGeneral,
|
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
isExcelExportLoading,
|
isExcelExportLoading,
|
||||||
isExcelGeneralExportLoading,
|
|
||||||
isPdfExportLoading,
|
isPdfExportLoading,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const TabActionsElement = useMemo(() => <TabActions />, [TabActions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => clearTabActions(tabId);
|
return () => {
|
||||||
}, [tabId, clearTabActions]);
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [filterModal.open, dateErrorShown]);
|
||||||
|
|
||||||
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
||||||
{
|
{
|
||||||
@@ -647,9 +604,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'>
|
||||||
@@ -673,27 +630,6 @@ 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) => {
|
||||||
@@ -781,27 +717,6 @@ 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 */}
|
||||||
@@ -812,23 +727,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
{/* Modal Header */}
|
||||||
<div className='flex items-center gap-2 text-primary'>
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
type='button'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant='link'
|
|
||||||
type='button'
|
|
||||||
onClick={filterModal.closeModal}
|
|
||||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<div>
|
<div>
|
||||||
@@ -837,68 +752,153 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
</label>
|
</label>
|
||||||
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
<div className='flex flex-row gap-1.5 items-center justify-between'>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='start_date'
|
name='startDate'
|
||||||
value={formik.values.start_date || ''}
|
value={formik.values.startDate || ''}
|
||||||
onChange={handleStartDateChange}
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('startDate', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.endDate) {
|
||||||
|
const startDate = new Date(value);
|
||||||
|
const endDateObj = new Date(formik.values.endDate);
|
||||||
|
|
||||||
|
if (endDateObj < startDate) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
formik.touched.startDate && !!formik.errors.startDate
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.startDate}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
/>
|
/>
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
<hr className='w-full max-w-3 h-px border-base-content/10'></hr>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='endDate'
|
||||||
value={formik.values.end_date || ''}
|
value={formik.values.endDate || ''}
|
||||||
onChange={handleEndDateChange}
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('endDate', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.startDate) {
|
||||||
|
const startDateObj = new Date(formik.values.startDate);
|
||||||
|
const endDate = new Date(value);
|
||||||
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
(formik.touched.endDate && !!formik.errors.endDate) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.endDate}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
isError={hasDateError}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectInputCheckbox
|
<div>
|
||||||
label='Supplier'
|
<SelectInputCheckbox
|
||||||
placeholder='Pilih Supplier'
|
label='Supplier'
|
||||||
options={supplierOptions}
|
placeholder='Pilih Supplier'
|
||||||
value={formik.values.suppliers}
|
isMulti
|
||||||
onChange={(val) =>
|
options={supplierOptions}
|
||||||
formik.setFieldValue('suppliers', Array.isArray(val) ? val : [])
|
value={
|
||||||
}
|
(formik.values.supplierIds as
|
||||||
onInputChange={setSupplierInputValue}
|
| { value: number; label: string }
|
||||||
onMenuScrollToBottom={loadMoreSuppliers}
|
| { value: number; label: string }[]
|
||||||
isLoading={isLoadingSupplierOptions}
|
| null
|
||||||
isClearable
|
| undefined) || []
|
||||||
className={{ wrapper: 'w-full' }}
|
}
|
||||||
/>
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'supplierIds',
|
||||||
|
Array.isArray(val) ? val : val ? [val] : null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isLoading={isLoadingSupplierOptions}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
formik.touched.supplierIds && !!formik.errors.supplierIds
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.supplierIds as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelectInputRadio
|
<div>
|
||||||
label='Filter Berdasarkan'
|
<SelectInputRadio
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
label='Filter Berdasarkan'
|
||||||
options={dataTypeOptions}
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
value={formik.values.filterBy ?? null}
|
options={dataTypeOptions}
|
||||||
onChange={(val) =>
|
value={
|
||||||
formik.setFieldValue(
|
(formik.values.filterBy as
|
||||||
'filterBy',
|
| { value: string; label: string }
|
||||||
!Array.isArray(val) ? (val ?? undefined) : undefined
|
| { value: string; label: string }[]
|
||||||
)
|
| null
|
||||||
}
|
| undefined) || null
|
||||||
className={{ wrapper: 'w-full' }}
|
}
|
||||||
isClearable
|
onChange={(val) => {
|
||||||
/>
|
formik.setFieldValue(
|
||||||
|
'filterBy',
|
||||||
|
val ? (val as OptionType) : null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isClearable
|
||||||
|
isError={formik.touched.filterBy && !!formik.errors.filterBy}
|
||||||
|
errorMessage={formik.errors.filterBy as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Action Buttons */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
|
||||||
variant='soft'
|
variant='soft'
|
||||||
|
color='none'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
type='reset'
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={hasDateError}
|
type='submit'
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -156,17 +156,8 @@ 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,8 +1,6 @@
|
|||||||
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;
|
||||||
@@ -16,8 +14,6 @@ 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,8 +1,6 @@
|
|||||||
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;
|
||||||
@@ -14,8 +12,6 @@ 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,10 +17,16 @@ import {
|
|||||||
formatVechicleNumber,
|
formatVechicleNumber,
|
||||||
formatTitleCase,
|
formatTitleCase,
|
||||||
} from '@/lib/helper';
|
} from '@/lib/helper';
|
||||||
import { DailyMarketingRow } from '@/types/api/report/marketing';
|
import {
|
||||||
|
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';
|
||||||
@@ -33,6 +39,8 @@ 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,
|
||||||
@@ -45,8 +53,6 @@ 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;
|
||||||
@@ -110,8 +116,6 @@ 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,
|
||||||
@@ -126,8 +130,6 @@ 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,
|
||||||
@@ -148,21 +150,8 @@ 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 =====
|
||||||
@@ -233,9 +222,6 @@ 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);
|
||||||
@@ -276,30 +262,67 @@ 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 params = new URLSearchParams();
|
const allDataForExport = await dailyMarketingsExport();
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
if (!allDataForExport || allDataForExport.length === 0) {
|
||||||
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
if (filterParams.location_id)
|
return;
|
||||||
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);
|
|
||||||
|
|
||||||
await MarketingReportApi.exportDailyMarketingToExcel(params.toString());
|
const period =
|
||||||
|
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 {
|
||||||
@@ -307,39 +330,34 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [filterParams, searchValue]);
|
}, [filterParams, dailyMarketingsExport, summaryTotal]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const allDataForExport = await dailyMarketingsExport();
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
if (!allDataForExport || allDataForExport.length === 0) {
|
||||||
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
if (filterParams.location_id)
|
return;
|
||||||
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);
|
|
||||||
|
|
||||||
await MarketingReportApi.exportDailyMarketingToPDF(params.toString());
|
const dailyMarketingReportPdfBlob = await pdf(
|
||||||
|
<DailyMarketingReportPDF data={allDataForExport} total={summaryTotal} />
|
||||||
|
).toBlob();
|
||||||
|
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}, [filterParams, searchValue]);
|
}, [dailyMarketingsExport, summaryTotal]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS COMPONENT =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
const TabActions = useMemo(() => {
|
const TabActions = useMemo(() => {
|
||||||
@@ -554,7 +572,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='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_qty
|
{summaryTotal?.total_qty
|
||||||
? formatNumber(summaryTotal.total_qty)
|
? formatNumber(summaryTotal.total_qty)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -567,7 +585,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='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_weight_kg
|
{summaryTotal?.average_weight_kg
|
||||||
? formatNumber(summaryTotal.average_weight_kg)
|
? formatNumber(summaryTotal.average_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -580,7 +598,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='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_weight_kg
|
{summaryTotal?.total_weight_kg
|
||||||
? formatNumber(summaryTotal.total_weight_kg)
|
? formatNumber(summaryTotal.total_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -593,9 +611,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='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_sales_price
|
{summaryTotal?.average_sales_price
|
||||||
? formatCurrency(summaryTotal.average_sales_price)
|
? formatNumber(summaryTotal.average_sales_price)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -606,7 +624,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='font-semibold text-gray-900'>
|
<div className='text-right 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)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -619,7 +637,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='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_sales_amount
|
{summaryTotal?.total_sales_amount
|
||||||
? formatCurrency(summaryTotal.total_sales_amount)
|
? formatCurrency(summaryTotal.total_sales_amount)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -670,27 +688,6 @@ 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,8 +40,6 @@ 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;
|
||||||
@@ -110,8 +108,6 @@ 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,
|
||||||
@@ -124,8 +120,6 @@ 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,
|
||||||
@@ -152,19 +146,8 @@ 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 =====
|
||||||
@@ -274,8 +257,6 @@ 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];
|
||||||
@@ -290,9 +271,7 @@ 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
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -342,9 +321,7 @@ 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;
|
||||||
@@ -489,7 +466,6 @@ 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'
|
||||||
@@ -869,25 +845,6 @@ 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={{
|
||||||
|
|||||||
+1
-36
@@ -263,43 +263,8 @@ 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] =
|
||||||
|
|||||||
+1
-11
@@ -197,7 +197,6 @@ 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',
|
||||||
],
|
],
|
||||||
@@ -205,10 +204,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
{
|
{
|
||||||
text: 'Stok Produk',
|
text: 'Stok Produk',
|
||||||
link: '/inventory/product',
|
link: '/inventory/product',
|
||||||
permission: [
|
permission: ['lti.inventory.product_stock.list'],
|
||||||
'lti.inventory.product_stock.list',
|
|
||||||
'lti.inventory.stock_log.list',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Penyesuaian Stok',
|
text: 'Penyesuaian Stok',
|
||||||
@@ -240,7 +236,6 @@ 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: [
|
||||||
{
|
{
|
||||||
@@ -308,11 +303,6 @@ 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,6 +218,4 @@ 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,7 +20,6 @@ interface DatePickerProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
formatDisplay?: (date: string) => string;
|
formatDisplay?: (date: string) => string;
|
||||||
hasError?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DatePicker({
|
export function DatePicker({
|
||||||
@@ -29,7 +28,6 @@ export function DatePicker({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
placeholder = 'Select date',
|
placeholder = 'Select date',
|
||||||
formatDisplay,
|
formatDisplay,
|
||||||
hasError = false,
|
|
||||||
}: DatePickerProps) {
|
}: DatePickerProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [currentMonth, setCurrentMonth] = useState(() => {
|
const [currentMonth, setCurrentMonth] = useState(() => {
|
||||||
@@ -156,7 +154,7 @@ export function DatePicker({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`w-full justify-start text-left font-normal hover:bg-gray-50 ${hasError ? 'border-red-500 focus:ring-red-500' : 'border-gray-200'}`}
|
className='w-full justify-start text-left font-normal border-gray-200 hover:bg-gray-50'
|
||||||
>
|
>
|
||||||
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
||||||
{date ? (
|
{date ? (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -89,10 +89,7 @@ export function Dashboard() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
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;
|
||||||
|
|||||||
+37
-320
@@ -40,12 +40,11 @@ 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, Row } from '@tanstack/react-table';
|
import { ColumnDef } 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' },
|
||||||
@@ -60,7 +59,6 @@ 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() {
|
||||||
@@ -89,9 +87,6 @@ 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 {
|
||||||
@@ -110,10 +105,7 @@ export function ListDailyChecklistContent() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
order_by: 'asc',
|
|
||||||
sort_by: 'name',
|
|
||||||
});
|
|
||||||
|
|
||||||
const checklistList = isResponseSuccess(checklistListRes)
|
const checklistList = isResponseSuccess(checklistListRes)
|
||||||
? checklistListRes.data || []
|
? checklistListRes.data || []
|
||||||
@@ -130,29 +122,12 @@ 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}`
|
||||||
@@ -160,7 +135,13 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item: DailyChecklist) => {
|
const handleEdit = (item: DailyChecklist) => {
|
||||||
router.push(`/daily-checklist/daily-checklist?checklistId=${item.id}`);
|
const formattedDate = new Date(item.date).toISOString().split('T')[0];
|
||||||
|
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) => {
|
||||||
@@ -168,22 +149,21 @@ 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 handleBulkReject = () => {
|
|
||||||
setRejectReason('');
|
|
||||||
setShowBulkRejectModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (item: DailyChecklist) => {
|
const handleDelete = (item: DailyChecklist) => {
|
||||||
|
// ✅ VALIDATION: Only DRAFT can be deleted
|
||||||
|
if (item.status !== 'DRAFT') {
|
||||||
|
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
||||||
|
description: `Status saat ini: ${item.status}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
};
|
};
|
||||||
@@ -215,31 +195,6 @@ 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;
|
||||||
|
|
||||||
@@ -274,40 +229,6 @@ 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;
|
||||||
|
|
||||||
@@ -404,37 +325,6 @@ 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',
|
||||||
@@ -547,17 +437,19 @@ export function ListDailyChecklistContent() {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
{row.original.status === 'DRAFT' && (
|
||||||
<Button
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
size='sm'
|
<Button
|
||||||
variant='destructive'
|
size='sm'
|
||||||
onClick={() => handleDelete(row.original)}
|
variant='destructive'
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
onClick={() => handleDelete(row.original)}
|
||||||
>
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
<Trash2 className='w-4 h-4 mr-1' />
|
>
|
||||||
Hapus
|
<Trash2 className='w-4 h-4 mr-1' />
|
||||||
</Button>
|
Hapus
|
||||||
</RequirePermission>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -567,39 +459,13 @@ 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 flex flex-row justify-between items-center gap-3'>
|
<div className='mb-6'>
|
||||||
<div>
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
List Daily Checklist
|
||||||
List Daily Checklist
|
</h1>
|
||||||
</h1>
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
Daftar semua checklist harian
|
||||||
Daftar semua checklist harian
|
</p>
|
||||||
</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 */}
|
||||||
@@ -722,10 +588,6 @@ 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':
|
||||||
@@ -804,76 +666,6 @@ 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'>
|
||||||
@@ -943,81 +735,6 @@ 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'>
|
||||||
|
|||||||
+33
-190
@@ -2,14 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
||||||
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';
|
||||||
@@ -60,7 +53,6 @@ interface ChecklistHeader {
|
|||||||
progress_percent: number;
|
progress_percent: number;
|
||||||
total_phases: number;
|
total_phases: number;
|
||||||
total_activities: number;
|
total_activities: number;
|
||||||
empty_kandang_end_date?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PhaseGroup {
|
interface PhaseGroup {
|
||||||
@@ -114,7 +106,6 @@ 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'];
|
||||||
@@ -146,8 +137,6 @@ 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();
|
||||||
@@ -180,9 +169,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
|
|
||||||
setDocuments(rawDetailChecklist?.document_urls || []);
|
setDocuments(rawDetailChecklist?.document_urls || []);
|
||||||
|
|
||||||
const emptyKandangEndDate =
|
|
||||||
rawDetailChecklist?.empty_kandang?.end_date ?? null;
|
|
||||||
|
|
||||||
const checklistData = {
|
const checklistData = {
|
||||||
id: rawDetailChecklist?.id,
|
id: rawDetailChecklist?.id,
|
||||||
date: rawDetailChecklist?.date,
|
date: rawDetailChecklist?.date,
|
||||||
@@ -209,7 +195,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: 0,
|
total_phases: 0,
|
||||||
total_activities: 0,
|
total_activities: 0,
|
||||||
empty_kandang_end_date: emptyKandangEndDate,
|
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -277,7 +262,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
||||||
total_activities: tasks.length,
|
total_activities: tasks.length,
|
||||||
empty_kandang_end_date: emptyKandangEndDate,
|
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -328,7 +312,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: progressPercent,
|
progress_percent: progressPercent,
|
||||||
total_phases: uniquePhases.size,
|
total_phases: uniquePhases.size,
|
||||||
total_activities: uniqueActivities.size,
|
total_activities: uniqueActivities.size,
|
||||||
empty_kandang_end_date: emptyKandangEndDate,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching checklist detail:', error);
|
console.error('Error fetching checklist detail:', error);
|
||||||
@@ -564,103 +547,6 @@ 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'>
|
||||||
@@ -687,8 +573,8 @@ export function DetailDailyChecklistContent() {
|
|||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Action Buttons */}
|
{/* Page Title with Back Button */}
|
||||||
<div className='mb-6 flex items-start sm:items-center justify-between gap-4 flex-wrap'>
|
<div className='mb-6 flex items-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -698,68 +584,37 @@ 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'>
|
||||||
<div className='flex items-center gap-2 flex-wrap'>
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
{header.status === 'SUBMITTED' && (
|
Detail Daily Checklist
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
</h1>
|
||||||
<div className='flex gap-2 flex-wrap'>
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
<Button
|
Lihat detail checklist harian
|
||||||
onClick={handleApprove}
|
</p>
|
||||||
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>
|
||||||
</div>
|
{header.status === 'SUBMITTED' && (
|
||||||
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
{/* Page Title */}
|
<div className='flex gap-2'>
|
||||||
<div className='mb-6'>
|
<Button
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
onClick={handleApprove}
|
||||||
Detail Daily Checklist
|
disabled={actionLoading}
|
||||||
</h1>
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
>
|
||||||
Lihat detail checklist harian
|
<CheckCircle className='w-4 h-4 mr-2' />
|
||||||
</p>
|
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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header Info Card */}
|
{/* Header Info Card */}
|
||||||
@@ -784,18 +639,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
{CATEGORY_LABELS[header.category] || header.category}
|
{CATEGORY_LABELS[header.category] || header.category}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{header.category === 'empty_kandang' && (
|
|
||||||
<div>
|
|
||||||
<Label className='text-xs text-gray-500'>
|
|
||||||
Tanggal Selesai Kandang Kosong
|
|
||||||
</Label>
|
|
||||||
<p className='text-sm font-medium text-gray-900 mt-1'>
|
|
||||||
{header.empty_kandang_end_date
|
|
||||||
? formatDate(header.empty_kandang_end_date)
|
|
||||||
: '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
<Label className='text-xs text-gray-500'>Status</Label>
|
<Label className='text-xs text-gray-500'>Status</Label>
|
||||||
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
||||||
|
|||||||
@@ -96,10 +96,7 @@ export function MasterEmployeeContent() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
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;
|
||||||
@@ -220,9 +217,7 @@ export function MasterEmployeeContent() {
|
|||||||
'Error creating employee:',
|
'Error creating employee:',
|
||||||
createEmployeeResponse.message
|
createEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error(
|
toast.error('Gagal menambahkan ABK');
|
||||||
'Gagal menambahkan ABK: ' + createEmployeeResponse.message
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +238,7 @@ export function MasterEmployeeContent() {
|
|||||||
'Error updating employee:',
|
'Error updating employee:',
|
||||||
updateEmployeeResponse.message
|
updateEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error(
|
toast.error('Gagal menambahkan ABK');
|
||||||
'Gagal memperbarui ABK: ' + updateEmployeeResponse.message
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ 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 { LocationApi } from '@/services/api/master-data';
|
import { KandangApi, 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() {
|
||||||
@@ -107,6 +108,12 @@ 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);
|
||||||
@@ -368,9 +375,7 @@ export function MasterKandangContent() {
|
|||||||
name='search'
|
name='search'
|
||||||
placeholder='Cari kandang...'
|
placeholder='Cari kandang...'
|
||||||
value={tableFilterState.search}
|
value={tableFilterState.search}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateFilter('search', e.target.value)}
|
||||||
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',
|
||||||
@@ -385,11 +390,7 @@ export function MasterKandangContent() {
|
|||||||
<Select
|
<Select
|
||||||
value={tableFilterState.location_id}
|
value={tableFilterState.location_id}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateFilter(
|
updateFilter('location_id', value === 'all' ? '' : value)
|
||||||
'location_id',
|
|
||||||
value === 'all' ? '' : value,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='w-[180px] border-gray-200'>
|
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user