mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-22 14:25:47 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 584b495e4b | |||
| c8e76a8558 | |||
| a468a5948b | |||
| e05e1a4121 | |||
| e3b86e3033 | |||
| d467c56ea6 | |||
| 784d9f26ab | |||
| 978ef764ea | |||
| 928136ff18 | |||
| 79567e4f1b | |||
| 633deece21 | |||
| 46483af4c2 | |||
| c2653e5068 | |||
| 8569bda7d6 | |||
| 7da63dc542 | |||
| aeceef4361 | |||
| ff6955be54 | |||
| eccab314b3 |
@@ -45,9 +45,3 @@ next-env.d.ts
|
|||||||
|
|
||||||
# claude
|
# claude
|
||||||
.claude
|
.claude
|
||||||
|
|
||||||
# rtk
|
|
||||||
rtk.exe
|
|
||||||
|
|
||||||
# local specs
|
|
||||||
/local-specs
|
|
||||||
+3
-50
@@ -15,7 +15,7 @@ default:
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
.build_template: &build_template
|
.build_template: &build_template
|
||||||
stage: build
|
stage: build
|
||||||
image: public.ecr.aws/docker/library/node:20-alpine
|
image: node:20-alpine
|
||||||
cache:
|
cache:
|
||||||
key: npm-cache
|
key: npm-cache
|
||||||
paths:
|
paths:
|
||||||
@@ -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:
|
||||||
@@ -64,7 +56,7 @@ default:
|
|||||||
.deploy_template: &deploy_template
|
.deploy_template: &deploy_template
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image:
|
image:
|
||||||
name: public.ecr.aws/aws-cli/aws-cli:latest
|
name: amazon/aws-cli:latest
|
||||||
entrypoint: ['/bin/sh', '-c']
|
entrypoint: ['/bin/sh', '-c']
|
||||||
script:
|
script:
|
||||||
- set -e
|
- set -e
|
||||||
@@ -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
|
||||||
@@ -198,35 +183,3 @@ deploy:staging:
|
|||||||
environment:
|
environment:
|
||||||
name: staging
|
name: staging
|
||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# ====== (Branch production) ======
|
|
||||||
# ==========================================================
|
|
||||||
build:production:
|
|
||||||
<<: *build_template
|
|
||||||
rules:
|
|
||||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
|
||||||
environment:
|
|
||||||
name: staging
|
|
||||||
variables:
|
|
||||||
NEXT_PUBLIC_LTI_URL: 'https://lti-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_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_template
|
|
||||||
needs: ['build:production']
|
|
||||||
rules:
|
|
||||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
|
||||||
variables:
|
|
||||||
S3_BUCKET: 'production-lti-erp.mbugroup.id'
|
|
||||||
AWS_REGION: 'ap-southeast-3'
|
|
||||||
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
|
|
||||||
environment:
|
|
||||||
name: staging
|
|
||||||
url: https://lti-erp.mbugroup.id
|
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npx tsc --noEmit
|
||||||
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 -->
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM public.ecr.aws/docker/library/node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
RUN apk add --no-cache git bash build-base curl
|
RUN apk add --no-cache git bash build-base curl
|
||||||
|
|
||||||
@@ -22,4 +22,4 @@ RUN mkdir -p .next/server/app/_next && \
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
||||||
+1
-3
@@ -7,10 +7,8 @@
|
|||||||
"build": "next build --turbopack",
|
"build": "next build --turbopack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"typecheck": "next typegen && tsc --noEmit",
|
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write ."
|
||||||
"pre-commit": "npm run format && npm run lint && npm run typecheck && npm run build"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.1",
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
|
|
||||||
|
|
||||||
const MasterKandangPage = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full'>
|
|
||||||
<MasterKandangContent />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MasterKandangPage;
|
|
||||||
@@ -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;
|
|
||||||
@@ -11,13 +11,10 @@ const RecordingEdit = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const recordingId = searchParams.get('recordingId');
|
const recordingId = searchParams.get('recordingId');
|
||||||
const recordingDetailKey = recordingId
|
|
||||||
? ['recording-detail', recordingId]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingDetailKey,
|
recordingId,
|
||||||
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
|
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ const RecordingDetail = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const recordingId = searchParams.get('recordingId');
|
const recordingId = searchParams.get('recordingId');
|
||||||
const recordingDetailKey = recordingId
|
|
||||||
? ['recording-detail', recordingId]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingDetailKey,
|
recordingId,
|
||||||
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
|
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -51,7 +51,7 @@ const Button = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(!href || (href && disabled)) && (
|
{!href && (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
type={type}
|
type={type}
|
||||||
@@ -68,9 +68,9 @@ const Button = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{href && !disabled && (
|
{href && (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={disabled ? '#' : href}
|
||||||
target={target}
|
target={target}
|
||||||
rel={rel}
|
rel={rel}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ const NumberInput = ({
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (newChangeEvent) {
|
if (newChangeEvent) {
|
||||||
newChangeEvent.target.value = parseFloat(
|
newChangeEvent.target.value = numberFormatValues.value;
|
||||||
numberFormatValues.value
|
|
||||||
) as unknown as string;
|
|
||||||
|
|
||||||
onChange?.(newChangeEvent);
|
onChange?.(newChangeEvent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import {
|
|||||||
} from '@/types/api/api-general';
|
} from '@/types/api/api-general';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
export interface OptionType<T = string | number> {
|
export interface OptionType {
|
||||||
value: T;
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
@@ -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();
|
||||||
@@ -566,31 +566,23 @@ const useSelect = <T,>(
|
|||||||
setSize(size + 1);
|
setSize(size + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
|
||||||
|
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
|
||||||
|
|
||||||
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
|
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
|
||||||
|
|
||||||
const { formattedSuccessRawData, formattedErrorRawData } = useMemo(() => {
|
if (isResponseSuccess(pages?.[latestPagesIndex])) {
|
||||||
let successData: SuccessApiResponse<T[]> | undefined = undefined;
|
formattedSuccessRawData = {
|
||||||
let errorData: ErrorApiResponse | undefined = undefined;
|
...pages?.[latestPagesIndex],
|
||||||
|
data:
|
||||||
if (isResponseSuccess(pages?.[latestPagesIndex])) {
|
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
|
||||||
successData = {
|
[],
|
||||||
...pages![latestPagesIndex],
|
|
||||||
data:
|
|
||||||
pages?.flatMap((page) =>
|
|
||||||
isResponseSuccess(page) ? page.data : []
|
|
||||||
) ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResponseError(pages?.[latestPagesIndex])) {
|
|
||||||
errorData = pages![latestPagesIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
formattedSuccessRawData: successData,
|
|
||||||
formattedErrorRawData: errorData,
|
|
||||||
};
|
};
|
||||||
}, [pages, latestPagesIndex]);
|
}
|
||||||
|
|
||||||
|
if (isResponseError(pages?.[latestPagesIndex])) {
|
||||||
|
formattedErrorRawData = pages?.[latestPagesIndex];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputValue,
|
inputValue,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -112,11 +112,12 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
kandangData={kandangData}
|
kandangData={kandangData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ClosingKandangList
|
{!kandangData && (
|
||||||
initialValue={initialValue}
|
<ClosingKandangList
|
||||||
projectData={projectData}
|
initialValue={initialValue}
|
||||||
selectedKandangId={kandangData?.id}
|
projectData={projectData}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
|
|||||||
const ClosingKandangList = ({
|
const ClosingKandangList = ({
|
||||||
initialValue,
|
initialValue,
|
||||||
projectData,
|
projectData,
|
||||||
selectedKandangId,
|
|
||||||
}: {
|
}: {
|
||||||
initialValue?: ClosingGeneralInformation;
|
initialValue?: ClosingGeneralInformation;
|
||||||
projectData?: ProjectFlock;
|
projectData?: ProjectFlock;
|
||||||
selectedKandangId?: number;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
|
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
|
||||||
@@ -24,9 +22,6 @@ const ClosingKandangList = ({
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
|
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
|
||||||
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
||||||
disabled={
|
|
||||||
selectedKandangId === kandang.project_flock_kandang_id
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{kandang.name}
|
{kandang.name}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
|
|||||||
{
|
{
|
||||||
id: 'kandang',
|
id: 'kandang',
|
||||||
accessorKey: 'kandang',
|
accessorKey: 'kandang',
|
||||||
header: 'Kandang Atribusi',
|
header: 'Kandang',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const kandang = props.getValue() as Kandang;
|
const kandang = props.getValue() as Kandang;
|
||||||
return kandang?.name || '-';
|
return kandang?.name || '-';
|
||||||
|
|||||||
@@ -127,11 +127,11 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'source_warehouse',
|
accessorKey: 'source_warehouse',
|
||||||
header: 'Gudang Asal (Fisik)',
|
header: 'Gudang Asal',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'destination_warehouse',
|
accessorKey: 'destination_warehouse',
|
||||||
header: 'Gudang Tujuan (Fisik)',
|
header: 'Gudang Tujuan',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'quantity',
|
accessorKey: 'quantity',
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { DashboardApi } from '@/services/api/dashboard';
|
import { DashboardApi } from '@/services/api/dashboard';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import {
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
ProjectFlockApi,
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
ProjectFlockKandangApi,
|
|
||||||
} from '@/services/api/production';
|
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
|
||||||
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
|
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
|
||||||
import {
|
import {
|
||||||
DashboardFilterType,
|
DashboardFilterType,
|
||||||
@@ -25,7 +22,10 @@ import DashboardExportCharts, {
|
|||||||
DashboardExportChartsRef,
|
DashboardExportChartsRef,
|
||||||
} from '@/components/pages/dashboard/export/DashboardExportCharts';
|
} from '@/components/pages/dashboard/export/DashboardExportCharts';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
||||||
import { DashboardMeta } from '@/types/api/dashboard/dashboard';
|
import {
|
||||||
|
DashboardFilter,
|
||||||
|
DashboardMeta,
|
||||||
|
} from '@/types/api/dashboard/dashboard';
|
||||||
import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
|
import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
@@ -42,8 +42,6 @@ import { cn } from '@/lib/helper';
|
|||||||
import DashboardExportStats, {
|
import DashboardExportStats, {
|
||||||
DashboardExportStatsRef,
|
DashboardExportStatsRef,
|
||||||
} from '@/components/pages/dashboard/export/DashboardExportStats';
|
} from '@/components/pages/dashboard/export/DashboardExportStats';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
|
||||||
|
|
||||||
// Helper function to normalize values to array
|
// Helper function to normalize values to array
|
||||||
const normalizeToArray = (
|
const normalizeToArray = (
|
||||||
@@ -70,6 +68,7 @@ const DashboardProduction = () => {
|
|||||||
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
||||||
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
||||||
);
|
);
|
||||||
|
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
||||||
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
|
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
|
||||||
normalizeToArray(filterValues.location)
|
normalizeToArray(filterValues.location)
|
||||||
);
|
);
|
||||||
@@ -81,29 +80,9 @@ const DashboardProduction = () => {
|
|||||||
const {
|
const {
|
||||||
data: dashboardProductionResponse,
|
data: dashboardProductionResponse,
|
||||||
isLoading: isLoadingDashboardProductionData,
|
isLoading: isLoadingDashboardProductionData,
|
||||||
} = useSWR(
|
mutate: refreshDashboardProductionData,
|
||||||
[
|
} = useSWR(endpointUrl, () =>
|
||||||
'dashboard-production',
|
DashboardApi.getDashboardProductionFetcher(endpointUrl)
|
||||||
filterValues.startDate ?? '',
|
|
||||||
filterValues.endDate ?? '',
|
|
||||||
filterValues.analysisMode ?? 'OVERVIEW',
|
|
||||||
normalizeToArray(filterValues.location).toString(),
|
|
||||||
normalizeToArray(filterValues.flock).toString(),
|
|
||||||
normalizeToArray(filterValues.kandang).toString(),
|
|
||||||
filterValues.comparisonType ?? '',
|
|
||||||
],
|
|
||||||
() =>
|
|
||||||
DashboardApi.getDashboardProductionFetcher({
|
|
||||||
start_date: filterValues.startDate || '',
|
|
||||||
end_date: filterValues.endDate || '',
|
|
||||||
analysis_mode:
|
|
||||||
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') ||
|
|
||||||
'OVERVIEW',
|
|
||||||
location_ids: normalizeToArray(filterValues.location),
|
|
||||||
flock_ids: normalizeToArray(filterValues.flock),
|
|
||||||
kandang_ids: normalizeToArray(filterValues.kandang),
|
|
||||||
comparison_type: filterValues.comparisonType || '',
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const dashboardProductionData = isResponseSuccess(dashboardProductionResponse)
|
const dashboardProductionData = isResponseSuccess(dashboardProductionResponse)
|
||||||
@@ -116,23 +95,23 @@ const DashboardProduction = () => {
|
|||||||
options: flockOptions,
|
options: flockOptions,
|
||||||
isLoadingOptions: isLoadingFlockOptions,
|
isLoadingOptions: isLoadingFlockOptions,
|
||||||
loadMore: loadMoreFlock,
|
loadMore: loadMoreFlock,
|
||||||
} = useSelect<ProjectFlock>(
|
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
||||||
ProjectFlockApi.basePath,
|
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||||
'id',
|
});
|
||||||
'flock_name',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setInputValueLocation,
|
setInputValue: setInputValueLocation,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
loadMore: loadMoreLocation,
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name');
|
} = useSelect(LocationApi.basePath, 'id', 'name');
|
||||||
|
const {
|
||||||
|
setInputValue: setInputValueKandang,
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangOptions,
|
||||||
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
|
||||||
|
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||||
|
});
|
||||||
const comparisonTypeOptions = [
|
const comparisonTypeOptions = [
|
||||||
{ value: 'FARM', label: 'Farm' },
|
{ value: 'FARM', label: 'Farm' },
|
||||||
{ value: 'FLOCK', label: 'Flock' },
|
{ value: 'FLOCK', label: 'Flock' },
|
||||||
@@ -156,43 +135,68 @@ const DashboardProduction = () => {
|
|||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: getDashboardFilterSchema(analysisMode),
|
validationSchema: getDashboardFilterSchema(analysisMode),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
|
// Save filter values to store
|
||||||
setFilterValues(values);
|
setFilterValues(values);
|
||||||
filterModal.closeModal();
|
|
||||||
|
handleApplyFilter({
|
||||||
|
start_date: values.startDate || '',
|
||||||
|
end_date: values.endDate || '',
|
||||||
|
analysis_mode: values.analysisMode as 'OVERVIEW' | 'COMPARISON',
|
||||||
|
location_ids: normalizeToArray(values.location),
|
||||||
|
flock_ids: normalizeToArray(values.flock),
|
||||||
|
kandang_ids: normalizeToArray(values.kandang),
|
||||||
|
comparison_type: values.comparisonType,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resetForm } = formik;
|
const { resetForm } = formik;
|
||||||
|
|
||||||
const selectedLocationValues = normalizeToArray(formik.values.location);
|
|
||||||
const selectedFlockValues = normalizeToArray(formik.values.flock);
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setInputValueKandang,
|
|
||||||
options: kandangOptions,
|
|
||||||
isLoadingOptions: isLoadingKandangOptions,
|
|
||||||
loadMore: loadMoreKandang,
|
|
||||||
} = useSelect<ProjectFlockKandang>(
|
|
||||||
ProjectFlockKandangApi.basePath,
|
|
||||||
'kandang_id',
|
|
||||||
'kandang.name',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
location_id:
|
|
||||||
selectedLocationValues.length > 0
|
|
||||||
? selectedLocationValues.toString()
|
|
||||||
: '',
|
|
||||||
project_flock_id:
|
|
||||||
selectedFlockValues.length > 0 ? selectedFlockValues.toString() : '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResetFilter = useCallback(() => {
|
const handleResetFilter = useCallback(() => {
|
||||||
resetForm();
|
resetForm();
|
||||||
resetFilterValues(); // Clear stored filter values
|
resetFilterValues(); // Clear stored filter values
|
||||||
setAnalysisMode('OVERVIEW');
|
setAnalysisMode('OVERVIEW');
|
||||||
|
setEndpointUrl('/dashboards');
|
||||||
setSelectedLocationIds([]);
|
setSelectedLocationIds([]);
|
||||||
filterModal.closeModal();
|
}, [resetForm, resetFilterValues]);
|
||||||
}, [filterModal, resetForm, resetFilterValues]);
|
|
||||||
|
const handleApplyFilter = useCallback(
|
||||||
|
(values: DashboardFilter) => {
|
||||||
|
// Build query params object, only include non-empty values
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (values.start_date) params.start_date = values.start_date;
|
||||||
|
if (values.end_date) params.end_date = values.end_date;
|
||||||
|
if (values.analysis_mode) params.analysis_mode = values.analysis_mode;
|
||||||
|
if (values.location_ids.length > 0)
|
||||||
|
params.location_ids = values.location_ids.toString();
|
||||||
|
if (values.flock_ids.length > 0)
|
||||||
|
params.flock_ids = values.flock_ids.toString();
|
||||||
|
if (values.kandang_ids.length > 0)
|
||||||
|
params.kandang_ids = values.kandang_ids.toString();
|
||||||
|
if (values.comparison_type)
|
||||||
|
params.comparison_type = values.comparison_type;
|
||||||
|
|
||||||
|
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
||||||
|
filterModal.closeModal();
|
||||||
|
refreshDashboardProductionData();
|
||||||
|
},
|
||||||
|
[filterModal, refreshDashboardProductionData]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== Load filter from store on mount =====
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filterValues) return;
|
||||||
|
handleApplyFilter({
|
||||||
|
start_date: filterValues.startDate,
|
||||||
|
end_date: filterValues.endDate,
|
||||||
|
analysis_mode: filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON',
|
||||||
|
location_ids: normalizeToArray(filterValues.location),
|
||||||
|
flock_ids: normalizeToArray(filterValues.flock),
|
||||||
|
kandang_ids: normalizeToArray(filterValues.kandang),
|
||||||
|
comparison_type: filterValues.comparisonType,
|
||||||
|
});
|
||||||
|
}, [filterValues, handleApplyFilter]);
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
@@ -264,6 +268,14 @@ const DashboardProduction = () => {
|
|||||||
};
|
};
|
||||||
}, [clearNavbarActions]);
|
}, [clearNavbarActions]);
|
||||||
|
|
||||||
|
if (isLoadingDashboardProductionData) {
|
||||||
|
return (
|
||||||
|
<div className='w-full min-h-screen flex items-center justify-center'>
|
||||||
|
<span className='loading loading-spinner loading-xl'></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full p-3 space-y-3'>
|
<section className='w-full p-3 space-y-3'>
|
||||||
@@ -315,15 +327,9 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* Dashboard Stats */}
|
{/* Dashboard Stats */}
|
||||||
<div>
|
<div>
|
||||||
{isLoadingDashboardProductionData ? (
|
<DashboardStats
|
||||||
<div className='w-full min-h-screen flex items-center justify-center'>
|
data={dashboardProductionData?.statistics_data ?? []}
|
||||||
<span className='loading loading-spinner loading-xl'></span>
|
/>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<DashboardStats
|
|
||||||
data={dashboardProductionData?.statistics_data ?? []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Use DashboardLineChart component or skeleton */}
|
{/* Use DashboardLineChart component or skeleton */}
|
||||||
@@ -531,7 +537,6 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
isClearable={true}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -568,7 +573,6 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
isClearable={true}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -600,7 +604,6 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
isClearable={true}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -640,7 +643,6 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
isClearable={true}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -667,7 +669,6 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
isClearable={true}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -706,7 +707,6 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
isClearable={true}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -733,7 +733,6 @@ const DashboardProduction = () => {
|
|||||||
className={{
|
className={{
|
||||||
select: 'rounded-lg text-sm border-base-content/10',
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
}}
|
}}
|
||||||
isClearable={true}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -16,7 +15,6 @@ interface ExpenseDetailProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 expenseDetailTabs = useMemo(() => {
|
const expenseDetailTabs = useMemo(() => {
|
||||||
@@ -48,8 +46,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='/expense'
|
||||||
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} />
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -19,7 +16,6 @@ import {
|
|||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
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 { buildExpenseActionHref } from '@/lib/expense-list-navigation';
|
|
||||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
|
|
||||||
interface ExpenseRealizationContentProps {
|
interface ExpenseRealizationContentProps {
|
||||||
@@ -29,8 +25,6 @@ interface ExpenseRealizationContentProps {
|
|||||||
const ExpenseRealizationContent = ({
|
const ExpenseRealizationContent = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRealizationContentProps) => {
|
}: ExpenseRealizationContentProps) => {
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
documents: [],
|
documents: [],
|
||||||
@@ -80,11 +74,7 @@ const ExpenseRealizationContent = ({
|
|||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
href={buildExpenseActionHref(
|
href={`/expense/realization/edit/?expenseId=${initialValues?.id}`}
|
||||||
'/expense/realization/edit/',
|
|
||||||
initialValues?.id as number,
|
|
||||||
searchParams
|
|
||||||
)}
|
|
||||||
className='px-4 grow sm:grow-0'
|
className='px-4 grow sm:grow-0'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter } 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,15 +26,11 @@ 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';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
|
||||||
buildExpenseActionHref,
|
|
||||||
getExpenseListReturnTo,
|
|
||||||
} from '@/lib/expense-list-navigation';
|
|
||||||
|
|
||||||
interface ExpenseRequestContentProps {
|
interface ExpenseRequestContentProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
@@ -46,13 +40,6 @@ const ExpenseRequestContent = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRequestContentProps) => {
|
}: ExpenseRequestContentProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
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 +89,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 +140,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);
|
||||||
|
|
||||||
@@ -192,7 +148,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
if (isResponseSuccess(deleteResponse)) {
|
if (isResponseSuccess(deleteResponse)) {
|
||||||
toast.success('Berhasil menghapus data biaya operasional!');
|
toast.success('Berhasil menghapus data biaya operasional!');
|
||||||
router.push(returnTo);
|
router.push('/expense');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menghapus data biaya operasional!');
|
toast.error('Gagal menghapus data biaya operasional!');
|
||||||
}
|
}
|
||||||
@@ -208,7 +164,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
if (isResponseSuccess(completeRes)) {
|
if (isResponseSuccess(completeRes)) {
|
||||||
toast.success(completeRes.message);
|
toast.success(completeRes.message);
|
||||||
router.push(returnTo);
|
router.push('/expense');
|
||||||
} else {
|
} else {
|
||||||
toast.error(completeRes?.message as string);
|
toast.error(completeRes?.message as string);
|
||||||
}
|
}
|
||||||
@@ -248,7 +204,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
toast.success(approveResponse?.message);
|
toast.success(approveResponse?.message);
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
router.push(returnTo);
|
router.push('/expense');
|
||||||
} else {
|
} else {
|
||||||
approveModal.closeModal();
|
approveModal.closeModal();
|
||||||
|
|
||||||
@@ -283,7 +239,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
toast.success(rejectResponse.message);
|
toast.success(rejectResponse.message);
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
router.push(returnTo);
|
router.push('/expense');
|
||||||
} else {
|
} else {
|
||||||
rejectModal.closeModal();
|
rejectModal.closeModal();
|
||||||
|
|
||||||
@@ -323,6 +279,8 @@ const ExpenseRequestContent = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='w-full mt-4 flex flex-col gap-4'>
|
<div className='w-full mt-4 flex flex-col gap-4'>
|
||||||
|
{/* TODO: apply RBAC */}
|
||||||
|
|
||||||
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
{isCurrentApprovalOnHeadArea && (
|
{isCurrentApprovalOnHeadArea && (
|
||||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
@@ -409,11 +367,7 @@ const ExpenseRequestContent = ({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='info'
|
color='info'
|
||||||
href={buildExpenseActionHref(
|
href={`/expense/realization/?expenseId=${initialValues?.id}`}
|
||||||
'/expense/realization/',
|
|
||||||
initialValues?.id as number,
|
|
||||||
searchParams
|
|
||||||
)}
|
|
||||||
className='w-full sm:w-fit'
|
className='w-full sm:w-fit'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -426,35 +380,13 @@ 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'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
href={buildExpenseActionHref(
|
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
|
||||||
'/expense/detail/edit/',
|
|
||||||
initialValues?.id as number,
|
|
||||||
searchParams
|
|
||||||
)}
|
|
||||||
className='px-4 grow sm:grow-0'
|
className='px-4 grow sm:grow-0'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
@@ -589,19 +521,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 +536,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 +746,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,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ import { isResponseError } from '@/lib/api-helper';
|
|||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
|
||||||
@@ -49,8 +48,6 @@ const ExpenseRealizationForm = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: ExpenseRealizationFormProps) => {
|
}: ExpenseRealizationFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const returnTo = getExpenseListReturnTo(searchParams);
|
|
||||||
|
|
||||||
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
|
||||||
|
|
||||||
@@ -67,9 +64,9 @@ const ExpenseRealizationForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success(createExpenseRes?.message as string);
|
toast.success(createExpenseRes?.message as string);
|
||||||
router.push(returnTo);
|
router.push('/expense');
|
||||||
},
|
},
|
||||||
[initialValues?.id, returnTo, router]
|
[router, initialValues?.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateExpenseHandler = useCallback(
|
const updateExpenseHandler = useCallback(
|
||||||
@@ -86,9 +83,9 @@ const ExpenseRealizationForm = ({
|
|||||||
|
|
||||||
toast.success(updateExpenseRes?.message as string);
|
toast.success(updateExpenseRes?.message as string);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
router.push(returnTo);
|
router.push('/expense');
|
||||||
},
|
},
|
||||||
[returnTo, router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formik = useFormik<ExpenseRealizationFormValues>({
|
const formik = useFormik<ExpenseRealizationFormValues>({
|
||||||
@@ -210,7 +207,7 @@ const ExpenseRealizationForm = ({
|
|||||||
|
|
||||||
// add new realizations for each kandang
|
// add new realizations for each kandang
|
||||||
kandangs.forEach((kandangItem) => {
|
kandangs.forEach((kandangItem) => {
|
||||||
if (isNaN(Number(kandangItem.id))) return;
|
if (!kandangItem.id) return;
|
||||||
|
|
||||||
const existingRealization = formik.values.realizations?.find(
|
const existingRealization = formik.values.realizations?.find(
|
||||||
(realizationItem) => realizationItem.kandang_id === kandangItem.id
|
(realizationItem) => realizationItem.kandang_id === kandangItem.id
|
||||||
@@ -261,7 +258,7 @@ const ExpenseRealizationForm = ({
|
|||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href={returnTo}
|
href='/expense'
|
||||||
variant='link'
|
variant='link'
|
||||||
className='w-fit p-0 text-primary'
|
className='w-fit p-0 text-primary'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
|
|||||||
setInputValue: setNonstockInputValue,
|
setInputValue: setNonstockInputValue,
|
||||||
options: nonstockOptions,
|
options: nonstockOptions,
|
||||||
isLoadingOptions: isLoadingNonstockOptions,
|
isLoadingOptions: isLoadingNonstockOptions,
|
||||||
loadMore: loadMoreNonstocks,
|
|
||||||
} = useSelect<Nonstock>(
|
} = useSelect<Nonstock>(
|
||||||
NonstockApi.basePath,
|
NonstockApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
@@ -165,7 +164,6 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
|
|||||||
options={nonstockOptions}
|
options={nonstockOptions}
|
||||||
isLoading={isLoadingNonstockOptions}
|
isLoading={isLoadingNonstockOptions}
|
||||||
onInputChange={setNonstockInputValue}
|
onInputChange={setNonstockInputValue}
|
||||||
onMenuScrollToBottom={loadMoreNonstocks}
|
|
||||||
className={{ wrapper: 'min-w-48' }}
|
className={{ wrapper: 'min-w-48' }}
|
||||||
isDisabled
|
isDisabled
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -178,14 +178,14 @@ const ExpenseRequestForm = ({
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
loadMore: loadMoreLocations,
|
loadMore: loadMoreLocationOptions,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: supplierOptions,
|
options: supplierOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
loadMore: loadMoreSuppliers,
|
loadMore: loadMoreVendorOptions,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -410,13 +410,13 @@ const ExpenseRequestForm = ({
|
|||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
|
||||||
isError={
|
isError={
|
||||||
formik.touched.location_id && Boolean(formik.errors.location_id)
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.location_id as string}
|
errorMessage={formik.errors.location_id as string}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||||
|
onMenuScrollToBottom={loadMoreLocationOptions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
@@ -455,12 +455,12 @@ const ExpenseRequestForm = ({
|
|||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
isLoading={isLoadingVendorOptions}
|
isLoading={isLoadingVendorOptions}
|
||||||
onMenuScrollToBottom={loadMoreSuppliers}
|
|
||||||
isError={
|
isError={
|
||||||
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.supplier_id as string}
|
errorMessage={formik.errors.supplier_id as string}
|
||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
|
onMenuScrollToBottom={loadMoreVendorOptions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.document'>
|
<RequirePermission permissions='lti.expense.document'>
|
||||||
|
|||||||
@@ -1,212 +1,154 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import React from 'react';
|
||||||
Document,
|
import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
|
||||||
Image,
|
|
||||||
Link,
|
|
||||||
Page,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from '@react-pdf/renderer';
|
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||||
|
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||||
|
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
|
||||||
|
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
||||||
|
|
||||||
interface ExpensePDFProps {
|
interface ExpensePDFProps {
|
||||||
expense?: Expense;
|
expense?: Expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExpensePDFStyle = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
paddingTop: 24,
|
fontSize: 10,
|
||||||
paddingBottom: 64,
|
fontFamily: 'Helvetica',
|
||||||
paddingHorizontal: 32,
|
padding: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
},
|
},
|
||||||
|
titleSection: {
|
||||||
companyInfoHeader: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
companyLogo: {
|
|
||||||
width: 64,
|
|
||||||
height: 'auto',
|
|
||||||
},
|
|
||||||
companyInfoHeaderDate: {
|
|
||||||
paddingTop: 8,
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
companyName: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
companyAddress: {
|
|
||||||
fontSize: 8,
|
|
||||||
maxWidth: 400,
|
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
|
parameterContainer: {
|
||||||
title: {
|
|
||||||
marginTop: 16,
|
|
||||||
fontSize: 16,
|
|
||||||
lineHeight: '150%',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontFamily: 'Times-Roman',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
|
|
||||||
footer: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
flexWrap: 'wrap',
|
||||||
alignItems: 'center',
|
marginBottom: 8,
|
||||||
paddingHorizontal: 32,
|
},
|
||||||
|
infoTableSection: {
|
||||||
position: 'absolute',
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
infoTableTitle: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
bottom: 30,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'grey',
|
|
||||||
},
|
|
||||||
|
|
||||||
// wrapper
|
|
||||||
generalInfoTable: {
|
|
||||||
width: '100%',
|
|
||||||
marginTop: 8,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#000000',
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
|
|
||||||
generalInfoTableRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#000000',
|
|
||||||
},
|
|
||||||
|
|
||||||
// columns
|
|
||||||
generalInfoTableColLabel: {
|
|
||||||
width: '30%',
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
},
|
|
||||||
generalInfoTableColSeparator: {
|
|
||||||
width: '3%',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: 6,
|
|
||||||
},
|
|
||||||
generalInfoTableColValue: {
|
|
||||||
width: '67%',
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
},
|
|
||||||
|
|
||||||
generalInfoTableLabelText: {
|
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 6,
|
||||||
|
color: '#333',
|
||||||
},
|
},
|
||||||
generalInfoTableValueText: {},
|
tableSection: {
|
||||||
|
marginBottom: 12,
|
||||||
// expense detail table
|
|
||||||
expenseDetailContainer: {
|
|
||||||
width: '100%',
|
|
||||||
marginTop: 12,
|
|
||||||
},
|
},
|
||||||
expenseDetailTitle: {
|
tableTitle: {
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: '150%',
|
|
||||||
fontFamily: 'Times-Roman',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
kandangExpenseContainer: {
|
|
||||||
width: '100%',
|
|
||||||
marginTop: 8,
|
|
||||||
},
|
|
||||||
kandangExpenseTitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: '150%',
|
|
||||||
fontFamily: 'Times-Roman',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
kandangExpenseTable: {
|
|
||||||
width: '100%',
|
|
||||||
marginTop: 8,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#000000',
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
kandangExpenseTableRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#000000',
|
|
||||||
},
|
|
||||||
kandangExpenseTableColLabel: {
|
|
||||||
width: '20%',
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
},
|
|
||||||
kandangExpenseTableColLabelBorderRight: {
|
|
||||||
borderRight: '1px solid #000000',
|
|
||||||
},
|
|
||||||
kandangExpenseTableColNonstock: {
|
|
||||||
width: '20%',
|
|
||||||
},
|
|
||||||
kandangExpenseTableColNote: {
|
|
||||||
width: '40%',
|
|
||||||
},
|
|
||||||
kandangExpenseHeaderLabelText: {
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
kandangExpenseLabelText: {
|
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 6,
|
||||||
|
color: '#333',
|
||||||
},
|
},
|
||||||
kandangExpenseTableFooterColTotalExpenseCaption: {
|
emptyText: {
|
||||||
width: '40%',
|
fontSize: 8,
|
||||||
paddingVertical: 6,
|
color: '#666',
|
||||||
paddingHorizontal: 8,
|
fontStyle: 'italic',
|
||||||
textAlign: 'right',
|
|
||||||
},
|
|
||||||
kandangExpenseTableFooterColTotalExpenseValue: {
|
|
||||||
width: '60%',
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
},
|
|
||||||
|
|
||||||
// utils
|
|
||||||
doubleDivider: {
|
|
||||||
width: '100%',
|
|
||||||
height: 6,
|
|
||||||
borderTop: '2px solid black',
|
|
||||||
borderBottom: '2px solid black',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
type ExpenseKandang = Expense['kandangs'][number];
|
||||||
|
type PengajuanItem = NonNullable<ExpenseKandang['pengajuans']>[number];
|
||||||
|
type RealisasiItem = NonNullable<ExpenseKandang['realisasi']>[number];
|
||||||
|
|
||||||
|
const valueText = (v: unknown) => {
|
||||||
|
if (v === null || v === undefined) return '-';
|
||||||
|
if (typeof v === 'number') return formatNumber(v);
|
||||||
|
return String(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPengajuanColumns = (): PdfColumn<PengajuanItem>[] => [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'nonstock',
|
||||||
|
header: 'Nonstock',
|
||||||
|
flex: 1.5,
|
||||||
|
cell: ({ row }) => row.nonstock.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'qty',
|
||||||
|
header: 'Kuantitas',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.qty),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'price',
|
||||||
|
header: 'Harga Satuan',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.price),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notes',
|
||||||
|
header: 'Catatan',
|
||||||
|
flex: 1.5,
|
||||||
|
cell: ({ row }) => row.notes || '-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getRealisasiColumns = (): PdfColumn<RealisasiItem>[] => [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'nonstock',
|
||||||
|
header: 'Nonstock',
|
||||||
|
flex: 1.5,
|
||||||
|
cell: ({ row }) => row.nonstock.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'qty',
|
||||||
|
header: 'Kuantitas',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.qty),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'price',
|
||||||
|
header: 'Harga Satuan',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.price),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notes',
|
||||||
|
header: 'Catatan',
|
||||||
|
flex: 1.5,
|
||||||
|
cell: ({ row }) => row.notes || '-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getInfoTableRows = (expense?: Expense) => {
|
||||||
const isLatestApprovalRejected =
|
const isLatestApprovalRejected =
|
||||||
expense?.latest_approval?.action === 'REJECTED';
|
expense?.latest_approval?.action === 'REJECTED';
|
||||||
const isExpenseRealized =
|
const isExpenseRealized =
|
||||||
expense?.latest_approval?.step_number &&
|
expense?.latest_approval?.step_number &&
|
||||||
expense?.latest_approval.step_number >= 5;
|
expense?.latest_approval.step_number >= 5;
|
||||||
|
|
||||||
const realizationStatus = isExpenseRealized
|
const realizationStatus = isExpenseRealized
|
||||||
? 'Sudah Realisasi'
|
? 'Sudah Realisasi'
|
||||||
: 'Belum Realisasi';
|
: 'Belum Realisasi';
|
||||||
|
|
||||||
const rows = [
|
return [
|
||||||
{ label: 'Nomor PO', value: expense?.po_number },
|
{ label: 'Nomor PO', value: expense?.po_number || '-' },
|
||||||
{ label: 'Nomor Referensi', value: expense?.reference_number },
|
{ label: 'Nomor Referensi', value: expense?.reference_number || '-' },
|
||||||
{
|
{
|
||||||
label: 'Kategori',
|
label: 'Kategori',
|
||||||
value:
|
value:
|
||||||
@@ -214,9 +156,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
? 'Biaya Operasional'
|
? 'Biaya Operasional'
|
||||||
: expense?.category === 'NON-BOP'
|
: expense?.category === 'NON-BOP'
|
||||||
? 'Non Biaya Operasional'
|
? 'Non Biaya Operasional'
|
||||||
: '',
|
: '-',
|
||||||
},
|
},
|
||||||
{ label: 'Lokasi', value: expense?.location.name },
|
{ label: 'Lokasi', value: expense?.location?.name || '-' },
|
||||||
{
|
{
|
||||||
label: 'Kandang',
|
label: 'Kandang',
|
||||||
value:
|
value:
|
||||||
@@ -227,7 +169,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
.join(', ')
|
.join(', ')
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{ label: 'Vendor', value: expense?.supplier.name },
|
{ label: 'Vendor', value: expense?.supplier?.name || '-' },
|
||||||
{
|
{
|
||||||
label: 'Tanggal Transaksi',
|
label: 'Tanggal Transaksi',
|
||||||
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
|
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
|
||||||
@@ -238,12 +180,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
? formatDate(expense?.realization_date, 'DD MMMM YYYY')
|
? formatDate(expense?.realization_date, 'DD MMMM YYYY')
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{ label: 'Nama Pengaju', value: expense?.created_user.name },
|
{ label: 'Nama Pengaju', value: expense?.created_user?.name || '-' },
|
||||||
{
|
{
|
||||||
label: 'Nominal Biaya',
|
label: 'Nominal Biaya',
|
||||||
value: formatCurrency(
|
value: formatCurrency(
|
||||||
expense?.latest_approval.step_number === 5 ||
|
expense?.latest_approval?.step_number === 5 ||
|
||||||
expense?.latest_approval.step_number === 6
|
expense?.latest_approval?.step_number === 6
|
||||||
? (expense?.total_realisasi ?? 0)
|
? (expense?.total_realisasi ?? 0)
|
||||||
: (expense?.total_pengajuan ?? 0)
|
: (expense?.total_pengajuan ?? 0)
|
||||||
),
|
),
|
||||||
@@ -263,401 +205,136 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
label: 'Status Biaya',
|
label: 'Status Biaya',
|
||||||
value: isLatestApprovalRejected
|
value: isLatestApprovalRejected
|
||||||
? 'Ditolak'
|
? 'Ditolak'
|
||||||
: expense?.latest_approval?.step_name,
|
: expense?.latest_approval?.step_name || '-',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface InfoRow {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInfoTableColumns = (): PdfColumn<InfoRow>[] => [
|
||||||
|
{
|
||||||
|
key: 'label',
|
||||||
|
header: 'Field',
|
||||||
|
flex: 1,
|
||||||
|
cell: ({ row }) => row.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'value',
|
||||||
|
header: 'Value',
|
||||||
|
flex: 2,
|
||||||
|
cell: ({ row }) => row.value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||||
|
const kandangs = expense?.kandangs || [];
|
||||||
|
const infoRows = getInfoTableRows(expense);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page style={ExpensePDFStyle.page}>
|
<Page style={styles.page} size='A4'>
|
||||||
<View>
|
{/* Title Section */}
|
||||||
<View style={ExpensePDFStyle.companyInfoHeader}>
|
<View style={styles.titleSection}>
|
||||||
<Image
|
<PdfTypography size='h1' variant='primary'>
|
||||||
style={ExpensePDFStyle.companyLogo}
|
Laporan{' '}
|
||||||
src='/assets/img/lti-logo.png'
|
{expense?.category === 'BOP'
|
||||||
/>
|
? 'Biaya Operasional'
|
||||||
|
: 'Non-Biaya Operasional'}
|
||||||
<Text style={ExpensePDFStyle.companyInfoHeaderDate}>
|
</PdfTypography>
|
||||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
<PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography>
|
||||||
</Text>
|
<View style={styles.parameterContainer}>
|
||||||
</View>
|
<PdfParamBadge>
|
||||||
|
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
<View>
|
</PdfParamBadge>
|
||||||
<Text style={ExpensePDFStyle.companyName}>
|
<PdfParamBadge>
|
||||||
PT LUMBUNG TELUR INDONESIA
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
</Text>
|
</PdfParamBadge>
|
||||||
<Text style={ExpensePDFStyle.companyAddress}>
|
|
||||||
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
|
|
||||||
Bandung Barat, Jawa Barat 40514
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={ExpensePDFStyle.doubleDivider} />
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={ExpensePDFStyle.title}>
|
{/* Info Table Section */}
|
||||||
Laporan{' '}
|
<View style={styles.infoTableSection}>
|
||||||
{expense?.category === 'BOP'
|
<Text style={styles.infoTableTitle}>Informasi Biaya</Text>
|
||||||
? 'Biaya Operasional'
|
<PdfTable columns={getInfoTableColumns()} data={infoRows} />
|
||||||
: 'Non-Biaya Operasional'}{' '}
|
|
||||||
{expense?.po_number}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* General info table */}
|
|
||||||
<View style={ExpensePDFStyle.generalInfoTable}>
|
|
||||||
{rows.map((row) => (
|
|
||||||
<View style={ExpensePDFStyle.generalInfoTableRow} key={row.label}>
|
|
||||||
<View style={ExpensePDFStyle.generalInfoTableColLabel}>
|
|
||||||
<Text style={ExpensePDFStyle.generalInfoTableLabelText}>
|
|
||||||
{row.label}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={ExpensePDFStyle.generalInfoTableColSeparator}>
|
|
||||||
<Text>:</Text>
|
|
||||||
</View>
|
|
||||||
<View style={ExpensePDFStyle.generalInfoTableColValue}>
|
|
||||||
<Text style={ExpensePDFStyle.generalInfoTableValueText}>
|
|
||||||
{row.value}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Detail expense request */}
|
{/* Rincian Pengajuan Section */}
|
||||||
<View
|
<View style={styles.tableSection}>
|
||||||
minPresenceAhead={80}
|
<Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
|
||||||
style={ExpensePDFStyle.expenseDetailContainer}
|
{kandangs.length === 0 ? (
|
||||||
>
|
<Text style={styles.emptyText}>Tidak ada data pengajuan.</Text>
|
||||||
<Text style={ExpensePDFStyle.expenseDetailTitle}>
|
) : (
|
||||||
Rincian Pengajuan Biaya Operasional
|
kandangs.map((kandang, idx) => {
|
||||||
</Text>
|
const pengajuans = kandang.pengajuans || [];
|
||||||
|
const kandangName =
|
||||||
|
kandang.kandang_id && kandang.name
|
||||||
|
? kandang.name
|
||||||
|
: expense?.location?.name || 'Umum';
|
||||||
|
|
||||||
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
return (
|
||||||
let expenseRequestTotal = 0;
|
<View key={idx} style={{ marginBottom: 12 }}>
|
||||||
|
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
|
||||||
kandangExpense.pengajuans?.forEach(
|
{idx + 1}) {kandangName}
|
||||||
(item) => (expenseRequestTotal += item.qty * item.price)
|
</PdfTypography>
|
||||||
);
|
{pengajuans.length > 0 ? (
|
||||||
|
<PdfTable
|
||||||
return (
|
columns={getPengajuanColumns()}
|
||||||
<View
|
data={pengajuans}
|
||||||
key={kandangExpenseIdx}
|
showFooter={true}
|
||||||
style={ExpensePDFStyle.kandangExpenseContainer}
|
footerLabel='Total'
|
||||||
>
|
/>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
) : (
|
||||||
{kandangExpense.kandang_id && kandangExpense.name
|
<Text style={styles.emptyText}>
|
||||||
? `Biaya ${kandangExpense.name}`
|
Tidak ada item pengajuan untuk kandang ini.
|
||||||
: `Biaya ${expense?.location.name || 'Umum'}`}
|
</Text>
|
||||||
</Text>
|
)}
|
||||||
|
|
||||||
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
|
||||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Nonstock
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Kuantitas
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Harga Satuan
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Catatan
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => (
|
|
||||||
<View
|
|
||||||
key={pengajuanIdx}
|
|
||||||
style={ExpensePDFStyle.kandangExpenseTableRow}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{pengajuan.nonstock.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{formatNumber(pengajuan.qty)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{formatCurrency(pengajuan.price)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{pengajuan.notes}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Total Biaya Keseluruhan
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
{formatCurrency(expenseRequestTotal)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
})
|
||||||
})}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Detail expense realization */}
|
{/* Rincian Realisasi Section */}
|
||||||
<View
|
<View style={styles.tableSection}>
|
||||||
minPresenceAhead={80}
|
<Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text>
|
||||||
style={ExpensePDFStyle.expenseDetailContainer}
|
{kandangs.length === 0 ? (
|
||||||
>
|
<Text style={styles.emptyText}>Tidak ada data realisasi.</Text>
|
||||||
<Text style={ExpensePDFStyle.expenseDetailTitle}>
|
) : (
|
||||||
Rincian Realisasi Biaya Operasional
|
kandangs.map((kandang, idx) => {
|
||||||
</Text>
|
const realisasi = kandang.realisasi || [];
|
||||||
|
const kandangName =
|
||||||
|
kandang.kandang_id && kandang.name
|
||||||
|
? kandang.name
|
||||||
|
: expense?.location?.name || 'Umum';
|
||||||
|
|
||||||
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
return (
|
||||||
let expenseRealizationTotal = 0;
|
<View key={idx} style={{ marginBottom: 12 }}>
|
||||||
|
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
|
||||||
kandangExpense.realisasi?.forEach(
|
{idx + 1}) {kandangName}
|
||||||
(item) => (expenseRealizationTotal += item.qty * item.price)
|
</PdfTypography>
|
||||||
);
|
{realisasi.length > 0 ? (
|
||||||
|
<PdfTable
|
||||||
return (
|
columns={getRealisasiColumns()}
|
||||||
<View
|
data={realisasi}
|
||||||
key={kandangExpenseIdx}
|
showFooter={true}
|
||||||
style={ExpensePDFStyle.kandangExpenseContainer}
|
footerLabel='Total'
|
||||||
>
|
/>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
) : (
|
||||||
{kandangExpense.kandang_id && kandangExpense.name
|
<Text style={styles.emptyText}>
|
||||||
? `Biaya ${kandangExpense.name}`
|
Tidak ada item realisasi untuk kandang ini.
|
||||||
: `Biaya ${expense?.location.name || 'Umum'}`}
|
</Text>
|
||||||
</Text>
|
)}
|
||||||
|
|
||||||
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
|
||||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Nonstock
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Kuantitas
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Harga Satuan
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Catatan
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{kandangExpense.realisasi?.map((realisasi, realisasiIdx) => (
|
|
||||||
<View
|
|
||||||
key={realisasiIdx}
|
|
||||||
style={ExpensePDFStyle.kandangExpenseTableRow}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{realisasi.nonstock.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{formatNumber(realisasi.qty)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{formatCurrency(realisasi.price)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
|
||||||
{realisasi.notes}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
|
|
||||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
Total Biaya Keseluruhan
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
|
||||||
>
|
|
||||||
{formatCurrency(expenseRealizationTotal)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
})
|
||||||
})}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={ExpensePDFStyle.footer} fixed>
|
<PdfPageNumber />
|
||||||
<Link
|
|
||||||
src={`${process.env.NEXT_PUBLIC_LTI_URL}expense/detail?expenseId=${expense?.id}`}
|
|
||||||
>
|
|
||||||
{expense?.po_number}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
render={({ pageNumber, totalPages }) =>
|
|
||||||
`${pageNumber} / ${totalPages}`
|
|
||||||
}
|
|
||||||
fixed
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Page>
|
</Page>
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,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 { 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';
|
||||||
@@ -24,10 +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 ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
||||||
@@ -40,89 +38,27 @@ import {
|
|||||||
AdjustmentFilterType,
|
AdjustmentFilterType,
|
||||||
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import { CellContext } from '@tanstack/react-table';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
|
||||||
popoverPosition = 'bottom',
|
|
||||||
props,
|
|
||||||
deleteClickHandler,
|
|
||||||
}: {
|
|
||||||
popoverPosition: 'bottom' | 'top';
|
|
||||||
props: CellContext<InventoryAdjustment, unknown>;
|
|
||||||
deleteClickHandler: () => void;
|
|
||||||
}) => {
|
|
||||||
const popoverId = `adjustment#${props.row.original.id}`;
|
|
||||||
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
|
|
||||||
|
|
||||||
const closePopover = () => {
|
|
||||||
document.getElementById(popoverId)?.hidePopover();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative'>
|
|
||||||
<PopoverButton
|
|
||||||
tabIndex={0}
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
popoverTarget={popoverId}
|
|
||||||
anchorName={popoverAnchorName}
|
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
|
||||||
</PopoverButton>
|
|
||||||
|
|
||||||
<PopoverContent
|
|
||||||
id={popoverId}
|
|
||||||
anchorName={popoverAnchorName}
|
|
||||||
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
|
||||||
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
|
||||||
>
|
|
||||||
<div className='flex flex-col bg-base-100 rounded-xl'>
|
|
||||||
<RequirePermission permissions='lti.inventory.delete'>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
deleteClickHandler();
|
|
||||||
closePopover();
|
|
||||||
}}
|
|
||||||
variant='ghost'
|
|
||||||
color='error'
|
|
||||||
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 +71,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 +79,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_id: 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', values.warehouse_id || '');
|
||||||
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,68 +133,85 @@ 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 = 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]
|
||||||
|
);
|
||||||
|
|
||||||
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 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]);
|
||||||
|
|
||||||
|
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 { data: inventoryAdjustments, isLoading } = useSWR(
|
||||||
data: inventoryAdjustments,
|
|
||||||
isLoading,
|
|
||||||
mutate: refreshAdjustments,
|
|
||||||
} = useSWR(
|
|
||||||
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||||
InventoryAdjustmentApi.getAllFetcher
|
InventoryAdjustmentApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const singleDeleteHandler = async () => {
|
|
||||||
setIsDeleteLoading(true);
|
|
||||||
|
|
||||||
const response = await InventoryAdjustmentApi.delete(
|
|
||||||
selectedAdjustment?.id as number
|
|
||||||
);
|
|
||||||
|
|
||||||
singleDeleteModal.closeModal();
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
|
|
||||||
if (isResponseSuccess(response)) {
|
|
||||||
toast.success(response?.message || 'Successfully delete Adjustment!');
|
|
||||||
refreshAdjustments();
|
|
||||||
} else {
|
|
||||||
toast.error(response?.message || 'Failed to delete Adjustment');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [selectedAdjustment, setSelectedAdjustment] = useState<
|
|
||||||
InventoryAdjustment | undefined
|
useEffect(() => {
|
||||||
>(undefined);
|
updateFilter('search', searchValue);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
}, [searchValue, updateFilter]);
|
||||||
const singleDeleteModal = useModal();
|
|
||||||
|
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(
|
||||||
@@ -368,39 +314,8 @@ const InventoryAdjustmentTable = () => {
|
|||||||
header: 'Oleh',
|
header: 'Oleh',
|
||||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: 'Aksi',
|
|
||||||
cell: (props: CellContext<InventoryAdjustment, unknown>) => {
|
|
||||||
const currentPageSize =
|
|
||||||
props.table.getPaginationRowModel().rows.length;
|
|
||||||
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
|
||||||
const currentRowRelativeIndex =
|
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
|
||||||
|
|
||||||
const deleteClickHandler = () => {
|
|
||||||
setSelectedAdjustment(props.row.original);
|
|
||||||
singleDeleteModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RowOptionsMenu
|
|
||||||
props={props}
|
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
[
|
[]
|
||||||
tableFilterState.pageSize,
|
|
||||||
tableFilterState.page,
|
|
||||||
singleDeleteModal,
|
|
||||||
setSelectedAdjustment,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSortingFilter = useCallback(
|
const updateSortingFilter = useCallback(
|
||||||
@@ -486,8 +401,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 +490,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}
|
||||||
@@ -589,7 +502,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
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}
|
||||||
@@ -601,7 +514,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 +524,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>
|
||||||
@@ -627,21 +544,6 @@ const InventoryAdjustmentTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={singleDeleteModal.ref}
|
|
||||||
type='error'
|
|
||||||
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'error',
|
|
||||||
isLoading: isDeleteLoading,
|
|
||||||
onClick: singleDeleteHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
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>;
|
warehouse_id: string | null;
|
||||||
transaction_type?: OptionType<string>;
|
transaction_type: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
InventoryAdjustmentFormSchema,
|
InventoryAdjustmentFormSchema,
|
||||||
InventoryAdjustmentFormValues,
|
InventoryAdjustmentFormValues,
|
||||||
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
|
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
import {
|
import {
|
||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
ProjectFlockKandangApi,
|
ProjectFlockKandangApi,
|
||||||
@@ -32,6 +32,8 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
|||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
@@ -117,19 +119,40 @@ const InventoryAdjustmentForm = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { rawData: approvedProjectFlockKandangsRawData } =
|
||||||
|
useSelect<ProjectFlockKandang>(
|
||||||
|
ProjectFlockKandangApi.basePath,
|
||||||
|
'id',
|
||||||
|
'id',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
step_name: 'Disetujui',
|
||||||
|
limit: '100',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const approvedProjectFlockKandangs = useMemo(() => {
|
||||||
|
if (
|
||||||
|
approvedProjectFlockKandangsRawData &&
|
||||||
|
'data' in approvedProjectFlockKandangsRawData
|
||||||
|
) {
|
||||||
|
return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [approvedProjectFlockKandangsRawData]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: projectFlockKandangOptions,
|
setInputValue: setKandangInputValue,
|
||||||
loadMore: loadMoreProjectFlockKandangs,
|
options: kandangOptionsFromApi,
|
||||||
setInputValue: setProjectFlockKandangInputValue,
|
isLoadingOptions: isLoadingKandangOptions,
|
||||||
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
loadMore: loadMoreKandangs,
|
||||||
} = useSelect(
|
} = useSelect<Kandang>(
|
||||||
selectedProjectFlock ? ProjectFlockKandangApi.basePath : '',
|
selectedProjectFlock ? KandangApi.basePath : '',
|
||||||
'kandang.id',
|
'id',
|
||||||
'kandang.name',
|
'name',
|
||||||
'search',
|
'search',
|
||||||
{
|
{
|
||||||
step_name: 'Disetujui',
|
location_id: selectedProjectFlockLocationId,
|
||||||
project_flock_id: String(selectedProjectFlock?.value),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -162,9 +185,7 @@ const InventoryAdjustmentForm = ({
|
|||||||
isLoadingOptions: isLoadingProductOptions,
|
isLoadingOptions: isLoadingProductOptions,
|
||||||
loadMore: loadMoreProducts,
|
loadMore: loadMoreProducts,
|
||||||
rawData: products,
|
rawData: products,
|
||||||
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search');
|
||||||
include_all: 'true',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setDepletionProductInputValue,
|
setInputValue: setDepletionProductInputValue,
|
||||||
@@ -199,6 +220,26 @@ const InventoryAdjustmentForm = ({
|
|||||||
return (product?.flags as string[]) || [];
|
return (product?.flags as string[]) || [];
|
||||||
}, [selectedProduct, productOptions]);
|
}, [selectedProduct, productOptions]);
|
||||||
|
|
||||||
|
const kandangOptions = useMemo(() => {
|
||||||
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
|
if (selectedProjectFlock) {
|
||||||
|
const approvedKandangIds = approvedProjectFlockKandangs
|
||||||
|
.filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value)
|
||||||
|
.map((pfk) => pfk.kandang_id);
|
||||||
|
|
||||||
|
options = kandangOptionsFromApi.filter((kandang) =>
|
||||||
|
approvedKandangIds.includes(kandang.value as number)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [
|
||||||
|
selectedProjectFlock,
|
||||||
|
kandangOptionsFromApi,
|
||||||
|
approvedProjectFlockKandangs,
|
||||||
|
]);
|
||||||
|
|
||||||
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
|
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
|
||||||
() => ({
|
() => ({
|
||||||
location: null,
|
location: null,
|
||||||
@@ -650,10 +691,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Kandang'
|
label='Kandang'
|
||||||
value={selectedKandang}
|
value={selectedKandang}
|
||||||
onChange={kandangChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
onInputChange={setProjectFlockKandangInputValue}
|
onInputChange={setKandangInputValue}
|
||||||
options={projectFlockKandangOptions}
|
options={kandangOptions}
|
||||||
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
|
onMenuScrollToBottom={loadMoreKandangs}
|
||||||
isLoading={isLoadingProjectFlockKandangOptions}
|
isLoading={isLoadingKandangOptions}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -13,8 +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 ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
@@ -35,11 +41,9 @@ import {
|
|||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
deleteClickHandler,
|
|
||||||
}: {
|
}: {
|
||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Movement, unknown>;
|
props: CellContext<Movement, unknown>;
|
||||||
deleteClickHandler: () => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const popoverId = `movement#${props.row.original.id}`;
|
const popoverId = `movement#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
||||||
@@ -79,20 +83,6 @@ const RowOptionsMenu = ({
|
|||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.inventory.transfer.delete'>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
deleteClickHandler();
|
|
||||||
closePopover();
|
|
||||||
}}
|
|
||||||
variant='ghost'
|
|
||||||
color='error'
|
|
||||||
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,21 +90,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 +111,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 +119,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,59 +162,67 @@ 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>([]);
|
||||||
const [selectedMovement, setSelectedMovement] = useState<
|
|
||||||
Movement | undefined
|
|
||||||
>(undefined);
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
const singleDeleteModal = useModal();
|
|
||||||
|
|
||||||
const {
|
const { data: movements, isLoading } = useSWR(
|
||||||
data: movements,
|
|
||||||
isLoading,
|
|
||||||
mutate: refreshMovements,
|
|
||||||
} = useSWR(
|
|
||||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||||
MovementApi.getAllFetcher
|
MovementApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const singleDeleteHandler = async () => {
|
useEffect(() => {
|
||||||
setIsDeleteLoading(true);
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
const response = await MovementApi.delete(selectedMovement?.id as number);
|
useEffect(() => {
|
||||||
|
setTableState('movement-table', pathname);
|
||||||
singleDeleteModal.closeModal();
|
}, [pathname, setTableState]);
|
||||||
setIsDeleteLoading(false);
|
|
||||||
|
|
||||||
if (isResponseSuccess(response)) {
|
|
||||||
toast.success(response?.message || 'Successfully delete Movement!');
|
|
||||||
refreshMovements();
|
|
||||||
} else {
|
|
||||||
toast.error(response?.message || 'Failed to delete Movement');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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(
|
||||||
@@ -281,27 +275,16 @@ const MovementTable = () => {
|
|||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
const deleteClickHandler = () => {
|
|
||||||
setSelectedMovement(props.row.original);
|
|
||||||
singleDeleteModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenu
|
<RowOptionsMenu
|
||||||
props={props}
|
props={props}
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[tableFilterState.pageSize, tableFilterState.page]
|
||||||
tableFilterState.pageSize,
|
|
||||||
tableFilterState.page,
|
|
||||||
singleDeleteModal,
|
|
||||||
setSelectedMovement,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -427,7 +410,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 +422,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 +435,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>
|
||||||
@@ -468,21 +455,6 @@ const MovementTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={singleDeleteModal.ref}
|
|
||||||
type='error'
|
|
||||||
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'error',
|
|
||||||
isLoading: isDeleteLoading,
|
|
||||||
onClick: singleDeleteHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -85,7 +82,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: number;
|
warehouse_id: number;
|
||||||
warehouse_name: string;
|
warehouse_name: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
transfer_available_qty?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== USE SELECT HOOKS =====
|
// ===== USE SELECT HOOKS =====
|
||||||
@@ -327,6 +323,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { setFieldValue, setFieldTouched, setFieldError } = formik;
|
||||||
|
|
||||||
const prevSourceWarehouseIdRef = useRef<number | null>(
|
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||||
formik.values.source_warehouse_id
|
formik.values.source_warehouse_id
|
||||||
);
|
);
|
||||||
@@ -340,15 +338,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||||
prevSourceWarehouseId !== null
|
prevSourceWarehouseId !== null
|
||||||
) {
|
) {
|
||||||
productStockCacheRef.current = new Map();
|
setFieldValue('products', [
|
||||||
formik.setFieldValue('products', [
|
|
||||||
{
|
{
|
||||||
product: null,
|
product: null,
|
||||||
product_id: 0,
|
product_id: 0,
|
||||||
product_qty: '',
|
product_qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('products', false);
|
setFieldTouched('products', false);
|
||||||
|
|
||||||
const updatedDeliveries = formik.values.deliveries.map(
|
const updatedDeliveries = formik.values.deliveries.map(
|
||||||
(delivery: DeliverySchema) => ({
|
(delivery: DeliverySchema) => ({
|
||||||
@@ -362,12 +359,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
setFieldValue('deliveries', updatedDeliveries);
|
||||||
formik.setFieldTouched('deliveries', false);
|
setFieldTouched('deliveries', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||||
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
}, [
|
||||||
|
formik.values.source_warehouse_id,
|
||||||
|
formik.values.deliveries,
|
||||||
|
setFieldValue,
|
||||||
|
setFieldTouched,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
const {
|
const {
|
||||||
@@ -384,8 +386,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: formik.values.source_warehouse_id
|
warehouse_id: formik.values.source_warehouse_id
|
||||||
? formik.values.source_warehouse_id.toString()
|
? formik.values.source_warehouse_id.toString()
|
||||||
: '',
|
: '',
|
||||||
transfer_context: 'inventory_transfer',
|
|
||||||
stock_mode: 'exclude_chickin',
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -398,20 +398,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: pw.warehouse.id,
|
warehouse_id: pw.warehouse.id,
|
||||||
warehouse_name: pw.warehouse.name,
|
warehouse_name: pw.warehouse.name,
|
||||||
quantity: pw.quantity,
|
quantity: pw.quantity,
|
||||||
transfer_available_qty: pw.transfer_available_qty,
|
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
}, [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,
|
||||||
@@ -472,9 +462,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
const handleTransferDateChange = useCallback(
|
const handleTransferDateChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
formik.setFieldValue('transfer_date', e.target.value);
|
setFieldValue('transfer_date', e.target.value);
|
||||||
},
|
},
|
||||||
[]
|
[setFieldValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSourceWarehouseChange = useCallback(
|
const handleSourceWarehouseChange = useCallback(
|
||||||
@@ -494,14 +484,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
formik.setFieldTouched('source_warehouse', true);
|
setFieldTouched('source_warehouse', true);
|
||||||
formik.setFieldValue('source_warehouse', val);
|
setFieldValue('source_warehouse', val);
|
||||||
formik.setFieldTouched('source_warehouse_id', true);
|
setFieldTouched('source_warehouse_id', true);
|
||||||
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
formik.values.destination_warehouse_id,
|
formik.values.destination_warehouse_id,
|
||||||
formik.values.destination_warehouse,
|
formik.values.destination_warehouse,
|
||||||
|
setFieldTouched,
|
||||||
|
setFieldValue,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -522,15 +514,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
formik.setFieldTouched('destination_warehouse', true);
|
setFieldTouched('destination_warehouse', true);
|
||||||
formik.setFieldValue('destination_warehouse', val);
|
setFieldValue('destination_warehouse', val);
|
||||||
formik.setFieldTouched('destination_warehouse_id', true);
|
setFieldTouched('destination_warehouse_id', true);
|
||||||
formik.setFieldValue(
|
setFieldValue('destination_warehouse_id', newDestinationWarehouseId);
|
||||||
'destination_warehouse_id',
|
|
||||||
newDestinationWarehouseId
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[formik.values.source_warehouse_id, formik.values.source_warehouse]
|
[
|
||||||
|
formik.values.source_warehouse_id,
|
||||||
|
formik.values.source_warehouse,
|
||||||
|
setFieldTouched,
|
||||||
|
setFieldValue,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addProduct = useCallback(() => {
|
const addProduct = useCallback(() => {
|
||||||
@@ -542,15 +536,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
product_qty: '',
|
product_qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
formik.setFieldValue('products', newProducts);
|
setFieldValue('products', newProducts);
|
||||||
}, [formik.values.products]);
|
}, [formik.values.products, setFieldValue]);
|
||||||
|
|
||||||
const removeProduct = useCallback(
|
const removeProduct = useCallback(
|
||||||
(i: number) => {
|
(i: number) => {
|
||||||
const updatedProducts = formik.values.products?.filter(
|
const updatedProducts = formik.values.products?.filter(
|
||||||
(_, idx) => idx !== i
|
(_, idx) => idx !== i
|
||||||
);
|
);
|
||||||
formik.setFieldValue('products', updatedProducts);
|
setFieldValue('products', updatedProducts);
|
||||||
|
|
||||||
setSelectedProducts([]);
|
setSelectedProducts([]);
|
||||||
|
|
||||||
@@ -559,7 +553,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
setProductQtyErrorShown(false);
|
setProductQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik.values.products, productQtyErrorShown, setSelectedProducts]
|
[
|
||||||
|
formik.values.products,
|
||||||
|
productQtyErrorShown,
|
||||||
|
setSelectedProducts,
|
||||||
|
setFieldValue,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const bulkRemoveProduct = useCallback(() => {
|
const bulkRemoveProduct = useCallback(() => {
|
||||||
@@ -567,26 +566,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.values.products?.filter(
|
formik.values.products?.filter(
|
||||||
(_, idx) => !selectedProducts.includes(idx)
|
(_, idx) => !selectedProducts.includes(idx)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('products', updatedProducts);
|
setFieldValue('products', updatedProducts);
|
||||||
setSelectedProducts([]);
|
setSelectedProducts([]);
|
||||||
|
|
||||||
if (productQtyErrorShown) {
|
if (productQtyErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setProductQtyErrorShown(false);
|
setProductQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]);
|
}, [
|
||||||
|
selectedProducts,
|
||||||
|
setSelectedProducts,
|
||||||
|
productQtyErrorShown,
|
||||||
|
setFieldValue,
|
||||||
|
formik.values.products,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleProductChange = useCallback(
|
const handleProductChange = useCallback(
|
||||||
(idx: number, val: OptionType | OptionType[] | null) => {
|
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched(`products.${idx}.product`, true);
|
setFieldTouched(`products.${idx}.product`, true);
|
||||||
formik.setFieldValue(`products.${idx}.product`, val);
|
setFieldValue(`products.${idx}.product`, val);
|
||||||
formik.setFieldTouched(`products.${idx}.product_id`, true);
|
setFieldTouched(`products.${idx}.product_id`, true);
|
||||||
formik.setFieldValue(
|
setFieldValue(
|
||||||
`products.${idx}.product_id`,
|
`products.${idx}.product_id`,
|
||||||
(val as ProductWarehouseOptionType)?.value
|
(val as ProductWarehouseOptionType)?.value
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[]
|
[setFieldTouched, setFieldValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleProductSelectAllChange = useCallback(
|
const handleProductSelectAllChange = useCallback(
|
||||||
@@ -613,7 +618,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addDelivery = useCallback(() => {
|
const addDelivery = useCallback(() => {
|
||||||
formik.setFieldValue('deliveries', [
|
setFieldValue('deliveries', [
|
||||||
...(formik.values.deliveries || []),
|
...(formik.values.deliveries || []),
|
||||||
{
|
{
|
||||||
delivery_cost: '',
|
delivery_cost: '',
|
||||||
@@ -632,14 +637,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}, [formik.values.deliveries]);
|
}, [formik.values.deliveries, setFieldValue]);
|
||||||
|
|
||||||
const removeDelivery = useCallback(
|
const removeDelivery = useCallback(
|
||||||
(i: number) => {
|
(i: number) => {
|
||||||
const updatedDeliveries = formik.values.deliveries?.filter(
|
const updatedDeliveries = formik.values.deliveries?.filter(
|
||||||
(_, idx) => idx !== i
|
(_, idx) => idx !== i
|
||||||
);
|
);
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
setFieldValue('deliveries', updatedDeliveries);
|
||||||
|
|
||||||
setSelectedDeliveries([]);
|
setSelectedDeliveries([]);
|
||||||
|
|
||||||
@@ -648,7 +653,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
setDeliveryQtyErrorShown(false);
|
setDeliveryQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries]
|
[
|
||||||
|
formik.values.deliveries,
|
||||||
|
deliveryQtyErrorShown,
|
||||||
|
setSelectedDeliveries,
|
||||||
|
setFieldValue,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const bulkRemoveDelivery = useCallback(() => {
|
const bulkRemoveDelivery = useCallback(() => {
|
||||||
@@ -656,7 +666,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.values.deliveries?.filter(
|
formik.values.deliveries?.filter(
|
||||||
(_, idx) => !selectedDeliveries.includes(idx)
|
(_, idx) => !selectedDeliveries.includes(idx)
|
||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
setFieldValue('deliveries', updatedDeliveries);
|
||||||
setSelectedDeliveries([]);
|
setSelectedDeliveries([]);
|
||||||
|
|
||||||
if (deliveryQtyErrorShown) {
|
if (deliveryQtyErrorShown) {
|
||||||
@@ -664,10 +674,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
setDeliveryQtyErrorShown(false);
|
setDeliveryQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
formik,
|
|
||||||
selectedDeliveries,
|
selectedDeliveries,
|
||||||
setSelectedDeliveries,
|
setSelectedDeliveries,
|
||||||
deliveryQtyErrorShown,
|
deliveryQtyErrorShown,
|
||||||
|
setFieldValue,
|
||||||
|
formik.values.deliveries,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDeliverySelectAllChange = useCallback(
|
const handleDeliverySelectAllChange = useCallback(
|
||||||
@@ -697,34 +708,28 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
const handleDeliveryProductChange = useCallback(
|
const handleDeliveryProductChange = useCallback(
|
||||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched(
|
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product`, true);
|
||||||
`deliveries.${deliveryIdx}.products.0.product`,
|
setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
||||||
true
|
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product_id`, true);
|
||||||
);
|
setFieldValue(
|
||||||
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`deliveries.${deliveryIdx}.products.0.product_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${deliveryIdx}.products.0.product_id`,
|
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||||
(val as OptionType)?.value
|
(val as OptionType)?.value
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[]
|
[setFieldTouched, setFieldValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliverySupplierChange = useCallback(
|
const handleDeliverySupplierChange = useCallback(
|
||||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
||||||
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
||||||
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
||||||
formik.setFieldValue(
|
setFieldValue(
|
||||||
`deliveries.${deliveryIdx}.supplier_id`,
|
`deliveries.${deliveryIdx}.supplier_id`,
|
||||||
(val as OptionType)?.value
|
(val as OptionType)?.value
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[]
|
[setFieldTouched, setFieldValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryDocumentChange = useCallback(
|
const handleDeliveryDocumentChange = useCallback(
|
||||||
@@ -736,15 +741,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[setFieldValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChange = useCallback(
|
const handleDeliveryCostChange = useCallback(
|
||||||
(idx: number, value: number) => {
|
(idx: number, value: number) => {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||||
|
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
if (delivery) {
|
if (delivery) {
|
||||||
@@ -754,21 +759,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
const perItem = value / productQty;
|
const perItem = value / productQty;
|
||||||
formik.setFieldValue(
|
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
|
||||||
perItem
|
|
||||||
);
|
|
||||||
} else if (value === 0) {
|
} else if (value === 0) {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik.values.deliveries]
|
[formik.values.deliveries, setFieldValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostPerItemChange = useCallback(
|
const handleDeliveryCostPerItemChange = useCallback(
|
||||||
(idx: number, value: number) => {
|
(idx: number, value: number) => {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
||||||
|
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
if (delivery) {
|
if (delivery) {
|
||||||
@@ -778,13 +780,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
const totalCost = value * productQty;
|
const totalCost = value * productQty;
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||||
} else if (value === 0) {
|
} else if (value === 0) {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik.values.deliveries]
|
[formik.values.deliveries, setFieldValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChangeWrapper = useCallback(
|
const handleDeliveryCostChangeWrapper = useCallback(
|
||||||
@@ -853,42 +855,14 @@ 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;
|
return productWarehouse?.quantity ?? 0;
|
||||||
const cached = productStockCacheRef.current.get(productId);
|
|
||||||
return cached?.transfer_available_qty ?? cached?.quantity ?? 0;
|
|
||||||
},
|
},
|
||||||
[productWarehouseOptions, type]
|
[productWarehouseOptions, type]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTotalStock = useCallback(
|
|
||||||
(productId: number) => {
|
|
||||||
if (type === 'detail') return 0;
|
|
||||||
const live = productWarehouseOptions.find(
|
|
||||||
(pw) => pw.product_id === productId
|
|
||||||
);
|
|
||||||
if (live) return live.quantity ?? 0;
|
|
||||||
return productStockCacheRef.current.get(productId)?.quantity ?? 0;
|
|
||||||
},
|
|
||||||
[productWarehouseOptions, type]
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasAvailableQty = useCallback(
|
|
||||||
(productId: number) => {
|
|
||||||
const live = productWarehouseOptions.find(
|
|
||||||
(pw) => pw.product_id === productId
|
|
||||||
);
|
|
||||||
if (live) return live.transfer_available_qty !== undefined;
|
|
||||||
return (
|
|
||||||
productStockCacheRef.current.get(productId)?.transfer_available_qty !==
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[productWarehouseOptions]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getProductQtyBottomLabel = useCallback(
|
const getProductQtyBottomLabel = useCallback(
|
||||||
(productIdx: number) => {
|
(productIdx: number) => {
|
||||||
if (type === 'detail') return undefined;
|
if (type === 'detail') return undefined;
|
||||||
@@ -896,31 +870,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (!product || !product.product_id) return undefined;
|
if (!product || !product.product_id) return undefined;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(product.product_id);
|
const availableStock = getAvailableStock(product.product_id);
|
||||||
const totalStock = getTotalStock(product.product_id);
|
|
||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
const remainingStock = availableStock - requestedQty;
|
const remainingStock = availableStock - requestedQty;
|
||||||
const isAyamProduct = hasAvailableQty(product.product_id);
|
|
||||||
|
|
||||||
if (requestedQty > 0) {
|
if (requestedQty > 0) {
|
||||||
if (isAyamProduct) {
|
|
||||||
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
|
|
||||||
}
|
|
||||||
return `Sisa: ${formatNumber(remainingStock)}`;
|
return `Sisa: ${formatNumber(remainingStock)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAyamProduct) {
|
|
||||||
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Tersedia: ${formatNumber(availableStock)}`;
|
return `Tersedia: ${formatNumber(availableStock)}`;
|
||||||
},
|
},
|
||||||
[
|
[formik.values.products, getAvailableStock, type]
|
||||||
formik.values.products,
|
|
||||||
getAvailableStock,
|
|
||||||
getTotalStock,
|
|
||||||
hasAvailableQty,
|
|
||||||
type,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getDeliveryProductQtyBottomLabel = useCallback(
|
const getDeliveryProductQtyBottomLabel = useCallback(
|
||||||
@@ -982,26 +941,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (!product || !product.product_id) return null;
|
if (!product || !product.product_id) return null;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(product.product_id);
|
const availableStock = getAvailableStock(product.product_id);
|
||||||
const totalStock = getTotalStock(product.product_id);
|
|
||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
const isAyamProduct = hasAvailableQty(product.product_id);
|
|
||||||
|
|
||||||
if (requestedQty > availableStock) {
|
if (requestedQty > availableStock) {
|
||||||
if (isAyamProduct) {
|
|
||||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
|
|
||||||
}
|
|
||||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[
|
[formik.values.products, getAvailableStock, type]
|
||||||
formik.values.products,
|
|
||||||
getAvailableStock,
|
|
||||||
getTotalStock,
|
|
||||||
hasAvailableQty,
|
|
||||||
type,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const validateDeliveryQty = useCallback(
|
const validateDeliveryQty = useCallback(
|
||||||
@@ -1115,12 +1063,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return !validateDeliveryQty(deliveryIdx, productIdx, qty);
|
return !validateDeliveryQty(deliveryIdx, productIdx, qty);
|
||||||
})
|
})
|
||||||
) ?? []),
|
) ?? []),
|
||||||
[
|
[formik.values.deliveries, validateDeliveryQty, type]
|
||||||
formik.values.deliveries,
|
|
||||||
formik.values.products,
|
|
||||||
validateDeliveryQty,
|
|
||||||
type,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasInvalidQty = useMemo(
|
const hasInvalidQty = useMemo(
|
||||||
@@ -1137,6 +1080,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.products, getProductQtyError, type]);
|
}, [formik.values.products, getProductQtyError, type]);
|
||||||
|
|
||||||
|
const deliveryCostDepString = useMemo(
|
||||||
|
() =>
|
||||||
|
formik.values.deliveries
|
||||||
|
?.map((d, idx) => ({
|
||||||
|
idx,
|
||||||
|
productQty: d.products.reduce(
|
||||||
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
|
||||||
|
deliveryCostPerItem:
|
||||||
|
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
|
||||||
|
)
|
||||||
|
.join('|'),
|
||||||
|
[formik.values.deliveries]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== EFFECTS =====
|
// ===== EFFECTS =====
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.values.deliveries?.forEach((delivery, idx) => {
|
formik.values.deliveries?.forEach((delivery, idx) => {
|
||||||
@@ -1153,36 +1117,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (deliveryCost > 0 && productQty > 0) {
|
if (deliveryCost > 0 && productQty > 0) {
|
||||||
const perItem = deliveryCost / productQty;
|
const perItem = deliveryCost / productQty;
|
||||||
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
|
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
|
||||||
formik.setFieldValue(
|
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
|
||||||
perItem
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (deliveryCostPerItem > 0 && productQty > 0) {
|
} else if (deliveryCostPerItem > 0 && productQty > 0) {
|
||||||
const totalCost = deliveryCostPerItem * productQty;
|
const totalCost = deliveryCostPerItem * productQty;
|
||||||
if (Math.abs(deliveryCost - totalCost) > 0.01) {
|
if (Math.abs(deliveryCost - totalCost) > 0.01) {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [
|
}, [deliveryCostDepString, setFieldValue, formik.values.deliveries]);
|
||||||
formik.values.deliveries
|
|
||||||
?.map((d, idx) => ({
|
|
||||||
idx,
|
|
||||||
productQty: d.products.reduce(
|
|
||||||
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
|
||||||
0
|
|
||||||
),
|
|
||||||
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
|
|
||||||
deliveryCostPerItem:
|
|
||||||
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
|
|
||||||
}))
|
|
||||||
.map(
|
|
||||||
(item) =>
|
|
||||||
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
|
|
||||||
)
|
|
||||||
.join('|'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -1192,7 +1136,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
!isInitialized
|
!isInitialized
|
||||||
) {
|
) {
|
||||||
if (formik.values.products.length === 0) {
|
if (formik.values.products.length === 0) {
|
||||||
formik.setFieldValue('products', [
|
setFieldValue('products', [
|
||||||
{
|
{
|
||||||
product: null,
|
product: null,
|
||||||
product_id: 0,
|
product_id: 0,
|
||||||
@@ -1201,7 +1145,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (formik.values.deliveries.length === 0) {
|
if (formik.values.deliveries.length === 0) {
|
||||||
formik.setFieldValue('deliveries', [
|
setFieldValue('deliveries', [
|
||||||
{
|
{
|
||||||
delivery_cost: undefined,
|
delivery_cost: undefined,
|
||||||
delivery_cost_per_item: undefined,
|
delivery_cost_per_item: undefined,
|
||||||
@@ -1223,7 +1167,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
}, [formik.values.source_warehouse_id, isInitialized, type]);
|
}, [
|
||||||
|
formik.values.source_warehouse_id,
|
||||||
|
isInitialized,
|
||||||
|
type,
|
||||||
|
setFieldValue,
|
||||||
|
formik.values.products.length,
|
||||||
|
formik.values.deliveries.length,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -1232,7 +1183,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.values.source_warehouse_id ===
|
formik.values.source_warehouse_id ===
|
||||||
formik.values.destination_warehouse_id
|
formik.values.destination_warehouse_id
|
||||||
) {
|
) {
|
||||||
formik.setFieldError(
|
setFieldError(
|
||||||
'destination_warehouse_id',
|
'destination_warehouse_id',
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||||
);
|
);
|
||||||
@@ -1241,13 +1192,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.errors.destination_warehouse_id ===
|
formik.errors.destination_warehouse_id ===
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||||
) {
|
) {
|
||||||
formik.setFieldError('destination_warehouse_id', undefined);
|
setFieldError('destination_warehouse_id', undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
formik.values.source_warehouse_id,
|
formik.values.source_warehouse_id,
|
||||||
formik.values.destination_warehouse_id,
|
formik.values.destination_warehouse_id,
|
||||||
formik.errors.destination_warehouse_id,
|
formik.errors.destination_warehouse_id,
|
||||||
|
setFieldError,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1283,29 +1235,37 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
setFieldValue('deliveries', updatedDeliveries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [formik.values.products]);
|
}, [formik.values.products, formik.values.deliveries, setFieldValue]);
|
||||||
|
|
||||||
|
const productQtyDepString = useMemo(
|
||||||
|
() => formik.values.products?.map((p) => p.product_qty).join(','),
|
||||||
|
[formik.values.products]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (productQtyErrorShown) {
|
if (productQtyErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setProductQtyErrorShown(false);
|
setProductQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [formik.values.products?.map((p) => p.product_qty).join(',')]);
|
}, [productQtyErrorShown]);
|
||||||
|
|
||||||
|
const deliveryProductQtyDepString = useMemo(
|
||||||
|
() =>
|
||||||
|
formik.values.deliveries
|
||||||
|
?.map((d) => d.products.map((p) => p.product_qty).join(','))
|
||||||
|
.join('|'),
|
||||||
|
[formik.values.deliveries]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deliveryQtyErrorShown) {
|
if (deliveryQtyErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDeliveryQtyErrorShown(false);
|
setDeliveryQtyErrorShown(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [deliveryProductQtyDepString, productQtyDepString, deliveryQtyErrorShown]);
|
||||||
formik.values.deliveries
|
|
||||||
?.map((d) => d.products.map((p) => p.product_qty).join(','))
|
|
||||||
.join('|'),
|
|
||||||
formik.values.products?.map((p) => p.product_qty).join(','),
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
|
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -199,9 +199,6 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
'yyyy-MM-DD'
|
'yyyy-MM-DD'
|
||||||
),
|
),
|
||||||
vehicle_number: product.vehicle_number,
|
vehicle_number: product.vehicle_number,
|
||||||
weight_per_convertion: parseFloat(
|
|
||||||
String(product.weight_per_convertion ?? 0)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -371,9 +368,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
const currentProducts = deliveryOrderValues?.find(
|
const currentProducts = deliveryOrderValues?.find(
|
||||||
(product) => product.id == id
|
(product) => product.id == id
|
||||||
);
|
);
|
||||||
|
setSelectedDeliveryProduct(values ?? currentProducts ?? null);
|
||||||
setSelectedDeliveryProduct(currentProducts ?? values ?? null);
|
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
}
|
}
|
||||||
@@ -435,9 +430,6 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
'yyyy-MM-DD'
|
'yyyy-MM-DD'
|
||||||
),
|
),
|
||||||
vehicle_number: product.vehicle_number,
|
vehicle_number: product.vehicle_number,
|
||||||
weight_per_convertion: parseFloat(
|
|
||||||
String(product.weight_per_convertion ?? 0)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -544,9 +536,13 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
formModal.closeModal();
|
formModal.closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasLoadedInitialValues = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getFilledInitialValues = async () => {
|
const getFilledInitialValues = async () => {
|
||||||
if (marketingId && isResponseSuccess(marketing)) {
|
if (marketingId && isResponseSuccess(marketing)) {
|
||||||
|
if (hasLoadedInitialValues.current) return;
|
||||||
|
hasLoadedInitialValues.current = true;
|
||||||
|
|
||||||
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
||||||
marketing.data
|
marketing.data
|
||||||
);
|
);
|
||||||
@@ -590,9 +586,15 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
setFormErrorMessage('');
|
setFormErrorMessage('');
|
||||||
}, [step]);
|
}, [step]);
|
||||||
|
|
||||||
// sync delivery order values to formik
|
const prevDeliveryOrderValuesRef = useRef(deliveryOrderValues);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
if (
|
||||||
|
JSON.stringify(prevDeliveryOrderValuesRef.current) !==
|
||||||
|
JSON.stringify(deliveryOrderValues)
|
||||||
|
) {
|
||||||
|
prevDeliveryOrderValuesRef.current = deliveryOrderValues;
|
||||||
|
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||||
|
}
|
||||||
}, [deliveryOrderValues]);
|
}, [deliveryOrderValues]);
|
||||||
|
|
||||||
const grandTotal = useMemo(() => {
|
const grandTotal = useMemo(() => {
|
||||||
@@ -849,11 +851,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';
|
||||||
@@ -10,38 +10,22 @@ import SelectInput, {
|
|||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
import {
|
|
||||||
MarketingFilterFormValues,
|
|
||||||
MarketingFilterSchema,
|
|
||||||
} from '@/components/pages/marketing/filter/MarketingFilter';
|
|
||||||
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 { 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,35 +33,51 @@ 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>(MarketingApi.basePath, 'id', 'so_number', '', {
|
||||||
include_all: 'true',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
isLoadingOptions: isLoadingCustomersOptions,
|
isLoadingOptions: isLoadingCustomersOptions,
|
||||||
setInputValue: setCustomersInputValue,
|
setInputValue: setCustomersInputValue,
|
||||||
loadMore: loadMoreCustomers,
|
loadMore: loadMoreCustomers,
|
||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
|
||||||
has_marketing: 'true',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const uniqueCustomersOptions = useMemo(() => {
|
||||||
options: projectFlockOptions,
|
const seen = new Set();
|
||||||
rawData: projectFlocksRawData,
|
return customersOptions.filter((customer) => {
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
if (seen.has(customer.value)) return false;
|
||||||
setInputValue: setProjectFlockInputValue,
|
seen.add(customer.value);
|
||||||
loadMore: loadMoreProjectFlocks,
|
return true;
|
||||||
} = useSelect<ProjectFlock>(
|
});
|
||||||
ProjectFlockApi.basePath,
|
}, [customersOptions]);
|
||||||
'id',
|
|
||||||
'flock_name',
|
|
||||||
'search'
|
|
||||||
);
|
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
...MARKETING_APPROVAL_LINE.map((item) => ({
|
...MARKETING_APPROVAL_LINE.map((item) => ({
|
||||||
@@ -87,30 +87,23 @@ const MarketingFilterModal = ({
|
|||||||
{ value: 'DITOLAK', label: 'Ditolak' },
|
{ value: 'DITOLAK', label: 'Ditolak' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const formik = useFormik<MarketingFilterFormValues>({
|
const formik = useFormik<{
|
||||||
initialValues: initialValues || {
|
product_ids: OptionType[];
|
||||||
|
status: OptionType | null;
|
||||||
|
customer_id: OptionType | null;
|
||||||
|
}>({
|
||||||
|
initialValues: {
|
||||||
product_ids: [],
|
product_ids: [],
|
||||||
status: null,
|
status: null,
|
||||||
customer: null,
|
customer_id: null,
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
},
|
||||||
validationSchema: MarketingFilterSchema,
|
|
||||||
|
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues: MarketingFilter = {
|
const formattedValues = {
|
||||||
|
...values,
|
||||||
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_id?.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,58 +116,18 @@ 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[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue('customer_id', val as OptionType);
|
||||||
'customer',
|
|
||||||
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
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 +137,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 */}
|
||||||
@@ -234,44 +187,13 @@ const MarketingFilterModal = ({
|
|||||||
label='Customer'
|
label='Customer'
|
||||||
isClearable
|
isClearable
|
||||||
placeholder='Pilih customer'
|
placeholder='Pilih customer'
|
||||||
options={customersOptions}
|
options={uniqueCustomersOptions}
|
||||||
isLoading={isLoadingCustomersOptions}
|
isLoading={isLoadingCustomersOptions}
|
||||||
value={formik.values.customer}
|
value={formik.values.customer_id}
|
||||||
onChange={customerChangeHandler}
|
onChange={customerChangeHandler}
|
||||||
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,14 +226,6 @@ const MarketingTable = () => {
|
|||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const productsClickHandler = useCallback(
|
|
||||||
(item: Marketing) => {
|
|
||||||
setSelectedItem(item);
|
|
||||||
productsModal.openModal();
|
|
||||||
},
|
|
||||||
[productsModal]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMarketingHandler = async () => {
|
const deleteMarketingHandler = async () => {
|
||||||
const deleteMarketingRes = await MarketingApi.delete(
|
const deleteMarketingRes = await MarketingApi.delete(
|
||||||
selectedItem?.id as number
|
selectedItem?.id as number
|
||||||
@@ -357,226 +246,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 +324,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 +331,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 +373,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 +389,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 +424,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,8 +444,12 @@ const MarketingTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'marketing_products.length',
|
accessorKey: 'marketing_products.length',
|
||||||
header: 'Product Details',
|
header: 'Product Details',
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
|
const productsClickHandler = (item: Marketing) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
productsModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
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) {
|
||||||
return (
|
return (
|
||||||
@@ -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, setSelectedItem, productsModal]);
|
||||||
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={{
|
||||||
@@ -1263,7 +746,7 @@ const MarketingTable = () => {
|
|||||||
}
|
}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Gudang Fisik',
|
header: 'Kandang',
|
||||||
accessorFn(row) {
|
accessorFn(row) {
|
||||||
return row.product_warehouse.warehouse.name;
|
return row.product_warehouse.warehouse.name;
|
||||||
},
|
},
|
||||||
@@ -1294,7 +777,6 @@ const MarketingTable = () => {
|
|||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
initialValues={marketingFilterInitialValues}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -195,9 +195,7 @@ const SalesOrderFormModal = ({
|
|||||||
product.marketing_type?.value?.toLowerCase() === 'telur'
|
product.marketing_type?.value?.toLowerCase() === 'telur'
|
||||||
? convertionUnitValue === 'PETI'
|
? convertionUnitValue === 'PETI'
|
||||||
? 'PETI'
|
? 'PETI'
|
||||||
: convertionUnitValue === 'QTY'
|
: 'KG' // termasuk "QTY" dan "KG"
|
||||||
? 'QTY'
|
|
||||||
: 'KG'
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
|
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
|
||||||
@@ -209,6 +207,7 @@ const SalesOrderFormModal = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
vehicle_number: product.vehicle_number as string,
|
vehicle_number: product.vehicle_number as string,
|
||||||
|
kandang_id: product.kandang_id as number,
|
||||||
product_warehouse_id: product.product_warehouse_id as number,
|
product_warehouse_id: product.product_warehouse_id as number,
|
||||||
unit_price: parseFloat(String(product.unit_price || 0)),
|
unit_price: parseFloat(String(product.unit_price || 0)),
|
||||||
total_weight: parseFloat(String(product.total_weight || 0)),
|
total_weight: parseFloat(String(product.total_weight || 0)),
|
||||||
@@ -246,7 +245,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 +260,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) => {
|
||||||
@@ -456,9 +458,13 @@ const SalesOrderFormModal = ({
|
|||||||
);
|
);
|
||||||
}, [memoSalesOrder]);
|
}, [memoSalesOrder]);
|
||||||
|
|
||||||
|
const hasLoadedInitialValues = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getFilledInitialValues = async () => {
|
const getFilledInitialValues = async () => {
|
||||||
if (marketingId && isResponseSuccess(marketing)) {
|
if (marketingId && isResponseSuccess(marketing)) {
|
||||||
|
if (hasLoadedInitialValues.current) return;
|
||||||
|
hasLoadedInitialValues.current = true;
|
||||||
|
|
||||||
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
||||||
marketing.data
|
marketing.data
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { array, mixed, object } from 'yup';
|
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
|
||||||
|
|
||||||
export const MarketingFilterSchema = object({
|
|
||||||
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
|
||||||
status: mixed<OptionType<string>>().nullable(),
|
|
||||||
customer: mixed<OptionType<number>>().nullable(),
|
|
||||||
project_flock: mixed<OptionType<number>>().nullable(),
|
|
||||||
project_flock_kandang: mixed<OptionType<number>>().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type MarketingFilterFormValues = {
|
|
||||||
product_ids: OptionType<number>[];
|
|
||||||
status: OptionType<string> | null;
|
|
||||||
customer: OptionType<number> | null;
|
|
||||||
project_flock: OptionType<number> | null;
|
|
||||||
project_flock_kandang: OptionType<number> | null;
|
|
||||||
};
|
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
Marketing,
|
Marketing,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { formatDate, formatTitleCase } from '@/lib/helper';
|
import { formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
|
||||||
|
|
||||||
type MarketingSchemaType = {
|
type MarketingSchemaType = {
|
||||||
customer_id: number | undefined;
|
customer_id: number | undefined;
|
||||||
@@ -71,14 +70,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 });
|
||||||
});
|
});
|
||||||
@@ -98,21 +97,17 @@ export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
|
|||||||
export const SalesProductToFieldValues = (
|
export const SalesProductToFieldValues = (
|
||||||
product: BaseSalesOrder
|
product: BaseSalesOrder
|
||||||
): SalesOrderProductFormValues => {
|
): SalesOrderProductFormValues => {
|
||||||
const warehouseOption = {
|
|
||||||
value: product.product_warehouse.warehouse.id,
|
|
||||||
label: product.product_warehouse.warehouse.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
vehicle_number: product.vehicle_number,
|
vehicle_number: product.vehicle_number,
|
||||||
warehouse_id: product.product_warehouse.warehouse.id,
|
|
||||||
warehouse: warehouseOption,
|
|
||||||
kandang_id: product.product_warehouse.warehouse.id,
|
kandang_id: product.product_warehouse.warehouse.id,
|
||||||
kandang: warehouseOption,
|
kandang: {
|
||||||
|
value: product.product_warehouse.warehouse.id,
|
||||||
|
label: product.product_warehouse.warehouse.name,
|
||||||
|
},
|
||||||
product_warehouse: {
|
product_warehouse: {
|
||||||
value: product.product_warehouse.id,
|
value: product.product_warehouse.id,
|
||||||
label: getProductWarehouseOptionLabel(product.product_warehouse),
|
label: product.product_warehouse.product.name,
|
||||||
},
|
},
|
||||||
product_warehouse_data: product.product_warehouse,
|
product_warehouse_data: product.product_warehouse,
|
||||||
product_warehouse_id: product.product_warehouse.id,
|
product_warehouse_id: product.product_warehouse.id,
|
||||||
@@ -123,17 +118,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,36 +139,11 @@ export const DeliveryProductToFieldValues = (
|
|||||||
delivery: BaseDeliveryOrder
|
delivery: BaseDeliveryOrder
|
||||||
): DeliveryOrderProductFormValues[] => {
|
): DeliveryOrderProductFormValues[] => {
|
||||||
const data = delivery.deliveries.map((item) => {
|
const data = delivery.deliveries.map((item) => {
|
||||||
const salesOrder =
|
const soId = salesOrders.find(
|
||||||
salesOrders.find((so) => so.id === item.marketing_product_id) ??
|
(so) => so.product_warehouse.id === item.product_warehouse.id
|
||||||
salesOrders.find(
|
)?.id;
|
||||||
(so) => so.product_warehouse.id === item.product_warehouse.id
|
|
||||||
);
|
|
||||||
const warehouseOption = {
|
|
||||||
value: item.product_warehouse.warehouse.id,
|
|
||||||
label: item.product_warehouse.warehouse.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialSisaBerat =
|
|
||||||
item?.total_weight &&
|
|
||||||
salesOrder?.weight_per_convertion &&
|
|
||||||
salesOrder?.total_peti
|
|
||||||
? Number(item.total_weight) -
|
|
||||||
Number(salesOrder.weight_per_convertion) *
|
|
||||||
Number(salesOrder.total_peti)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const initialPricePerConvertion =
|
|
||||||
item?.total_price &&
|
|
||||||
salesOrder?.total_peti &&
|
|
||||||
Number(salesOrder.total_peti) !== 0
|
|
||||||
? (Number(item.total_price) -
|
|
||||||
initialSisaBerat * Number(item.unit_price || 0)) /
|
|
||||||
Number(salesOrder.total_peti)
|
|
||||||
: Number(item?.unit_price || 0);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: salesOrder?.id,
|
id: soId,
|
||||||
unit_price: item.unit_price,
|
unit_price: item.unit_price,
|
||||||
total_weight: item.total_weight,
|
total_weight: item.total_weight,
|
||||||
qty: item.qty,
|
qty: item.qty,
|
||||||
@@ -191,40 +152,19 @@ 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: soId,
|
||||||
marketing_type: salesOrder?.marketing_type
|
|
||||||
? {
|
|
||||||
value:
|
|
||||||
salesOrder?.marketing_type === 'AYAM' ||
|
|
||||||
salesOrder?.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM,AYAM_PULLET'
|
|
||||||
: salesOrder?.marketing_type,
|
|
||||||
label: formatTitleCase(
|
|
||||||
salesOrder?.marketing_type === 'AYAM' ||
|
|
||||||
salesOrder?.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM'
|
|
||||||
: salesOrder?.marketing_type
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
convertion_unit: salesOrder?.convertion_unit
|
|
||||||
? {
|
|
||||||
value: salesOrder?.convertion_unit.toLowerCase(),
|
|
||||||
label: formatTitleCase(salesOrder?.convertion_unit),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
marketing_product: {
|
marketing_product: {
|
||||||
id: item.marketing_product_id ?? salesOrder?.id,
|
id: soId,
|
||||||
vehicle_number: item.vehicle_number,
|
vehicle_number: item.vehicle_number,
|
||||||
warehouse_id: item.product_warehouse.warehouse.id,
|
|
||||||
warehouse: warehouseOption,
|
|
||||||
kandang_id: item.product_warehouse.warehouse.id,
|
kandang_id: item.product_warehouse.warehouse.id,
|
||||||
kandang: warehouseOption,
|
kandang: {
|
||||||
|
value: item.product_warehouse.warehouse.id,
|
||||||
|
label: item.product_warehouse.warehouse.name,
|
||||||
|
},
|
||||||
product_warehouse: {
|
product_warehouse: {
|
||||||
value: item.product_warehouse.id,
|
value: item.product_warehouse.id,
|
||||||
label: getProductWarehouseOptionLabel(item.product_warehouse),
|
label: item.product_warehouse.product.name,
|
||||||
},
|
},
|
||||||
product_warehouse_data: item.product_warehouse,
|
|
||||||
product_warehouse_id: item.product_warehouse.id,
|
product_warehouse_id: item.product_warehouse.id,
|
||||||
unit_price: item.unit_price,
|
unit_price: item.unit_price,
|
||||||
total_weight: item.total_weight,
|
total_weight: item.total_weight,
|
||||||
@@ -232,13 +172,8 @@ export const DeliveryProductToFieldValues = (
|
|||||||
avg_weight: item.avg_weight,
|
avg_weight: item.avg_weight,
|
||||||
total_price: item.total_price,
|
total_price: item.total_price,
|
||||||
},
|
},
|
||||||
total_peti: salesOrder?.total_peti,
|
|
||||||
weight_per_convertion:
|
|
||||||
item?.weight_per_convertion ?? salesOrder?.weight_per_convertion ?? 0,
|
|
||||||
price_per_convertion: initialPricePerConvertion,
|
|
||||||
} as DeliveryOrderProductFormValues;
|
} as DeliveryOrderProductFormValues;
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
export const mergeSOwithDO = (
|
export const mergeSOwithDO = (
|
||||||
@@ -246,25 +181,10 @@ export const mergeSOwithDO = (
|
|||||||
deliveryOrders: DeliveryOrderProductFormValues[],
|
deliveryOrders: DeliveryOrderProductFormValues[],
|
||||||
autofill?: boolean
|
autofill?: boolean
|
||||||
): DeliveryOrderProductFormValues[] => {
|
): DeliveryOrderProductFormValues[] => {
|
||||||
const hasDeliveryOrders = deliveryOrders.length > 0;
|
|
||||||
|
|
||||||
return salesOrders.map((so) => {
|
return salesOrders.map((so) => {
|
||||||
const delivery = deliveryOrders.find(
|
const delivery = deliveryOrders.find(
|
||||||
(d) => d?.marketing_product_id === so.id
|
(d) => d?.marketing_product_id === so.id
|
||||||
);
|
);
|
||||||
const isTelurQty =
|
|
||||||
so.marketing_type?.value?.toLowerCase() === 'telur' &&
|
|
||||||
so.convertion_unit?.value?.toLowerCase() === 'qty';
|
|
||||||
const salesOrderUnitPrice =
|
|
||||||
isTelurQty && Number(so.total_price || 0) > 0 && Number(so.qty || 0) > 0
|
|
||||||
? Number(so.total_price) / Number(so.qty)
|
|
||||||
: so.unit_price;
|
|
||||||
const salesOrderPricePerQty =
|
|
||||||
isTelurQty &&
|
|
||||||
Number(so.total_price || 0) > 0 &&
|
|
||||||
Number(so.total_weight || 0) > 0
|
|
||||||
? Number(so.total_price) / Number(so.total_weight)
|
|
||||||
: so.price_per_qty;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...so, // nilai dasar dari sales order
|
...so, // nilai dasar dari sales order
|
||||||
@@ -272,50 +192,30 @@ export const mergeSOwithDO = (
|
|||||||
delivery_date: delivery?.delivery_date || undefined,
|
delivery_date: delivery?.delivery_date || undefined,
|
||||||
do_number: delivery?.do_number || undefined,
|
do_number: delivery?.do_number || undefined,
|
||||||
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
|
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
|
||||||
unit_price:
|
unit_price: autofill ? so.unit_price : delivery?.unit_price,
|
||||||
autofill && hasDeliveryOrders
|
total_weight: autofill ? so.total_weight : delivery?.total_weight,
|
||||||
? delivery?.unit_price
|
qty: autofill ? so.qty : delivery?.qty,
|
||||||
: salesOrderUnitPrice,
|
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
|
||||||
total_weight:
|
total_price: autofill ? so.total_price : delivery?.total_price,
|
||||||
autofill && hasDeliveryOrders
|
|
||||||
? delivery?.total_weight
|
|
||||||
: so.total_weight,
|
|
||||||
qty: autofill && hasDeliveryOrders ? delivery?.qty : so.qty,
|
|
||||||
avg_weight:
|
|
||||||
autofill && hasDeliveryOrders ? delivery?.avg_weight : so.avg_weight,
|
|
||||||
total_price:
|
|
||||||
autofill && hasDeliveryOrders ? delivery?.total_price : so.total_price,
|
|
||||||
marketing_product: so, // jika ada, override
|
marketing_product: so, // jika ada, override
|
||||||
uom: autofill && hasDeliveryOrders ? delivery?.uom : so.uom,
|
uom: autofill ? so.uom : delivery?.uom,
|
||||||
weight_per_convertion:
|
weight_per_convertion: autofill
|
||||||
autofill && hasDeliveryOrders
|
? so.weight_per_convertion
|
||||||
? delivery?.weight_per_convertion
|
: delivery?.weight_per_convertion,
|
||||||
: so.weight_per_convertion,
|
price_per_convertion: autofill
|
||||||
price_per_convertion:
|
? so.price_per_convertion
|
||||||
autofill && hasDeliveryOrders
|
: delivery?.price_per_convertion,
|
||||||
? delivery?.price_per_convertion
|
convertion_unit: autofill
|
||||||
: so.price_per_convertion,
|
? so.convertion_unit
|
||||||
convertion_unit:
|
: delivery?.convertion_unit,
|
||||||
autofill && hasDeliveryOrders
|
marketing_type: autofill ? so.marketing_type : delivery?.marketing_type,
|
||||||
? delivery?.convertion_unit
|
total_peti: autofill ? so.total_peti : delivery?.total_peti,
|
||||||
: so.convertion_unit,
|
price_per_qty: autofill ? so.price_per_qty : delivery?.price_per_qty,
|
||||||
marketing_type:
|
sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat,
|
||||||
autofill && hasDeliveryOrders
|
price_sisa_berat: autofill
|
||||||
? delivery?.marketing_type
|
? so.price_sisa_berat
|
||||||
: so.marketing_type,
|
: delivery?.price_sisa_berat,
|
||||||
total_peti:
|
week: autofill ? so.week : delivery?.week,
|
||||||
autofill && hasDeliveryOrders ? delivery?.total_peti : so.total_peti,
|
|
||||||
price_per_qty:
|
|
||||||
autofill && hasDeliveryOrders
|
|
||||||
? delivery?.price_per_qty
|
|
||||||
: salesOrderPricePerQty,
|
|
||||||
sisa_berat:
|
|
||||||
autofill && hasDeliveryOrders ? delivery?.sisa_berat : so.sisa_berat,
|
|
||||||
price_sisa_berat:
|
|
||||||
autofill && hasDeliveryOrders
|
|
||||||
? delivery?.price_sisa_berat
|
|
||||||
: so.price_sisa_berat,
|
|
||||||
week: autofill && hasDeliveryOrders ? delivery?.week : so.week,
|
|
||||||
} as DeliveryOrderProductFormValues;
|
} as DeliveryOrderProductFormValues;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+82
-160
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
DeliveryOrderProductFormValues,
|
DeliveryOrderProductFormValues,
|
||||||
DeliveryOrderProductSchema,
|
DeliveryOrderProductSchema,
|
||||||
@@ -32,63 +32,6 @@ import Dropdown from '@/components/Dropdown';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
||||||
|
|
||||||
type PricingOption =
|
|
||||||
| string
|
|
||||||
| {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
type PricingSource =
|
|
||||||
| {
|
|
||||||
marketing_type?: PricingOption;
|
|
||||||
convertion_unit?: PricingOption;
|
|
||||||
total_price?: string | number | null;
|
|
||||||
qty?: string | number | null;
|
|
||||||
total_weight?: string | number | null;
|
|
||||||
unit_price?: string | number | null;
|
|
||||||
price_per_qty?: number | null;
|
|
||||||
}
|
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
const getOptionValue = (value?: PricingOption) => {
|
|
||||||
if (!value) return undefined;
|
|
||||||
if (typeof value === 'string') return value.toLowerCase();
|
|
||||||
|
|
||||||
return value.value?.toLowerCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isTelurQtyProduct = (value?: PricingSource) =>
|
|
||||||
getOptionValue(value?.marketing_type) === 'telur' &&
|
|
||||||
getOptionValue(value?.convertion_unit) === 'qty';
|
|
||||||
|
|
||||||
const getDisplayedUnitPrice = (value?: PricingSource) => {
|
|
||||||
if (
|
|
||||||
isTelurQtyProduct(value) &&
|
|
||||||
Number(value?.total_price || 0) > 0 &&
|
|
||||||
Number(value?.qty || 0) > 0
|
|
||||||
) {
|
|
||||||
return Number(value?.total_price) / Number(value?.qty);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value?.unit_price ?? undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDisplayedPricePerQty = (value?: PricingSource) => {
|
|
||||||
if (
|
|
||||||
isTelurQtyProduct(value) &&
|
|
||||||
Number(value?.total_price || 0) > 0 &&
|
|
||||||
Number(value?.total_weight || 0) > 0
|
|
||||||
) {
|
|
||||||
return Number(value?.total_price) / Number(value?.total_weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value?.price_per_qty ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DeliveryOrderProductForm = ({
|
const DeliveryOrderProductForm = ({
|
||||||
formState,
|
formState,
|
||||||
salesOrders,
|
salesOrders,
|
||||||
@@ -133,7 +76,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
? (Number(initialValues.total_price) -
|
? (Number(initialValues.total_price) -
|
||||||
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
||||||
Number(initialValues.total_peti)
|
Number(initialValues.total_peti)
|
||||||
: Number(initialValues?.unit_price || 0);
|
: 0;
|
||||||
|
|
||||||
const initialPriceSisaBerat =
|
const initialPriceSisaBerat =
|
||||||
initialValues?.total_price && initialValues?.total_peti
|
initialValues?.total_price && initialValues?.total_peti
|
||||||
@@ -146,6 +89,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(() => {
|
||||||
@@ -160,7 +112,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
if (!Boolean(item.qty)) {
|
if (!Boolean(item.qty)) {
|
||||||
return {
|
return {
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.warehouse?.label ?? item.marketing_product?.kandang?.label}`,
|
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`,
|
||||||
} as OptionType;
|
} as OptionType;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@@ -181,19 +133,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,
|
||||||
@@ -209,27 +154,6 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === initialValues?.marketing_product_id
|
(item) => item.id === initialValues?.marketing_product_id
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultPricingSource: PricingSource = {
|
|
||||||
marketing_type:
|
|
||||||
initialValues?.marketing_type ?? salesOrder?.marketing_type ?? null,
|
|
||||||
convertion_unit:
|
|
||||||
initialValues?.convertion_unit ?? salesOrder?.convertion_unit ?? null,
|
|
||||||
total_price:
|
|
||||||
deliveryOrder?.total_price ??
|
|
||||||
initialValues?.total_price ??
|
|
||||||
salesOrder?.total_price,
|
|
||||||
qty: deliveryOrder?.qty ?? initialValues?.qty ?? salesOrder?.qty,
|
|
||||||
total_weight:
|
|
||||||
deliveryOrder?.total_weight ??
|
|
||||||
initialValues?.total_weight ??
|
|
||||||
salesOrder?.total_weight,
|
|
||||||
unit_price:
|
|
||||||
deliveryOrder?.unit_price ??
|
|
||||||
initialValues?.unit_price ??
|
|
||||||
salesOrder?.unit_price,
|
|
||||||
price_per_qty: initialValues?.price_per_qty ?? null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const formik = useFormik<DeliveryOrderProductFormValues>({
|
const formik = useFormik<DeliveryOrderProductFormValues>({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -243,7 +167,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
undefined,
|
undefined,
|
||||||
marketing_product_id:
|
marketing_product_id:
|
||||||
salesOrder?.id || initialValues?.marketing_product_id || undefined,
|
salesOrder?.id || initialValues?.marketing_product_id || undefined,
|
||||||
unit_price: getDisplayedUnitPrice(defaultPricingSource),
|
unit_price:
|
||||||
|
deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined,
|
||||||
total_weight:
|
total_weight:
|
||||||
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
|
deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
|
||||||
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
|
qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
|
||||||
@@ -261,7 +186,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
convertion_unit: initialValues?.convertion_unit || null,
|
convertion_unit: initialValues?.convertion_unit || null,
|
||||||
marketing_type: initialValues?.marketing_type || null,
|
marketing_type: initialValues?.marketing_type || null,
|
||||||
total_peti: initialValues?.total_peti ?? null,
|
total_peti: initialValues?.total_peti ?? null,
|
||||||
price_per_qty: getDisplayedPricePerQty(defaultPricingSource),
|
price_per_qty: initialValues?.price_per_qty ?? null,
|
||||||
sisa_berat: initialSisaBerat,
|
sisa_berat: initialSisaBerat,
|
||||||
price_sisa_berat: initialPriceSisaBerat,
|
price_sisa_berat: initialPriceSisaBerat,
|
||||||
week: initialValues?.week ?? null,
|
week: initialValues?.week ?? null,
|
||||||
@@ -299,6 +224,8 @@ const DeliveryOrderProductForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { resetForm } = formik;
|
||||||
|
|
||||||
const hasWeekField = useMemo(() => {
|
const hasWeekField = useMemo(() => {
|
||||||
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
||||||
if (marketingType === 'ayam_pullet') {
|
if (marketingType === 'ayam_pullet') {
|
||||||
@@ -318,9 +245,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
return false;
|
return false;
|
||||||
}, [formik.values.marketing_product, formik.values.marketing_type]);
|
}, [formik.values.marketing_product, formik.values.marketing_type]);
|
||||||
|
|
||||||
const handleResetForm = () => {
|
const handleResetForm = useCallback(() => {
|
||||||
setFormErrorMessage('');
|
setFormErrorMessage('');
|
||||||
formik.resetForm({
|
resetForm({
|
||||||
values: {
|
values: {
|
||||||
delivery_date: '',
|
delivery_date: '',
|
||||||
vehicle_number: '',
|
vehicle_number: '',
|
||||||
@@ -344,17 +271,20 @@ const DeliveryOrderProductForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// setSelectedProduct(null);
|
// setSelectedProduct(null);
|
||||||
};
|
}, [resetForm]);
|
||||||
|
|
||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = useCallback(
|
||||||
setCurrentInput(field);
|
(field: string) => {
|
||||||
|
setCurrentInput(field);
|
||||||
|
|
||||||
handleMarketingCalculation(field, {
|
handleMarketingCalculation(field, {
|
||||||
values: formik.values,
|
values: formik.values,
|
||||||
setFieldValue: formik.setFieldValue,
|
setFieldValue: formik.setFieldValue,
|
||||||
hasSisaBerat,
|
hasSisaBerat,
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[formik.values, formik.setFieldValue, hasSisaBerat]
|
||||||
|
);
|
||||||
|
|
||||||
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
||||||
const handleFieldChange = (
|
const handleFieldChange = (
|
||||||
@@ -399,28 +329,25 @@ const DeliveryOrderProductForm = ({
|
|||||||
|
|
||||||
const { setValues: setFormikValues } = formik;
|
const { setValues: setFormikValues } = formik;
|
||||||
|
|
||||||
|
const processedInitialValuesRef = useRef<number | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
if (
|
if (processedInitialValuesRef.current === initialValues.id) return;
|
||||||
!Boolean(initialValues.qty) &&
|
processedInitialValuesRef.current = initialValues.id as number;
|
||||||
!Boolean(initialValues.marketing_product_id)
|
|
||||||
) {
|
if (!Boolean(initialValues.qty)) {
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
} else {
|
} else {
|
||||||
setFormikValues({
|
setFormikValues(initialValues);
|
||||||
...initialValues,
|
|
||||||
unit_price: getDisplayedUnitPrice(initialValues),
|
|
||||||
price_per_qty: getDisplayedPricePerQty(initialValues),
|
|
||||||
});
|
|
||||||
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?.kandang?.label}`,
|
||||||
} as OptionType);
|
} as OptionType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [initialValues]);
|
}, [handleResetForm, initialValues, setFormikValues]);
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||||
@@ -431,16 +358,17 @@ 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 ?? ''
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleBlurField('week');
|
if (formik.values.week) {
|
||||||
}, [formik.values.week]);
|
handleBlurField('week');
|
||||||
|
}
|
||||||
|
}, [formik.values.week, handleBlurField]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -541,11 +469,10 @@ const DeliveryOrderProductForm = ({
|
|||||||
marketing_product_id: selected.value as number,
|
marketing_product_id: selected.value as number,
|
||||||
marketing_product: soFieldValues,
|
marketing_product: soFieldValues,
|
||||||
qty: so.qty,
|
qty: so.qty,
|
||||||
unit_price: getDisplayedUnitPrice(so),
|
unit_price: so.unit_price,
|
||||||
total_price: so.total_price,
|
total_price: so.total_price,
|
||||||
avg_weight: so.avg_weight,
|
avg_weight: so.avg_weight,
|
||||||
total_weight: so.total_weight,
|
total_weight: so.total_weight,
|
||||||
price_per_qty: getDisplayedPricePerQty(so),
|
|
||||||
vehicle_number: so.vehicle_number,
|
vehicle_number: so.vehicle_number,
|
||||||
week: soFieldValues.week ?? null,
|
week: soFieldValues.week ?? null,
|
||||||
});
|
});
|
||||||
@@ -556,11 +483,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
text={
|
text={
|
||||||
exisitingValues?.find(
|
exisitingValues?.find(
|
||||||
(item) => item.id === selectedProduct?.value
|
(item) => item.id === selectedProduct?.value
|
||||||
)?.marketing_product?.warehouse?.label ??
|
)?.marketing_product?.kandang?.label ?? ''
|
||||||
exisitingValues?.find(
|
|
||||||
(item) => item.id === selectedProduct?.value
|
|
||||||
)?.marketing_product?.kandang?.label ??
|
|
||||||
''
|
|
||||||
}
|
}
|
||||||
color='success'
|
color='success'
|
||||||
className={{
|
className={{
|
||||||
@@ -726,7 +649,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
placeholder='Masukan Total Peti'
|
placeholder='Masukan Total Peti'
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-base-content/50'>Peti</span>
|
<span className='text-sm text-base-content/50'>Kg</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
||||||
@@ -776,9 +699,6 @@ const DeliveryOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.total_weight}
|
errorMessage={formik.errors.total_weight}
|
||||||
placeholder='Masukan Total Bobot'
|
placeholder='Masukan Total Bobot'
|
||||||
disabled={
|
|
||||||
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -805,8 +725,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 +738,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
|
||||||
|
: '')
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -846,32 +768,12 @@ const DeliveryOrderProductForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Harga Satuan */}
|
{/* Harga per butir untuk TELUR + QTY */}
|
||||||
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
|
||||||
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
|
|
||||||
name='unit_price'
|
|
||||||
value={formik.values.unit_price}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = Number(e.target.value);
|
|
||||||
handleFieldChange('unit_price', value, () =>
|
|
||||||
setCurrentInput(e.target.name)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
isError={Boolean(formik.errors.unit_price)}
|
|
||||||
errorMessage={formik.errors.unit_price}
|
|
||||||
placeholder='Masukan Harga Satuan'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Harga per kg untuk TELUR + QTY */}
|
|
||||||
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
||||||
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga / Kg (Rp)'
|
label='Harga / Butir (Rp)'
|
||||||
name='price_per_qty'
|
name='price_per_qty'
|
||||||
value={formik.values.price_per_qty ?? undefined}
|
value={formik.values.price_per_qty ?? undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -885,7 +787,27 @@ const DeliveryOrderProductForm = ({
|
|||||||
Boolean(formik.errors.price_per_qty)
|
Boolean(formik.errors.price_per_qty)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.price_per_qty}
|
errorMessage={formik.errors.price_per_qty}
|
||||||
placeholder='Masukan Harga per Kg'
|
placeholder='Masukan Harga per Butir'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Harga Satuan */}
|
||||||
|
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
||||||
|
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label={`Harga / ${isResponseSuccess(productData) ? productData?.data?.uom?.name : 'Produk'} (Rp)`}
|
||||||
|
name='unit_price'
|
||||||
|
value={formik.values.unit_price}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
handleFieldChange('unit_price', value, () =>
|
||||||
|
setCurrentInput(e.target.name)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isError={Boolean(formik.errors.unit_price)}
|
||||||
|
errorMessage={formik.errors.unit_price}
|
||||||
|
placeholder='Masukan Harga Satuan'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+8
-20
@@ -3,11 +3,6 @@ import * as Yup from 'yup';
|
|||||||
|
|
||||||
type SalesOrderProductSchemaType = {
|
type SalesOrderProductSchemaType = {
|
||||||
id?: number | undefined;
|
id?: number | undefined;
|
||||||
warehouse_id?: number;
|
|
||||||
warehouse?: {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
kandang_id?: number;
|
kandang_id?: number;
|
||||||
kandang?: {
|
kandang?: {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -49,22 +44,15 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
|||||||
Yup.object({
|
Yup.object({
|
||||||
id: Yup.number(),
|
id: Yup.number(),
|
||||||
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
||||||
warehouse: Yup.object({
|
|
||||||
value: Yup.number()
|
|
||||||
.min(1, 'Gudang fisik wajib diisi!')
|
|
||||||
.required('Gudang fisik wajib diisi!'),
|
|
||||||
label: Yup.string().required('Gudang fisik wajib diisi!'),
|
|
||||||
}).nullable(),
|
|
||||||
warehouse_id: Yup.number()
|
|
||||||
.min(1, 'Gudang fisik wajib diisi!')
|
|
||||||
.required('Gudang fisik wajib diisi!'),
|
|
||||||
kandang: Yup.object({
|
kandang: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number()
|
||||||
label: Yup.string().required(),
|
.min(1, 'Kandang wajib diisi!')
|
||||||
})
|
.required('Kandang wajib diisi!'),
|
||||||
.nullable()
|
label: Yup.string().required('Kandang wajib diisi!'),
|
||||||
.optional(),
|
}).nullable(),
|
||||||
kandang_id: Yup.number().optional(),
|
kandang_id: Yup.number()
|
||||||
|
.min(1, 'Kandang wajib diisi!')
|
||||||
|
.required('Kandang wajib diisi!'),
|
||||||
product_warehouse: Yup.object({
|
product_warehouse: Yup.object({
|
||||||
value: Yup.number()
|
value: Yup.number()
|
||||||
.min(1, 'Produk wajib diisi!')
|
.min(1, 'Produk wajib diisi!')
|
||||||
|
|||||||
+82
-157
@@ -5,9 +5,9 @@ import {
|
|||||||
SalesOrderProductFormValues,
|
SalesOrderProductFormValues,
|
||||||
SalesOrderProductSchema,
|
SalesOrderProductSchema,
|
||||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||||
import { RefObject, useEffect, useMemo, useState } from 'react';
|
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { WarehouseApi } from '@/services/api/master-data';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
||||||
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
|
||||||
|
|
||||||
const SalesOrderProductForm = ({
|
const SalesOrderProductForm = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -68,25 +67,7 @@ const SalesOrderProductForm = ({
|
|||||||
? (Number(initialValues.total_price) -
|
? (Number(initialValues.total_price) -
|
||||||
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
initialSisaBerat * Number(initialValues.unit_price || 0)) /
|
||||||
Number(initialValues.total_peti)
|
Number(initialValues.total_peti)
|
||||||
: Number(initialValues?.unit_price || 0);
|
: 0;
|
||||||
|
|
||||||
const isInitialTelurQty =
|
|
||||||
initialValues?.marketing_type?.value?.toLowerCase() === 'telur' &&
|
|
||||||
initialValues?.convertion_unit?.value?.toLowerCase() === 'qty';
|
|
||||||
|
|
||||||
const initialUnitPrice =
|
|
||||||
isInitialTelurQty &&
|
|
||||||
Number(initialValues?.total_price || 0) > 0 &&
|
|
||||||
Number(initialValues?.qty || 0) > 0
|
|
||||||
? Number(initialValues?.total_price) / Number(initialValues?.qty)
|
|
||||||
: initialValues?.unit_price || '';
|
|
||||||
|
|
||||||
const initialPricePerQty =
|
|
||||||
isInitialTelurQty &&
|
|
||||||
Number(initialValues?.total_price || 0) > 0 &&
|
|
||||||
Number(initialValues?.total_weight || 0) > 0
|
|
||||||
? Number(initialValues?.total_price) / Number(initialValues?.total_weight)
|
|
||||||
: (initialValues?.price_per_qty ?? null);
|
|
||||||
|
|
||||||
const initialPriceSisaBerat =
|
const initialPriceSisaBerat =
|
||||||
initialValues?.total_price && initialValues?.total_peti
|
initialValues?.total_price && initialValues?.total_peti
|
||||||
@@ -103,15 +84,11 @@ const SalesOrderProductForm = ({
|
|||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
vehicle_number: initialValues?.vehicle_number || '',
|
vehicle_number: initialValues?.vehicle_number || '',
|
||||||
warehouse_id:
|
|
||||||
initialValues?.warehouse_id ?? initialValues?.kandang_id ?? undefined,
|
|
||||||
warehouse: initialValues?.warehouse ?? initialValues?.kandang ?? null,
|
|
||||||
kandang_id: initialValues?.kandang_id || undefined,
|
kandang_id: initialValues?.kandang_id || undefined,
|
||||||
kandang: initialValues?.kandang || null,
|
kandang: initialValues?.kandang || null,
|
||||||
product_warehouse: initialValues?.product_warehouse || null,
|
product_warehouse: initialValues?.product_warehouse || null,
|
||||||
product_warehouse_data: initialValues?.product_warehouse_data || null,
|
|
||||||
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
||||||
unit_price: initialUnitPrice,
|
unit_price: initialValues?.unit_price || '',
|
||||||
total_weight: initialValues?.total_weight || '',
|
total_weight: initialValues?.total_weight || '',
|
||||||
qty: initialValues?.qty || '',
|
qty: initialValues?.qty || '',
|
||||||
avg_weight: initialValues?.avg_weight || '',
|
avg_weight: initialValues?.avg_weight || '',
|
||||||
@@ -125,7 +102,7 @@ const SalesOrderProductForm = ({
|
|||||||
convertion_unit: initialValues?.convertion_unit || null,
|
convertion_unit: initialValues?.convertion_unit || null,
|
||||||
marketing_type: initialValues?.marketing_type || null,
|
marketing_type: initialValues?.marketing_type || null,
|
||||||
total_peti: initialValues?.total_peti ?? null,
|
total_peti: initialValues?.total_peti ?? null,
|
||||||
price_per_qty: initialPricePerQty,
|
price_per_qty: initialValues?.price_per_qty ?? null,
|
||||||
sisa_berat: initialSisaBerat,
|
sisa_berat: initialSisaBerat,
|
||||||
price_sisa_berat: initialPriceSisaBerat,
|
price_sisa_berat: initialPriceSisaBerat,
|
||||||
week: initialValues?.week ?? null,
|
week: initialValues?.week ?? null,
|
||||||
@@ -155,11 +132,11 @@ const SalesOrderProductForm = ({
|
|||||||
|
|
||||||
// ===== Options =====
|
// ===== Options =====
|
||||||
const {
|
const {
|
||||||
options: warehouseOptions,
|
options: kandangSourceOptions,
|
||||||
isLoadingOptions: isLoadingWarehouseOptions,
|
isLoadingOptions: isLoadingKandangSourceOptions,
|
||||||
setInputValue: setWarehouseSearchValue,
|
setInputValue: setKandangInputValue,
|
||||||
loadMore: loadMoreWarehouses,
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// Options Week dari minggu 1 - 22
|
// Options Week dari minggu 1 - 22
|
||||||
// const optionsWeek = useMemo(() => {
|
// const optionsWeek = useMemo(() => {
|
||||||
@@ -170,6 +147,7 @@ const SalesOrderProductForm = ({
|
|||||||
// }, []);
|
// }, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
options: warehouseSourceOptions,
|
||||||
rawData: warehouseSourceRawData,
|
rawData: warehouseSourceRawData,
|
||||||
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
||||||
setInputValue: setWarehouseInputValue,
|
setInputValue: setWarehouseInputValue,
|
||||||
@@ -178,69 +156,32 @@ const SalesOrderProductForm = ({
|
|||||||
ProductWarehouseApi.basePath,
|
ProductWarehouseApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
'product.name',
|
'product.name',
|
||||||
'search',
|
'',
|
||||||
{
|
{
|
||||||
limit: '100',
|
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
||||||
available_only: 'true',
|
|
||||||
warehouse_id: formik.values.warehouse_id?.toString() ?? '',
|
|
||||||
type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
|
type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const productOptionsFiltered = useMemo(() => {
|
const productOptionsFiltered = useMemo(() => {
|
||||||
if (!isResponseSuccess(warehouseSourceRawData)) {
|
return warehouseSourceOptions.filter(
|
||||||
return initialValues?.product_warehouse
|
(product) =>
|
||||||
? [initialValues.product_warehouse]
|
!exisitingValues
|
||||||
: [];
|
?.map((item) => item.product_warehouse_id)
|
||||||
}
|
.includes(product.value)
|
||||||
|
|
||||||
const selectedProductIds = new Set(
|
|
||||||
exisitingValues
|
|
||||||
?.filter((item) => item.id !== initialValues?.id)
|
|
||||||
.map((item) => Number(item.product_warehouse_id))
|
|
||||||
.filter((item) => item > 0) ?? []
|
|
||||||
);
|
);
|
||||||
|
}, [warehouseSourceOptions, exisitingValues]);
|
||||||
const options = warehouseSourceRawData.data
|
|
||||||
.filter((item: ProductWarehouse) => !selectedProductIds.has(item.id))
|
|
||||||
.map((item: ProductWarehouse) => ({
|
|
||||||
value: item.id,
|
|
||||||
label: getProductWarehouseOptionLabel(item),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (
|
|
||||||
initialValues?.product_warehouse &&
|
|
||||||
initialValues?.product_warehouse_id
|
|
||||||
) {
|
|
||||||
const exists = options.find(
|
|
||||||
(option) =>
|
|
||||||
Number(option.value) === Number(initialValues.product_warehouse_id)
|
|
||||||
);
|
|
||||||
if (!exists) {
|
|
||||||
options.push(initialValues.product_warehouse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [warehouseSourceRawData, exisitingValues, initialValues]);
|
|
||||||
|
|
||||||
// ===== Handler =====
|
// ===== Handler =====
|
||||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const warehouse = (val as OptionType | null) ?? null;
|
formik.setFieldValue('kandang', val as OptionType);
|
||||||
|
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
|
||||||
formik.setFieldValue('warehouse', warehouse);
|
|
||||||
formik.setFieldValue('warehouse_id', warehouse?.value);
|
|
||||||
formik.setFieldValue('kandang', warehouse);
|
|
||||||
formik.setFieldValue('kandang_id', warehouse?.value);
|
|
||||||
formik.setFieldValue('product_warehouse_id', null);
|
formik.setFieldValue('product_warehouse_id', null);
|
||||||
formik.setFieldValue('product_warehouse', null);
|
formik.setFieldValue('product_warehouse', null);
|
||||||
formik.setFieldValue('product_warehouse_data', null);
|
|
||||||
formik.setFieldValue('qty', '');
|
formik.setFieldValue('qty', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const productWarehouseChangeHandler = (
|
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
val: OptionType | OptionType[] | null
|
|
||||||
) => {
|
|
||||||
formik.setFieldValue('product_warehouse', val as OptionType);
|
formik.setFieldValue('product_warehouse', val as OptionType);
|
||||||
const newId = (val as OptionType)?.value;
|
const newId = (val as OptionType)?.value;
|
||||||
formik.setFieldValue('product_warehouse_id', newId);
|
formik.setFieldValue('product_warehouse_id', newId);
|
||||||
@@ -250,13 +191,7 @@ const SalesOrderProductForm = ({
|
|||||||
(item: ProductWarehouse) => item.id === newId
|
(item: ProductWarehouse) => item.id === newId
|
||||||
);
|
);
|
||||||
setSelectedProductWarehouse(productWarehouse || null);
|
setSelectedProductWarehouse(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 &&
|
||||||
@@ -269,8 +204,6 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
handleBlurField('qty');
|
handleBlurField('qty');
|
||||||
} else {
|
} else {
|
||||||
setSelectedProductWarehouse(null);
|
|
||||||
formik.setFieldValue('product_warehouse_data', null);
|
|
||||||
formik.setFieldValue('qty', '');
|
formik.setFieldValue('qty', '');
|
||||||
formik.setFieldValue('uom', '');
|
formik.setFieldValue('uom', '');
|
||||||
formik.setFieldValue('week', null);
|
formik.setFieldValue('week', null);
|
||||||
@@ -284,12 +217,9 @@ const SalesOrderProductForm = ({
|
|||||||
formik.resetForm({
|
formik.resetForm({
|
||||||
values: {
|
values: {
|
||||||
vehicle_number: '',
|
vehicle_number: '',
|
||||||
warehouse_id: undefined,
|
|
||||||
warehouse: null,
|
|
||||||
kandang_id: undefined,
|
kandang_id: undefined,
|
||||||
kandang: null,
|
kandang: null,
|
||||||
product_warehouse: null,
|
product_warehouse: null,
|
||||||
product_warehouse_data: null,
|
|
||||||
product_warehouse_id: undefined,
|
product_warehouse_id: undefined,
|
||||||
unit_price: '',
|
unit_price: '',
|
||||||
total_weight: '',
|
total_weight: '',
|
||||||
@@ -310,15 +240,18 @@ const SalesOrderProductForm = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = useCallback(
|
||||||
setCurrentInput(field);
|
(field: string) => {
|
||||||
|
setCurrentInput(field);
|
||||||
|
|
||||||
handleMarketingCalculation(field, {
|
handleMarketingCalculation(field, {
|
||||||
values: formik.values,
|
values: formik.values,
|
||||||
setFieldValue: formik.setFieldValue,
|
setFieldValue: formik.setFieldValue,
|
||||||
hasSisaBerat,
|
hasSisaBerat,
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[formik.values, formik.setFieldValue, hasSisaBerat]
|
||||||
|
);
|
||||||
|
|
||||||
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
||||||
const handleFieldChange = (
|
const handleFieldChange = (
|
||||||
@@ -377,12 +310,10 @@ const SalesOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleBlurField('week');
|
if (formik.values.week) {
|
||||||
}, [formik.values.week]);
|
handleBlurField('week');
|
||||||
|
}
|
||||||
useEffect(() => {
|
}, [formik.values.week, handleBlurField]);
|
||||||
setSelectedProductWarehouse(initialValues?.product_warehouse_data || null);
|
|
||||||
}, [initialValues?.product_warehouse_data]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -422,22 +353,22 @@ const SalesOrderProductForm = ({
|
|||||||
errorMessage={formik.errors.vehicle_number}
|
errorMessage={formik.errors.vehicle_number}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Gudang Fisik */}
|
{/* Gudang */}
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
required
|
required
|
||||||
label='Gudang Fisik'
|
label='Gudang'
|
||||||
options={warehouseOptions}
|
options={kandangSourceOptions}
|
||||||
isLoading={isLoadingWarehouseOptions}
|
isLoading={isLoadingKandangSourceOptions}
|
||||||
value={formik.values.warehouse}
|
value={formik.values.kandang}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
isClearable
|
isClearable
|
||||||
onInputChange={setWarehouseSearchValue}
|
onInputChange={setKandangInputValue}
|
||||||
onMenuScrollToBottom={loadMoreWarehouses}
|
onMenuScrollToBottom={loadMoreKandang}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.warehouse_id && Boolean(formik.errors.warehouse_id)
|
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.warehouse_id}
|
errorMessage={formik.errors.kandang_id}
|
||||||
placeholder='Pilih Gudang Fisik'
|
placeholder='Pilih Gudang'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Kategori */}
|
{/* Kategori */}
|
||||||
@@ -448,9 +379,8 @@ const SalesOrderProductForm = ({
|
|||||||
value={formik.values.marketing_type}
|
value={formik.values.marketing_type}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldValue('marketing_type', val);
|
formik.setFieldValue('marketing_type', val);
|
||||||
productWarehouseChangeHandler(null);
|
warehouseChangeHandler(null);
|
||||||
formik.setFieldValue('product_warehouse', null);
|
formik.setFieldValue('product_warehouse', null);
|
||||||
formik.setFieldValue('product_warehouse_data', null);
|
|
||||||
formik.setFieldValue('product_warehouse_id', null);
|
formik.setFieldValue('product_warehouse_id', null);
|
||||||
formik.setFieldValue('convertion_unit', null);
|
formik.setFieldValue('convertion_unit', null);
|
||||||
formik.setFieldValue('weight_per_convertion', null);
|
formik.setFieldValue('weight_per_convertion', null);
|
||||||
@@ -467,18 +397,18 @@ const SalesOrderProductForm = ({
|
|||||||
options={productOptionsFiltered}
|
options={productOptionsFiltered}
|
||||||
isLoading={isLoadingWarehouseSourceOptions}
|
isLoading={isLoadingWarehouseSourceOptions}
|
||||||
value={formik.values.product_warehouse}
|
value={formik.values.product_warehouse}
|
||||||
onChange={productWarehouseChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
onMenuScrollToBottom={loadMoreWarehouse}
|
onMenuScrollToBottom={loadMoreWarehouse}
|
||||||
isClearable
|
isClearable
|
||||||
placeholder={
|
placeholder={
|
||||||
formik.values.warehouse_id
|
formik.values.kandang_id
|
||||||
? productOptionsFiltered.length == 0
|
? productOptionsFiltered.length == 0
|
||||||
? 'Tidak ada produk yang tersedia'
|
? 'Tidak ada produk yang tersedia'
|
||||||
: 'Pilih produk'
|
: 'Pilih produk'
|
||||||
: 'Pilih Gudang Fisik Terlebih Dahulu'
|
: 'Pilih Kandang Terlebih Dahulu'
|
||||||
}
|
}
|
||||||
isDisabled={!formik.values.warehouse_id}
|
isDisabled={!formik.values.kandang_id}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.product_warehouse_id &&
|
formik.touched.product_warehouse_id &&
|
||||||
Boolean(formik.errors.product_warehouse_id)
|
Boolean(formik.errors.product_warehouse_id)
|
||||||
@@ -546,7 +476,7 @@ const SalesOrderProductForm = ({
|
|||||||
<input
|
<input
|
||||||
type='radio'
|
type='radio'
|
||||||
checked={
|
checked={
|
||||||
formik.values.convertion_unit?.value.toLowerCase() ===
|
formik.values.convertion_unit?.value ===
|
||||||
option.value
|
option.value
|
||||||
}
|
}
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
@@ -569,9 +499,7 @@ const SalesOrderProductForm = ({
|
|||||||
} per ${formik.values.convertion_unit?.value}`}
|
} per ${formik.values.convertion_unit?.value}`}
|
||||||
value={formik.values.weight_per_convertion ?? ''}
|
value={formik.values.weight_per_convertion ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = Number(e.target.value)
|
const value = Number(e.target.value);
|
||||||
? Number(e.target.value)
|
|
||||||
: '';
|
|
||||||
handleFieldChange('weight_per_convertion', value, () =>
|
handleFieldChange('weight_per_convertion', value, () =>
|
||||||
setCurrentInput(e.target.name)
|
setCurrentInput(e.target.name)
|
||||||
);
|
);
|
||||||
@@ -625,7 +553,7 @@ const SalesOrderProductForm = ({
|
|||||||
placeholder='Masukan Total Peti'
|
placeholder='Masukan Total Peti'
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-base-content/50'>Peti</span>
|
<span className='text-sm text-base-content/50'>Kg</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
||||||
@@ -675,9 +603,6 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.total_weight}
|
errorMessage={formik.errors.total_weight}
|
||||||
placeholder='Masukan Total Bobot'
|
placeholder='Masukan Total Bobot'
|
||||||
disabled={
|
|
||||||
formik.values.convertion_unit?.value.toLowerCase() === 'peti'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -745,34 +670,12 @@ const SalesOrderProductForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Harga Satuan per Uom Produk Warehouse */}
|
{/* Harga per butir untuk TELUR + QTY */}
|
||||||
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
|
||||||
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label={`Harga / ${formik.values.convertion_unit?.label.toLowerCase() !== 'qty' ? 'Kg' : 'Butir'} (Rp)`}
|
|
||||||
name='unit_price'
|
|
||||||
value={formik.values.unit_price}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = Number(e.target.value);
|
|
||||||
handleFieldChange('unit_price', value, () =>
|
|
||||||
setCurrentInput(e.target.name)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
isError={
|
|
||||||
formik.touched.unit_price && Boolean(formik.errors.unit_price)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.unit_price}
|
|
||||||
placeholder='Masukan Harga Satuan...'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Harga per kg untuk TELUR + QTY */}
|
|
||||||
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
||||||
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga / Kg (Rp)'
|
label='Harga / Butir (Rp)'
|
||||||
name='price_per_qty'
|
name='price_per_qty'
|
||||||
value={formik.values.price_per_qty ?? undefined}
|
value={formik.values.price_per_qty ?? undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -786,7 +689,29 @@ const SalesOrderProductForm = ({
|
|||||||
Boolean(formik.errors.price_per_qty)
|
Boolean(formik.errors.price_per_qty)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.price_per_qty}
|
errorMessage={formik.errors.price_per_qty}
|
||||||
placeholder='Masukan Harga per Kg'
|
placeholder='Masukan Harga per Butir'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Harga Satuan per Uom Produk Warehouse */}
|
||||||
|
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
||||||
|
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label={`Harga / ${formik.values.convertion_unit?.label !== 'qty' ? 'Kg' : (selectedProductWarehouse?.product?.uom?.name ?? 'Produk')} (Rp)`}
|
||||||
|
name='unit_price'
|
||||||
|
value={formik.values.unit_price}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
handleFieldChange('unit_price', value, () =>
|
||||||
|
setCurrentInput(e.target.name)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isError={
|
||||||
|
formik.touched.unit_price && Boolean(formik.errors.unit_price)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.unit_price}
|
||||||
|
placeholder='Masukan Harga Satuan'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { Icon } from '@iconify/react';
|
|||||||
import { useRef, useMemo } from 'react';
|
import { useRef, useMemo } from 'react';
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
||||||
import { Marketing } from '@/types/api/marketing/marketing';
|
import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { DeliveryProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema';
|
|
||||||
|
|
||||||
type DeliveryOrderProductTableProps = {
|
type DeliveryOrderProductTableProps = {
|
||||||
data: DeliveryOrderProductFormValues[];
|
data: DeliveryOrderProductFormValues[];
|
||||||
@@ -56,17 +55,14 @@ const DeliveryOrderProductTable = ({
|
|||||||
|
|
||||||
const deliveryItems = useMemo(() => {
|
const deliveryItems = useMemo(() => {
|
||||||
if (!hasDeliveryOrder) return [];
|
if (!hasDeliveryOrder) return [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
marketing?.delivery_order?.flatMap((doItem) =>
|
marketing?.delivery_order?.flatMap((doItem) =>
|
||||||
DeliveryProductToFieldValues(marketing?.sales_order, doItem).map(
|
doItem.deliveries.map((delivery) => ({
|
||||||
(delivery) => ({
|
...delivery,
|
||||||
...delivery,
|
do_number: doItem.do_number,
|
||||||
do_number: doItem.do_number,
|
delivery_date: doItem.delivery_date,
|
||||||
delivery_date: doItem.delivery_date,
|
warehouse: doItem.warehouse,
|
||||||
warehouse: doItem.warehouse,
|
}))
|
||||||
})
|
|
||||||
)
|
|
||||||
) ?? []
|
) ?? []
|
||||||
);
|
);
|
||||||
}, [marketing?.delivery_order, hasDeliveryOrder]);
|
}, [marketing?.delivery_order, hasDeliveryOrder]);
|
||||||
@@ -85,7 +81,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||||
<div>Value</div>
|
<div>Value</div>
|
||||||
{/* {formType !== 'success' &&
|
{formType !== 'success' &&
|
||||||
(formType === 'add_delivery' ||
|
(formType === 'add_delivery' ||
|
||||||
formType === 'edit_delivery' ||
|
formType === 'edit_delivery' ||
|
||||||
formType === 'detail') && (
|
formType === 'detail') && (
|
||||||
@@ -102,16 +98,15 @@ const DeliveryOrderProductTable = ({
|
|||||||
<Icon icon='heroicons:pencil' width={20} height={20} />
|
<Icon icon='heroicons:pencil' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
|
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{doItem?.warehouse?.name ||
|
{doItem?.warehouse?.name ||
|
||||||
item.marketing_product?.warehouse?.label ||
|
|
||||||
item.marketing_product?.product_warehouse_data?.warehouse?.name}
|
item.marketing_product?.product_warehouse_data?.warehouse?.name}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -124,7 +119,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>
|
||||||
@@ -141,15 +136,12 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatNumber(Number(item.total_weight))} Kg
|
{formatNumber(Number(item.total_weight))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||||
Total Harga Satuan
|
|
||||||
{item.convertion_unit?.label.toLowerCase() === 'peti' && ' (Kg)'}
|
|
||||||
</td>
|
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||||
</td>
|
</td>
|
||||||
@@ -219,7 +211,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderDeliveryOrderContent = (
|
const renderDeliveryOrderContent = (
|
||||||
item: DeliveryOrderProductFormValues & {
|
item: BaseDelivery & {
|
||||||
do_number: string;
|
do_number: string;
|
||||||
delivery_date: string;
|
delivery_date: string;
|
||||||
warehouse: Warehouse;
|
warehouse: Warehouse;
|
||||||
@@ -238,43 +230,25 @@ const DeliveryOrderProductTable = ({
|
|||||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||||
<div>Value</div>
|
<div>Value</div>
|
||||||
{formType !== 'success' &&
|
|
||||||
(formType === 'add_delivery' ||
|
|
||||||
formType === 'edit_delivery' ||
|
|
||||||
formType === 'detail') && (
|
|
||||||
<div className='flex flex-row gap-1.5 items-center'>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
onEditRef.current(item.id as number, item);
|
|
||||||
}}
|
|
||||||
className='p-0 hover:text-base-content'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:pencil' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
|
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||||
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
|
<td className='text-sm px-4 py-3'>{item.warehouse?.name}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Produk</td>
|
<td className='text-sm px-4 py-3'>Produk</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.marketing_product?.product_warehouse_data?.product.name}
|
{item.product_warehouse?.product?.name}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -297,13 +271,13 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(Number(item.unit_price))}
|
{formatCurrency(item.unit_price)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(Number(item.total_price))}
|
{formatCurrency(item.total_price)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
@@ -359,9 +333,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||||
{hasDeliveryOrder
|
{hasDeliveryOrder
|
||||||
? deliveryItems.map((item, index) => (
|
? deliveryItems.map((item, index) => (
|
||||||
<div
|
<div key={`do-table-${item.product_warehouse?.id}-${index}`}>
|
||||||
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
|
|
||||||
>
|
|
||||||
{formType === 'success' ? (
|
{formType === 'success' ? (
|
||||||
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
||||||
<table
|
<table
|
||||||
@@ -377,11 +349,8 @@ const DeliveryOrderProductTable = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Card
|
<Card
|
||||||
key={`do-table-${item.marketing_product?.product_warehouse?.value}-${index}`}
|
key={`do-table-${item.product_warehouse?.id}-${index}`}
|
||||||
title={
|
title={item.product_warehouse?.product?.name || 'Produk'}
|
||||||
item.marketing_product?.product_warehouse_data?.product
|
|
||||||
.name || 'Produk'
|
|
||||||
}
|
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
defaultCollapsed={false}
|
defaultCollapsed={false}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
|
|||||||
@@ -73,10 +73,8 @@ const SalesOrderProductTable = ({
|
|||||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Gudang Fisik</td>
|
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
|
||||||
{item.warehouse?.label ?? item.kandang?.label}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Kategori</td>
|
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||||
@@ -137,22 +135,8 @@ const SalesOrderProductTable = ({
|
|||||||
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
|
||||||
<tr>
|
|
||||||
<td className='text-sm px-4 py-3'>Harga Satuan Per Peti</td>
|
|
||||||
<td className='text-sm px-4 py-3'>
|
|
||||||
{formatCurrency(
|
|
||||||
parseFloat(item.unit_price as string) *
|
|
||||||
parseFloat(String(item.weight_per_convertion))
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>Harga Satuan</td>
|
||||||
Harga Satuan
|
|
||||||
{item.convertion_unit?.value.toLowerCase() === 'peti' && ' (Kg)'}
|
|
||||||
</td>
|
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
|
|||||||
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
interface DeliveryOrderExportProps {
|
interface DeliveryOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -20,9 +19,6 @@ const DeliveryOrderExport = ({
|
|||||||
}: DeliveryOrderExportProps) => {
|
}: DeliveryOrderExportProps) => {
|
||||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||||
const salesData = data;
|
const salesData = data;
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const action = searchParams.get('action');
|
|
||||||
const id = searchParams.get('id');
|
|
||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
@@ -53,7 +49,6 @@ const DeliveryOrderExport = ({
|
|||||||
toast.error('Failed to generate PDF. Please try again.');
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
window.location.href = `/marketing?action=${action}&id=${id}`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +87,7 @@ const PDFDocument = ({
|
|||||||
return (
|
return (
|
||||||
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
|
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
|
||||||
);
|
);
|
||||||
}, []);
|
}, [deliveryOrder.deliveries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
@@ -101,8 +96,8 @@ const PDFDocument = ({
|
|||||||
<View style={pdfStyles.header}>
|
<View style={pdfStyles.header}>
|
||||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||||
<Text style={pdfStyles.address}>
|
<Text style={pdfStyles.address}>
|
||||||
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
|
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||||
Bandung Barat, Jawa Barat 40514
|
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||||
</Text>
|
</Text>
|
||||||
<View style={pdfStyles.divider} />
|
<View style={pdfStyles.divider} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
|
|||||||
import { formatDate, formatNumber } from '@/lib/helper';
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
interface SalesOrderExportProps {
|
interface SalesOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -16,9 +15,6 @@ interface SalesOrderExportProps {
|
|||||||
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||||
const salesData = data;
|
const salesData = data;
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const action = searchParams.get('action');
|
|
||||||
const id = searchParams.get('id');
|
|
||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
@@ -47,7 +43,6 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
|||||||
toast.error('Failed to generate PDF. Please try again.');
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
window.location.href = `/marketing?action=${action}&id=${id}`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,8 +82,8 @@ const PDFDocument = ({ data }: { data: Marketing }) => {
|
|||||||
<View style={pdfStyles.header}>
|
<View style={pdfStyles.header}>
|
||||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||||
<Text style={pdfStyles.address}>
|
<Text style={pdfStyles.address}>
|
||||||
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
|
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||||
Bandung Barat, Jawa Barat 40514
|
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||||
</Text>
|
</Text>
|
||||||
<View style={pdfStyles.divider} />
|
<View style={pdfStyles.divider} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
@@ -284,10 +314,6 @@ const KandangsTable = () => {
|
|||||||
accessorFn: (row) => row.pic?.name ?? '-',
|
accessorFn: (row) => row.pic?.name ?? '-',
|
||||||
header: 'PIC',
|
header: 'PIC',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorFn: (row) => row.kandang_group?.name ?? '-',
|
|
||||||
header: 'Kandang Group',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Kandang, unknown>) => {
|
cell: (props: CellContext<Kandang, unknown>) => {
|
||||||
@@ -445,13 +471,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 +490,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 +506,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,4 +1,3 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
type KandangFormSchemaType = {
|
type KandangFormSchemaType = {
|
||||||
@@ -20,7 +19,6 @@ type KandangFormSchemaType = {
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
| null;
|
| null;
|
||||||
group?: OptionType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||||
@@ -44,11 +42,6 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
|||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
|
|
||||||
group: Yup.object({
|
|
||||||
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
|
|
||||||
label: Yup.string().required('Kandang Grup wajib diisi!'),
|
|
||||||
}).required('Kandang Grup wajib diisi!'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateKandangFormSchema = KandangFormSchema;
|
export const UpdateKandangFormSchema = KandangFormSchema;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { getIn, useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -34,8 +34,6 @@ import NumberInput from '@/components/input/NumberInput';
|
|||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { User } from '@/types/api/api-general';
|
import { User } from '@/types/api/api-general';
|
||||||
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
|
||||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
|
||||||
|
|
||||||
interface KandangFormProps {
|
interface KandangFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -98,12 +96,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
label: initialValues.pic.name,
|
label: initialValues.pic.name,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
group: initialValues?.kandang_group
|
|
||||||
? {
|
|
||||||
value: initialValues.kandang_group.id,
|
|
||||||
label: initialValues.kandang_group.name,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
@@ -119,7 +111,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
location_id: values.locationId!,
|
location_id: values.locationId!,
|
||||||
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
||||||
pic_id: values.picId!,
|
pic_id: values.picId!,
|
||||||
group_id: values.group?.value as number,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -171,23 +162,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
formik.setFieldValue('picId', (val as OptionType)?.value);
|
formik.setFieldValue('picId', (val as OptionType)?.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Kandang Group
|
|
||||||
const {
|
|
||||||
setInputValue: setKandangGroupSelectInputValue,
|
|
||||||
options: kandangGroupOptions,
|
|
||||||
isLoadingOptions: isLoadingKandangGroupOptions,
|
|
||||||
loadMore: loadMoreKandangGroups,
|
|
||||||
} = useSelect<DailyChecklistKandang>(
|
|
||||||
DailyChecklistKandangApi.basePath,
|
|
||||||
'id',
|
|
||||||
'name'
|
|
||||||
);
|
|
||||||
|
|
||||||
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
formik.setFieldTouched('group', true);
|
|
||||||
formik.setFieldValue('group', val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteKandangClickHandler = () => {
|
const deleteKandangClickHandler = () => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
};
|
};
|
||||||
@@ -295,24 +269,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
required
|
|
||||||
label='Kandang Group'
|
|
||||||
value={formik.values.group ?? undefined}
|
|
||||||
onChange={kandangGroupChangeHandler}
|
|
||||||
options={kandangGroupOptions}
|
|
||||||
onInputChange={setKandangGroupSelectInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreKandangGroups}
|
|
||||||
isLoading={isLoadingKandangGroupOptions}
|
|
||||||
isError={formik.touched.group && Boolean(formik.errors.group)}
|
|
||||||
errorMessage={
|
|
||||||
getIn(formik.errors.group, 'value') ??
|
|
||||||
(formik.errors.group as string)
|
|
||||||
}
|
|
||||||
isDisabled={type === 'detail'}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -154,17 +154,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
sku: values.sku,
|
sku: values.sku,
|
||||||
uom_id: values.uom_id,
|
uom_id: values.uom_id,
|
||||||
product_category_id: values.product_category_id,
|
product_category_id: values.product_category_id,
|
||||||
product_price: parseFloat(values.product_price.toString()) || 0,
|
product_price: parseInt(values.product_price.toString()) || 0,
|
||||||
selling_price: values.selling_price
|
selling_price: values.selling_price
|
||||||
? parseFloat(values.selling_price.toString()) || 0
|
? parseInt(values.selling_price.toString()) || 0
|
||||||
: undefined,
|
: undefined,
|
||||||
tax: values.tax ? parseFloat(values.tax.toString()) || 0 : undefined,
|
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
|
||||||
expiry_period: values.expiry_period
|
expiry_period: values.expiry_period
|
||||||
? parseFloat(values.expiry_period.toString()) || 0
|
? parseInt(values.expiry_period.toString()) || 0
|
||||||
: undefined,
|
: undefined,
|
||||||
suppliers: values.suppliers.map((s) => ({
|
suppliers: values.suppliers.map((s) => ({
|
||||||
supplier_id: s.supplier?.value as number,
|
supplier_id: s.supplier?.value as number,
|
||||||
price: parseFloat(s.price.toString()) || 0,
|
price: parseInt(s.price.toString()) || 0,
|
||||||
})),
|
})),
|
||||||
flag: values.flag,
|
flag: values.flag,
|
||||||
sub_flags: values.sub_flags,
|
sub_flags: values.sub_flags,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ const ChickinFormKandang = ({
|
|||||||
|
|
||||||
const afterSubmitFormChickin = () => {
|
const afterSubmitFormChickin = () => {
|
||||||
setOpenChickin(true);
|
setOpenChickin(true);
|
||||||
afterSubmit && afterSubmit();
|
if (afterSubmit) {
|
||||||
|
afterSubmit();
|
||||||
|
}
|
||||||
refreshApprovals();
|
refreshApprovals();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ChickinLogsView = ({
|
|||||||
rawDataApprovals: BaseApproval[];
|
rawDataApprovals: BaseApproval[];
|
||||||
}) => {
|
}) => {
|
||||||
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
|
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
|
||||||
const { openChickinApproveModal, openChickinDeleteModal } = useChickinStore();
|
const { openChickinApproveModal } = useChickinStore();
|
||||||
|
|
||||||
const handleClickApprove = () => {
|
const handleClickApprove = () => {
|
||||||
openChickinApproveModal(initialValues, async (notes?: string) => {
|
openChickinApproveModal(initialValues, async (notes?: string) => {
|
||||||
@@ -40,21 +40,8 @@ const ChickinLogsView = ({
|
|||||||
toast.error(approveChickinRes?.message as string);
|
toast.error(approveChickinRes?.message as string);
|
||||||
setChickinErrorMessage(approveChickinRes?.message as string);
|
setChickinErrorMessage(approveChickinRes?.message as string);
|
||||||
}
|
}
|
||||||
afterSubmit && afterSubmit();
|
if (afterSubmit) {
|
||||||
});
|
afterSubmit();
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteChickin = (chickinId: number) => {
|
|
||||||
openChickinDeleteModal(chickinId, async () => {
|
|
||||||
const deleteRes = await ChickinApi.delete(chickinId);
|
|
||||||
|
|
||||||
if (isResponseSuccess(deleteRes)) {
|
|
||||||
toast.success(deleteRes?.message || 'Chickin berhasil dihapus');
|
|
||||||
afterSubmit && afterSubmit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResponseError(deleteRes)) {
|
|
||||||
toast.error(deleteRes?.message || 'Gagal menghapus chickin');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -101,30 +88,14 @@ const ChickinLogsView = ({
|
|||||||
<div className='text-lg font-semibold'>
|
<div className='text-lg font-semibold'>
|
||||||
Chick In #{index + 1} - {latestApproval?.step_number}
|
Chick In #{index + 1} - {latestApproval?.step_number}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row gap-2 items-center'>
|
<PillBadge
|
||||||
<PillBadge
|
content={
|
||||||
content={
|
isApproved ? 'Disetujui' : isPending ? 'Pending' : '-'
|
||||||
isApproved ? 'Disetujui' : isPending ? 'Pending' : '-'
|
}
|
||||||
}
|
color={
|
||||||
color={
|
isApproved ? 'green' : isPending ? 'yellow' : 'gray'
|
||||||
isApproved ? 'green' : isPending ? 'yellow' : 'gray'
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
{isApproved && (
|
|
||||||
<Button
|
|
||||||
color='error'
|
|
||||||
className='w-fit text-sm text-base-100 rounded-lg shadow-sm btn-xs!'
|
|
||||||
onClick={() => handleDeleteChickin(chickin.id)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:trash-solid'
|
|
||||||
width={10}
|
|
||||||
height={10}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tanggal Chick In */}
|
{/* Tanggal Chick In */}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -58,7 +59,8 @@ const RowOptionsMenu = ({
|
|||||||
detailClickHandler: (id: number) => void;
|
detailClickHandler: (id: number) => void;
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const showEditButton = props.row.original.approval?.step_number !== 2;
|
// TODO: change this to real condition
|
||||||
|
const showEditButton = true;
|
||||||
|
|
||||||
const showDeleteButton = showEditButton;
|
const showDeleteButton = showEditButton;
|
||||||
|
|
||||||
@@ -147,6 +149,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 +175,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 +186,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 =====
|
||||||
@@ -204,25 +200,20 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
const confirmModal = useModal();
|
const confirmModal = useModal();
|
||||||
const successModal = useModal();
|
const successModal = useModal();
|
||||||
const chickinApproveModal = useModal();
|
const chickinApproveModal = useModal();
|
||||||
const chickinDeleteModal = useModal();
|
|
||||||
const closingModal = useModal();
|
const closingModal = useModal();
|
||||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||||
'APPROVED'
|
'APPROVED'
|
||||||
);
|
);
|
||||||
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,
|
||||||
chickinApproveCallback,
|
chickinApproveCallback,
|
||||||
closeChickinApproveModal,
|
closeChickinApproveModal,
|
||||||
setChickinApproveLoading,
|
setChickinApproveLoading,
|
||||||
isChickinDeleteModalOpen,
|
|
||||||
isChickinDeleteLoading,
|
|
||||||
chickinDeleteCallback,
|
|
||||||
closeChickinDeleteModal,
|
|
||||||
setChickinDeleteLoading,
|
|
||||||
} = useChickinStore();
|
} = useChickinStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -261,18 +252,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 +261,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 +302,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 +345,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 +420,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'
|
||||||
@@ -503,14 +478,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
}
|
}
|
||||||
}, [isChickinApproveModalOpen, chickinApproveModal]);
|
}, [isChickinApproveModalOpen, chickinApproveModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isChickinDeleteModalOpen) {
|
|
||||||
chickinDeleteModal.openModal();
|
|
||||||
} else {
|
|
||||||
chickinDeleteModal.closeModal();
|
|
||||||
}
|
|
||||||
}, [isChickinDeleteModalOpen, chickinDeleteModal]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isClosingModalOpen) {
|
if (isClosingModalOpen) {
|
||||||
closingModal.openModal();
|
closingModal.openModal();
|
||||||
@@ -574,7 +541,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 +763,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 +959,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>
|
||||||
|
|
||||||
@@ -1196,38 +1208,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Chickin Delete Modal */}
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={chickinDeleteModal.ref}
|
|
||||||
type='error'
|
|
||||||
text='Apakah anda yakin ingin menghapus data chick in ini?'
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
onClick: () => {
|
|
||||||
closeChickinDeleteModal();
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
className={{
|
|
||||||
modal: 'z-9999',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'error',
|
|
||||||
isLoading: isChickinDeleteLoading,
|
|
||||||
onClick: async () => {
|
|
||||||
if (chickinDeleteCallback) {
|
|
||||||
setChickinDeleteLoading(true);
|
|
||||||
try {
|
|
||||||
await chickinDeleteCallback();
|
|
||||||
} finally {
|
|
||||||
setChickinDeleteLoading(false);
|
|
||||||
closeChickinDeleteModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Filter Modal */}
|
{/* Filter Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
@@ -1324,14 +1304,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>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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,96 +29,63 @@ 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;
|
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
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!')
|
.min(1, 'Jumlah penggunaan tidak boleh 0!')
|
||||||
.typeError('Jumlah penggunaan harus berupa angka!'),
|
.typeError('Jumlah penggunaan harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
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()
|
|
||||||
.optional()
|
|
||||||
.typeError('Gudang sumber harus berupa angka!'),
|
|
||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Jumlah depletions harus berupa angka!'),
|
.typeError('Jumlah depletions harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
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 +243,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 +258,12 @@ 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,
|
|
||||||
qty: depletion.qty,
|
qty: depletion.qty,
|
||||||
})
|
})
|
||||||
) ?? [
|
) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: undefined,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -318,15 +275,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: '',
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
|||||||
export type RecordingRestriction = {
|
|
||||||
canEditStock: boolean;
|
|
||||||
canEditDepletion: boolean;
|
|
||||||
canEditEgg: boolean;
|
|
||||||
isLocked: boolean;
|
|
||||||
lockReason?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRecordingRestriction = (
|
|
||||||
isLaying: boolean,
|
|
||||||
isTransition: boolean,
|
|
||||||
currentIsLaying?: boolean
|
|
||||||
): RecordingRestriction => {
|
|
||||||
if (isTransition && !isLaying) {
|
|
||||||
const isLayingKandangInTransition = currentIsLaying === true;
|
|
||||||
|
|
||||||
if (isLayingKandangInTransition) {
|
|
||||||
return {
|
|
||||||
canEditStock: false,
|
|
||||||
canEditDepletion: true,
|
|
||||||
canEditEgg: true,
|
|
||||||
isLocked: false,
|
|
||||||
lockReason: undefined,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
canEditStock: true,
|
|
||||||
canEditDepletion: false,
|
|
||||||
canEditEgg: false,
|
|
||||||
isLocked: false,
|
|
||||||
lockReason: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLaying && !isTransition && currentIsLaying) {
|
|
||||||
return {
|
|
||||||
canEditStock: false,
|
|
||||||
canEditDepletion: false,
|
|
||||||
canEditEgg: false,
|
|
||||||
isLocked: true,
|
|
||||||
lockReason:
|
|
||||||
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLaying && !isTransition) {
|
|
||||||
return {
|
|
||||||
canEditStock: true,
|
|
||||||
canEditDepletion: true,
|
|
||||||
canEditEgg: false,
|
|
||||||
isLocked: false,
|
|
||||||
lockReason: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (isLaying && !isTransition) {
|
|
||||||
return {
|
|
||||||
canEditStock: true,
|
|
||||||
canEditDepletion: true,
|
|
||||||
canEditEgg: true,
|
|
||||||
isLocked: false,
|
|
||||||
lockReason: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
canEditStock: false,
|
|
||||||
canEditDepletion: false,
|
|
||||||
canEditEgg: false,
|
|
||||||
isLocked: true,
|
|
||||||
lockReason: 'Kondisi transisi tidak valid',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+8
-18
@@ -50,18 +50,12 @@ const TransferToLayingConfirmationModalTable = ({
|
|||||||
transferToLayingForm?: TransferToLayingFormValues;
|
transferToLayingForm?: TransferToLayingFormValues;
|
||||||
transferToLayingId?: number;
|
transferToLayingId?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const isValidId =
|
|
||||||
transferToLayingId !== undefined &&
|
|
||||||
transferToLayingId !== null &&
|
|
||||||
!isNaN(transferToLayingId) &&
|
|
||||||
transferToLayingId > 0;
|
|
||||||
|
|
||||||
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
|
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
|
||||||
useSWR(
|
useSWR(
|
||||||
isValidId
|
transferToLayingId
|
||||||
? ['detail-transfer-to-laying', String(transferToLayingId)]
|
? ['detail-transfer-to-laying', String(transferToLayingId)]
|
||||||
: undefined,
|
: undefined,
|
||||||
([, id]) => TransferToLayingApi.getSingle(Number(id))
|
([id]) => TransferToLayingApi.getSingle(Number(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] =
|
const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] =
|
||||||
@@ -279,16 +273,12 @@ const TransferToLayingConfirmationModal = ({
|
|||||||
|
|
||||||
{transferToLayingIds &&
|
{transferToLayingIds &&
|
||||||
!transferToLayingForm &&
|
!transferToLayingForm &&
|
||||||
transferToLayingIds
|
transferToLayingIds.map((transferToLayingId, idx) => (
|
||||||
.filter(
|
<TransferToLayingConfirmationModalTable
|
||||||
(id) => id !== undefined && id !== null && !isNaN(id) && id > 0
|
key={idx}
|
||||||
)
|
transferToLayingId={transferToLayingId}
|
||||||
.map((transferToLayingId, idx) => (
|
/>
|
||||||
<TransferToLayingConfirmationModalTable
|
))}
|
||||||
key={idx}
|
|
||||||
transferToLayingId={transferToLayingId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{withNote && (
|
{withNote && (
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|||||||
+24
-36
@@ -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) {
|
||||||
@@ -86,7 +82,7 @@ const TransferToLayingDetailModal = () => {
|
|||||||
if (modalAction === 'detail') {
|
if (modalAction === 'detail') {
|
||||||
detailModal.openModal();
|
detailModal.openModal();
|
||||||
}
|
}
|
||||||
}, [modalAction]);
|
}, [modalAction, detailModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -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,16 +223,12 @@ 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<
|
||||||
ProjectFlock | undefined
|
ProjectFlock | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
const [maxSourceQuantity, setMaxSourceQuantity] = useState<number>(0);
|
|
||||||
|
|
||||||
const selectedFlockDestinationRawData = isResponseSuccess(
|
const selectedFlockDestinationRawData = isResponseSuccess(
|
||||||
flockDestinationRawData
|
flockDestinationRawData
|
||||||
)
|
)
|
||||||
@@ -357,14 +353,19 @@ const TransferToLayingFormModal = () => {
|
|||||||
return { available: countAvailable, unavailable: countUnavailable };
|
return { available: countAvailable, unavailable: countUnavailable };
|
||||||
}, [mappedFlockDestinationKandangsMaxTargetQty]);
|
}, [mappedFlockDestinationKandangsMaxTargetQty]);
|
||||||
|
|
||||||
|
const totalEnteredChickenForTransfer =
|
||||||
|
formik.values.flockSourceKandangs.reduce(
|
||||||
|
(acc, item) => acc + Number(item.quantity),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce(
|
const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce(
|
||||||
(acc, item) => acc + Number(item.quantity),
|
(acc, item) => acc + Number(item.quantity),
|
||||||
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 isNextButtonDisabled = useMemo(() => {
|
const isNextButtonDisabled = useMemo(() => {
|
||||||
if (step === 1) {
|
if (step === 1) {
|
||||||
@@ -396,7 +397,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
formik.setFieldValue('maxTotalQuantity', '');
|
formik.setFieldValue('maxTotalQuantity', '');
|
||||||
formik.setFieldValue('reason', '');
|
formik.setFieldValue('reason', '');
|
||||||
formik.setFieldTouched('reason', false);
|
formik.setFieldTouched('reason', false);
|
||||||
setMaxSourceQuantity(0);
|
|
||||||
|
|
||||||
setStep(2);
|
setStep(2);
|
||||||
};
|
};
|
||||||
@@ -404,7 +404,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('flockSource', val);
|
formik.setFieldValue('flockSource', val);
|
||||||
formik.setFieldValue('flockSourceKandangs', []);
|
formik.setFieldValue('flockSourceKandangs', []);
|
||||||
setMaxSourceQuantity(0);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const flockDestinationChangeHandler = (
|
const flockDestinationChangeHandler = (
|
||||||
@@ -457,39 +456,19 @@ 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);
|
||||||
formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);
|
formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);
|
||||||
}, [totalTransferedChicken, formik.values.flockDestinationKandangs]);
|
}, [totalTransferedChicken, formik.values.flockDestinationKandangs]);
|
||||||
|
|
||||||
// Auto-fill source kandang quantity from total destination quantity
|
|
||||||
useEffect(() => {
|
|
||||||
if (formik.values.flockSourceKandangs.length > 0) {
|
|
||||||
formik.setFieldValue(
|
|
||||||
'flockSourceKandangs.0.quantity',
|
|
||||||
totalTransferedChicken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [totalTransferedChicken]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
formik.values.flockSourceKandangs.length > 0 &&
|
|
||||||
formik.values.flockSourceKandangs[0].maxQuantity &&
|
|
||||||
maxSourceQuantity === 0
|
|
||||||
) {
|
|
||||||
setMaxSourceQuantity(formik.values.flockSourceKandangs[0].maxQuantity);
|
|
||||||
}
|
|
||||||
}, [formik.values.flockSourceKandangs, maxSourceQuantity]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -604,9 +583,14 @@ const TransferToLayingFormModal = () => {
|
|||||||
k.kandang.value === item.project_flock_kandang_id
|
k.kandang.value === item.project_flock_kandang_id
|
||||||
);
|
);
|
||||||
|
|
||||||
const flockSourceKandangRadioChangeHandler = () => {
|
const flockSourceKandangCheckboxChangeHandler: FormEventHandler<
|
||||||
if (isAvailable) {
|
HTMLInputElement
|
||||||
|
> = (e) => {
|
||||||
|
const checked = (e.target as HTMLInputElement)
|
||||||
|
.checked;
|
||||||
|
if (checked) {
|
||||||
formik.setFieldValue('flockSourceKandangs', [
|
formik.setFieldValue('flockSourceKandangs', [
|
||||||
|
...formik.values.flockSourceKandangs,
|
||||||
{
|
{
|
||||||
kandang: {
|
kandang: {
|
||||||
value: item.project_flock_kandang_id,
|
value: item.project_flock_kandang_id,
|
||||||
@@ -616,7 +600,15 @@ const TransferToLayingFormModal = () => {
|
|||||||
maxQuantity: item.available_qty,
|
maxQuantity: item.available_qty,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setMaxSourceQuantity(item.available_qty);
|
} else {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'flockSourceKandangs',
|
||||||
|
formik.values.flockSourceKandangs.filter(
|
||||||
|
(k) =>
|
||||||
|
k.kandang.value !==
|
||||||
|
item.project_flock_kandang_id
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -626,28 +618,32 @@ const TransferToLayingFormModal = () => {
|
|||||||
className='w-full p-3 flex flex-row items-center justify-between'
|
className='w-full p-3 flex flex-row items-center justify-between'
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-3'>
|
<div className='flex flex-row items-center gap-3'>
|
||||||
<input
|
<CheckboxInput
|
||||||
id={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
name={`flockSourceKandang.${itemIdx}.value`}
|
||||||
type='radio'
|
|
||||||
name='flockSourceKandang'
|
|
||||||
value={item.project_flock_kandang_id}
|
value={item.project_flock_kandang_id}
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
onChange={flockSourceKandangRadioChangeHandler}
|
onChange={
|
||||||
|
flockSourceKandangCheckboxChangeHandler
|
||||||
|
}
|
||||||
|
size='md'
|
||||||
disabled={!isAvailable}
|
disabled={!isAvailable}
|
||||||
className={cn('radio radio-md radio-primary', {
|
classNames={{
|
||||||
'opacity-50 cursor-not-allowed': !isAvailable,
|
checkbox: cn({
|
||||||
})}
|
'bg-base-200 border border-base-content/10 opacity-100':
|
||||||
|
!isAvailable,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
htmlFor={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
htmlFor={`flockSourceKandang.${itemIdx}.value`}
|
||||||
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': !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 && (
|
||||||
@@ -884,7 +858,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
key={`flockSourceKandangs-${item.kandang.value}-${index}`}
|
key={`flockSourceKandangs-${item.kandang.value}-${index}`}
|
||||||
name={`flockSourceKandangs.${index}.quantity`}
|
name={`flockSourceKandangs.${index}.quantity`}
|
||||||
placeholder='Masukkan Kuantitas pada Kandang Tujuan'
|
placeholder='Masukkan Kuantitas'
|
||||||
value={item.quantity}
|
value={item.quantity}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
isError={isInvalid}
|
isError={isInvalid}
|
||||||
@@ -901,8 +875,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
<div className='w-px bg-base-content/10' />
|
<div className='w-px bg-base-content/10' />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
readOnly
|
|
||||||
disabled
|
|
||||||
className={{
|
className={{
|
||||||
inputPrefix:
|
inputPrefix:
|
||||||
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
|
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
|
||||||
@@ -928,6 +900,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 && (
|
||||||
@@ -1011,7 +1000,7 @@ const TransferToLayingFormModal = () => {
|
|||||||
isError={totalAvailableChickenForTransfer < 0}
|
isError={totalAvailableChickenForTransfer < 0}
|
||||||
errorMessage={
|
errorMessage={
|
||||||
totalAvailableChickenForTransfer < 0
|
totalAvailableChickenForTransfer < 0
|
||||||
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
|
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
disabled
|
disabled
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -47,11 +48,11 @@ const RowOptionsMenu = ({
|
|||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const showEditButton = props.row.original.approval.action !== 'APPROVED';
|
const showEditButton =
|
||||||
|
props.row.original.approval.action !== 'APPROVED' &&
|
||||||
|
props.row.original.approval.action !== 'REJECTED';
|
||||||
|
|
||||||
const showDeleteButton =
|
const showDeleteButton = showEditButton;
|
||||||
props.row.original.approval.action === 'APPROVED' ||
|
|
||||||
props.row.original.approval.step_name.toLowerCase() === 'pengajuan';
|
|
||||||
|
|
||||||
const popoverId = `transferToLaying#${props.row.original.id}`;
|
const popoverId = `transferToLaying#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
|
||||||
@@ -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 () => {
|
||||||
@@ -541,7 +463,7 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('filter_by', '');
|
updateFilter('filter_by', '');
|
||||||
updateFilter('sort_by', '');
|
updateFilter('sort_by', '');
|
||||||
}
|
}
|
||||||
}, [sorting]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user