mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-26 08:15:44 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 192a2be8b9 |
@@ -48,6 +48,3 @@ next-env.d.ts
|
|||||||
|
|
||||||
# rtk
|
# rtk
|
||||||
rtk.exe
|
rtk.exe
|
||||||
|
|
||||||
# local specs
|
|
||||||
/local-specs
|
|
||||||
+2
-21
@@ -30,10 +30,6 @@ default:
|
|||||||
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
||||||
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
||||||
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
||||||
- echo "NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV"
|
|
||||||
- echo "NEXT_PUBLIC_HELPDESK_URL=$NEXT_PUBLIC_HELPDESK_URL"
|
|
||||||
- echo "NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL=$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
|
||||||
- echo "NEXT_PUBLIC_S3_PUBLIC_BASE_URL=$NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
|
||||||
- echo "Building Next.js static export..."
|
- echo "Building Next.js static export..."
|
||||||
- npx next build
|
- npx next build
|
||||||
- |
|
- |
|
||||||
@@ -45,11 +41,7 @@ default:
|
|||||||
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||||
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
||||||
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
||||||
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL",
|
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL"
|
||||||
"NEXT_PUBLIC_APP_ENV": "$NEXT_PUBLIC_APP_ENV",
|
|
||||||
"NEXT_PUBLIC_HELPDESK_URL": "$NEXT_PUBLIC_HELPDESK_URL",
|
|
||||||
"NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL": "$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
|
||||||
"NEXT_PUBLIC_S3_PUBLIC_BASE_URL": "NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -150,10 +142,6 @@ build:dev:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
NEXT_PUBLIC_APP_ENV: 'development'
|
|
||||||
NEXT_PUBLIC_HELPDESK_URL: 'https://dev-helpdesk.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dev-dashboard-ho.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com'
|
|
||||||
|
|
||||||
deploy:dev:
|
deploy:dev:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -182,9 +170,6 @@ build:staging:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
NEXT_PUBLIC_APP_ENV: 'staging'
|
|
||||||
NEXT_PUBLIC_HELPDESK_URL: 'https://stg-helpdesk.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://stg-dashboard-ho.mbugroup.id/'
|
|
||||||
|
|
||||||
deploy:staging:
|
deploy:staging:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -200,7 +185,7 @@ deploy:staging:
|
|||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# ====== (Branch production) ======
|
# ====== STAGING (Branch production) ======
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
build:production:
|
build:production:
|
||||||
<<: *build_template
|
<<: *build_template
|
||||||
@@ -213,10 +198,6 @@ build:production:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
NEXT_PUBLIC_APP_ENV: 'production'
|
|
||||||
NEXT_PUBLIC_HELPDESK_URL: 'https://helpdesk.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dashboard-ho.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com/'
|
|
||||||
|
|
||||||
deploy:production:
|
deploy:production:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
git add .
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
# Project-local RTK filters — commit this file with your repo.
|
|
||||||
# Filters here override user-global and built-in filters.
|
|
||||||
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
|
||||||
schema_version = 1
|
|
||||||
|
|
||||||
# Example: suppress build noise from a custom tool
|
|
||||||
# [filters.my-tool]
|
|
||||||
# description = "Compact my-tool output"
|
|
||||||
# match_command = "^my-tool\\s+build"
|
|
||||||
# strip_ansi = true
|
|
||||||
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
|
|
||||||
# max_lines = 30
|
|
||||||
# on_empty = "my-tool: ok"
|
|
||||||
@@ -1,414 +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**
|
|
||||||
- Clear each filter with `updateFilter(fieldName, defaultValue, true)`
|
|
||||||
- Call `formik.resetForm({ values: { ...defaults } })`
|
|
||||||
- Close the modal at the end
|
|
||||||
- Attach to both button `onClick` and form `onReset` handler
|
|
||||||
|
|
||||||
**Optimization: Avoid useCallback for simple handlers**
|
|
||||||
|
|
||||||
- `useCallback` adds overhead and is only useful for complex logic or memoized child components
|
|
||||||
- Simple pass-through handlers don't need it:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// ✅ Good: Simple handler without useCallback
|
|
||||||
const handleFilterChange = (val) => setFieldValue('location', val);
|
|
||||||
|
|
||||||
// ❌ Avoid: Unnecessary useCallback overhead
|
|
||||||
const handleFilterChange = useCallback(
|
|
||||||
(val) => setFieldValue('location', val),
|
|
||||||
[setFieldValue]
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Best practice: Store OptionType objects directly, not IDs**
|
|
||||||
|
|
||||||
For select inputs, store the complete `OptionType` object in both formik state and tableFilterState. This eliminates the need for computed helper values (like searching options arrays to find the matching object).
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Type the useTableFilter with the filter state structure
|
|
||||||
const { state: tableFilterState, updateFilter, ... } = useTableFilter<{
|
|
||||||
search: string;
|
|
||||||
locationFilter?: OptionType<string>;
|
|
||||||
picFilter?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
|
||||||
search: '',
|
|
||||||
locationFilter: undefined,
|
|
||||||
picFilter: undefined
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
locationFilter: 'location_id',
|
|
||||||
picFilter: 'pic_id',
|
|
||||||
},
|
|
||||||
persist: true,
|
|
||||||
storeName: 'kandangs-table',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize formik with tableFilterState values (now typed OptionType objects)
|
|
||||||
const formik = useFormik<KandangFilterType>({
|
|
||||||
initialValues: {
|
|
||||||
location: tableFilterState.locationFilter,
|
|
||||||
pic: tableFilterState.picFilter,
|
|
||||||
},
|
|
||||||
...
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handlers store the complete OptionType, not just the ID
|
|
||||||
const handleFilterLocationChange = useCallback(
|
|
||||||
(val) => setFieldValue('location', val),
|
|
||||||
[setFieldValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use formik values directly in select inputs (no computed helpers needed)
|
|
||||||
<SelectInput
|
|
||||||
value={formik.values.location}
|
|
||||||
onChange={handleFilterLocationChange}
|
|
||||||
...
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
**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/`
|
|
||||||
- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.)
|
|
||||||
|
|
||||||
## Server-side sorting pattern
|
|
||||||
|
|
||||||
Data tables use TanStack Table's `SortingState` wired to `useTableFilter` so that sorting triggers a server re-fetch rather than client-side reordering.
|
|
||||||
|
|
||||||
**Four-part wiring:**
|
|
||||||
|
|
||||||
1. **Local sort state** — `const [sorting, setSorting] = useState<SortingState>([]);`
|
|
||||||
|
|
||||||
2. **`useTableFilter` config** — Add `sort_by` and `order_by` to `initial` and `paramMap`. The `paramMap` key is the internal name; the value is the query param name sent to the server (they can differ, e.g. `order_by` → `sort_order`):
|
|
||||||
|
|
||||||
```ts
|
|
||||||
initial: { sort_by: '', order_by: '' }
|
|
||||||
paramMap: { sort_by: 'sort_by', order_by: 'sort_order' }
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **`useEffect` sync** — Watches `sorting` and pushes changes into `useTableFilter`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
useEffect(() => {
|
|
||||||
if (sorting.length > 0) {
|
|
||||||
updateFilter('sort_by', sorting[0].id, true);
|
|
||||||
updateFilter('order_by', sorting[0].desc ? 'desc' : 'asc', true);
|
|
||||||
} else {
|
|
||||||
updateFilter('sort_by', '');
|
|
||||||
updateFilter('order_by', '');
|
|
||||||
}
|
|
||||||
}, [sorting]);
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **SWR key** — SWR uses `getTableFilterToQueryString()` as its key, so any filter change (including sort) automatically re-fetches with the new query params. TanStack Table's built-in client sorting is effectively disabled; the server does the sorting.
|
|
||||||
|
|
||||||
**Pass `sorting`, `setSorting`, and `manualSorting` to `<Table>`:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Table sorting={sorting} setSorting={handleSortingChange} manualSorting={true} ... />
|
|
||||||
```
|
|
||||||
|
|
||||||
`manualSorting={true}` is required — without it TanStack Table still applies its own client-side sort pass on top of the server-sorted data, producing incorrect order.
|
|
||||||
|
|
||||||
**Reference implementation:** `MarketingTable` in [src/components/pages/marketing/MarketingTable.tsx](src/components/pages/marketing/MarketingTable.tsx).
|
|
||||||
|
|
||||||
## Server-side file export pattern
|
|
||||||
|
|
||||||
All file exports (Excel, PDF, or any format) must use **server-side generation** — the server returns a binary blob and the browser triggers a download. Never generate files client-side with `xlsx`, `@react-pdf/renderer`, `jspdf`, or similar libraries.
|
|
||||||
|
|
||||||
**Rule:** Export methods live in the API service class, not in components. Components only build the query string and call the service method.
|
|
||||||
|
|
||||||
### Service method (in `src/services/api/{feature}.ts`)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
async exportToExcel(initialQueryString: string) {
|
|
||||||
const params = new URLSearchParams(initialQueryString);
|
|
||||||
|
|
||||||
params.set('export', 'excel'); // or 'pdf', 'csv', etc.
|
|
||||||
params.set('page', '1');
|
|
||||||
params.set('limit', '99999999999');
|
|
||||||
|
|
||||||
const res = await httpClient<Blob>(`${this.basePath}?${params.toString()}`, {
|
|
||||||
method: 'GET',
|
|
||||||
responseType: 'blob',
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([res]));
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute('download', `filename-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`);
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Change `export=excel` → `export=pdf` (and the file extension) for PDF exports.
|
|
||||||
- Add one method per format; keep them side-by-side in the same service class.
|
|
||||||
|
|
||||||
### Component handler (in the page/tab component)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const handleExportExcel = useCallback(async () => {
|
|
||||||
setIsExcelExportLoading(true);
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (filterParams.foo) params.set('foo', filterParams.foo);
|
|
||||||
// ... map all active filter params ...
|
|
||||||
|
|
||||||
await FeatureApi.exportToExcel(params.toString());
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExcelExportLoading(false);
|
|
||||||
}
|
|
||||||
}, [filterParams, searchValue]);
|
|
||||||
```
|
|
||||||
|
|
||||||
- Do **not** fetch all rows into the component to build the file — delegate entirely to the service method.
|
|
||||||
- Do **not** import `xlsx`, `@react-pdf/renderer`, `jspdf`, `exceljs` in page/tab components.
|
|
||||||
|
|
||||||
**Reference implementation:** `MarketingReportApiService.exportDailyMarketingToExcel` / `exportDailyMarketingToPDF` in [src/services/api/report/marketing-report.ts](src/services/api/report/marketing-report.ts), consumed by [src/components/pages/report/marketing/tab/DailyMarketingTab.tsx](src/components/pages/report/marketing/tab/DailyMarketingTab.tsx).
|
|
||||||
|
|
||||||
<!-- rtk-instructions v2 -->
|
|
||||||
|
|
||||||
# RTK (Rust Token Killer) - Token-Optimized Commands
|
|
||||||
|
|
||||||
## Golden Rule
|
|
||||||
|
|
||||||
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
|
|
||||||
|
|
||||||
**Important**: Even in command chains with `&&`, use `rtk`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ❌ Wrong
|
|
||||||
git add . && git commit -m "msg" && git push
|
|
||||||
|
|
||||||
# ✅ Correct
|
|
||||||
rtk git add . && rtk git commit -m "msg" && rtk git push
|
|
||||||
```
|
|
||||||
|
|
||||||
## RTK Commands by Workflow
|
|
||||||
|
|
||||||
### Build & Compile (80-90% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk cargo build # Cargo build output
|
|
||||||
rtk cargo check # Cargo check output
|
|
||||||
rtk cargo clippy # Clippy warnings grouped by file (80%)
|
|
||||||
rtk tsc # TypeScript errors grouped by file/code (83%)
|
|
||||||
rtk lint # ESLint/Biome violations grouped (84%)
|
|
||||||
rtk prettier --check # Files needing format only (70%)
|
|
||||||
rtk next build # Next.js build with route metrics (87%)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test (60-99% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk cargo test # Cargo test failures only (90%)
|
|
||||||
rtk go test # Go test failures only (90%)
|
|
||||||
rtk jest # Jest failures only (99.5%)
|
|
||||||
rtk vitest # Vitest failures only (99.5%)
|
|
||||||
rtk playwright test # Playwright failures only (94%)
|
|
||||||
rtk pytest # Python test failures only (90%)
|
|
||||||
rtk rake test # Ruby test failures only (90%)
|
|
||||||
rtk rspec # RSpec test failures only (60%)
|
|
||||||
rtk test <cmd> # Generic test wrapper - failures only
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git (59-80% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk git status # Compact status
|
|
||||||
rtk git log # Compact log (works with all git flags)
|
|
||||||
rtk git diff # Compact diff (80%)
|
|
||||||
rtk git show # Compact show (80%)
|
|
||||||
rtk git add # Ultra-compact confirmations (59%)
|
|
||||||
rtk git commit # Ultra-compact confirmations (59%)
|
|
||||||
rtk git push # Ultra-compact confirmations
|
|
||||||
rtk git pull # Ultra-compact confirmations
|
|
||||||
rtk git branch # Compact branch list
|
|
||||||
rtk git fetch # Compact fetch
|
|
||||||
rtk git stash # Compact stash
|
|
||||||
rtk git worktree # Compact worktree
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
|
|
||||||
|
|
||||||
### GitHub (26-87% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk gh pr view <num> # Compact PR view (87%)
|
|
||||||
rtk gh pr checks # Compact PR checks (79%)
|
|
||||||
rtk gh run list # Compact workflow runs (82%)
|
|
||||||
rtk gh issue list # Compact issue list (80%)
|
|
||||||
rtk gh api # Compact API responses (26%)
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript/TypeScript Tooling (70-90% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk pnpm list # Compact dependency tree (70%)
|
|
||||||
rtk pnpm outdated # Compact outdated packages (80%)
|
|
||||||
rtk pnpm install # Compact install output (90%)
|
|
||||||
rtk npm run <script> # Compact npm script output
|
|
||||||
rtk npx <cmd> # Compact npx command output
|
|
||||||
rtk prisma # Prisma without ASCII art (88%)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Files & Search (60-75% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk ls <path> # Tree format, compact (65%)
|
|
||||||
rtk read <file> # Code reading with filtering (60%)
|
|
||||||
rtk grep <pattern> # Search grouped by file (75%)
|
|
||||||
rtk find <pattern> # Find grouped by directory (70%)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Analysis & Debug (70-90% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk err <cmd> # Filter errors only from any command
|
|
||||||
rtk log <file> # Deduplicated logs with counts
|
|
||||||
rtk json <file> # JSON structure without values
|
|
||||||
rtk deps # Dependency overview
|
|
||||||
rtk env # Environment variables compact
|
|
||||||
rtk summary <cmd> # Smart summary of command output
|
|
||||||
rtk diff # Ultra-compact diffs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Infrastructure (85% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk docker ps # Compact container list
|
|
||||||
rtk docker images # Compact image list
|
|
||||||
rtk docker logs <c> # Deduplicated logs
|
|
||||||
rtk kubectl get # Compact resource list
|
|
||||||
rtk kubectl logs # Deduplicated pod logs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Network (65-70% savings)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk curl <url> # Compact HTTP responses (70%)
|
|
||||||
rtk wget <url> # Compact download output (65%)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Meta Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rtk gain # View token savings statistics
|
|
||||||
rtk gain --history # View command history with savings
|
|
||||||
rtk discover # Analyze Claude Code sessions for missed RTK usage
|
|
||||||
rtk proxy <cmd> # Run command without filtering (for debugging)
|
|
||||||
rtk init # Add RTK instructions to CLAUDE.md
|
|
||||||
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Token Savings Overview
|
|
||||||
|
|
||||||
| Category | Commands | Typical Savings |
|
|
||||||
| ---------------- | ------------------------------ | --------------- |
|
|
||||||
| Tests | vitest, playwright, cargo test | 90-99% |
|
|
||||||
| Build | next, tsc, lint, prettier | 70-87% |
|
|
||||||
| Git | status, log, diff, add, commit | 59-80% |
|
|
||||||
| GitHub | gh pr, gh run, gh issue | 26-87% |
|
|
||||||
| Package Managers | pnpm, npm, npx | 70-90% |
|
|
||||||
| Files | ls, read, grep, find | 60-75% |
|
|
||||||
| Infrastructure | docker, kubectl | 85% |
|
|
||||||
| Network | curl, wget | 65-70% |
|
|
||||||
|
|
||||||
Overall average: **60-90% token reduction** on common development operations.
|
|
||||||
|
|
||||||
<!-- /rtk-instructions -->
|
|
||||||
@@ -15,8 +15,8 @@ const ExpenseDetailPage = () => {
|
|||||||
const expenseId = searchParams.get('expenseId');
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
['expense-detail', expenseId],
|
expenseId,
|
||||||
([_, id]) => ExpenseApi.getSingle(Number(id))
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!expenseId) {
|
if (!expenseId) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { SystemConfigContent } from '@/figma-make/components/pages/master-data/system-config/SystemConfigContent';
|
|
||||||
|
|
||||||
const SystemConfigPage = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full'>
|
|
||||||
<SystemConfigContent />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemConfigPage;
|
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ const useSelect = <T,>(
|
|||||||
|
|
||||||
const qs = new URLSearchParams({
|
const qs = new URLSearchParams({
|
||||||
...(params ?? {}),
|
...(params ?? {}),
|
||||||
[searchKey ? searchKey : 'search']: inputValue ?? '',
|
[searchKey]: inputValue ?? '',
|
||||||
[pageKey]: String(pageIndex + 1),
|
[pageKey]: String(pageIndex + 1),
|
||||||
[limitKey]: String(limit),
|
[limitKey]: String(limit),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
secondaryButton={
|
secondaryButton={
|
||||||
secondaryButton
|
secondaryButton
|
||||||
? {
|
? {
|
||||||
...secondaryButton,
|
|
||||||
text: secondaryButton?.text ?? 'Tidak',
|
text: secondaryButton?.text ?? 'Tidak',
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
if (secondaryButton && secondaryButton?.onClick) {
|
if (secondaryButton && secondaryButton?.onClick) {
|
||||||
|
|||||||
@@ -173,8 +173,8 @@ const DashboardProduction = () => {
|
|||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect<ProjectFlockKandang>(
|
} = useSelect<ProjectFlockKandang>(
|
||||||
ProjectFlockKandangApi.basePath,
|
ProjectFlockKandangApi.basePath,
|
||||||
'kandang_id',
|
'id',
|
||||||
'kandang.name',
|
'name_with_period',
|
||||||
'search',
|
'search',
|
||||||
{
|
{
|
||||||
location_id:
|
location_id:
|
||||||
@@ -362,7 +362,7 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
||||||
{dashboardProductionData && (
|
{exporting && dashboardProductionData && (
|
||||||
<>
|
<>
|
||||||
{/* Export Stats Charts */}
|
{/* Export Stats Charts */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -10,14 +10,16 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont
|
|||||||
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
|
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
|
||||||
|
|
||||||
interface ExpenseDetailProps {
|
interface ExpenseDetailProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||||
const router = useRouter();
|
|
||||||
const [activeTab, setActiveTab] = useState<string>('request');
|
const [activeTab, setActiveTab] = useState<string>('request');
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
|
||||||
const expenseDetailTabs = useMemo(() => {
|
const expenseDetailTabs = useMemo(() => {
|
||||||
const validTabs = [
|
const validTabs = [
|
||||||
@@ -48,8 +50,8 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
<section className='w-full max-w-full pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
|
href={returnTo}
|
||||||
variant='link'
|
variant='link'
|
||||||
onClick={router.back}
|
|
||||||
className='w-fit p-0 text-primary'
|
className='w-fit p-0 text-primary'
|
||||||
>
|
>
|
||||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useSWRConfig } from 'swr';
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -20,7 +19,6 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
@@ -28,7 +26,7 @@ import {
|
|||||||
UploadRequestDocumentsFormSchema,
|
UploadRequestDocumentsFormSchema,
|
||||||
UploadRequestDocumentsFormValues,
|
UploadRequestDocumentsFormValues,
|
||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
@@ -48,11 +46,6 @@ const ExpenseRequestContent = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const returnTo = getExpenseListReturnTo(searchParams);
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
|
|
||||||
const refreshExpense = () => {
|
|
||||||
mutate((key) => Array.isArray(key) && key[0] === 'expense-detail');
|
|
||||||
};
|
|
||||||
|
|
||||||
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
||||||
useApprovalSteps({
|
useApprovalSteps({
|
||||||
@@ -102,24 +95,17 @@ const ExpenseRequestContent = ({
|
|||||||
!isLatestApprovalRejected &&
|
!isLatestApprovalRejected &&
|
||||||
initialValues?.latest_approval.step_number === 4;
|
initialValues?.latest_approval.step_number === 4;
|
||||||
|
|
||||||
const isExpensePaidOff = initialValues?.is_paid;
|
|
||||||
|
|
||||||
const showPaidOffButton =
|
|
||||||
!isExpensePaidOff && (initialValues?.latest_approval.step_number ?? 0) >= 4;
|
|
||||||
|
|
||||||
// Modal hooks
|
// Modal hooks
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const completeModal = useModal();
|
const completeModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
const paidOffModal = useModal();
|
|
||||||
|
|
||||||
// Modal loading state
|
// Modal loading state
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
const [isPaidOffLoading, setIsPaidOffLoading] = useState(false);
|
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
@@ -160,31 +146,7 @@ const ExpenseRequestContent = ({
|
|||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const paidOffClickHandler = () => {
|
|
||||||
paidOffModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Modal confirm click handler
|
// Modal confirm click handler
|
||||||
const confirmationModalPaidOffClickHandler = async () => {
|
|
||||||
setIsPaidOffLoading(true);
|
|
||||||
|
|
||||||
const paidOffResponse = await ExpenseApi.setExpensePaidOff(
|
|
||||||
initialValues?.id as number
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseSuccess(paidOffResponse)) {
|
|
||||||
toast.success('Berhasil menandai biaya operasional sebagai lunas!');
|
|
||||||
refreshExpense();
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
'Gagal menandai biaya operasional sebagai lunas!: ' +
|
|
||||||
paidOffResponse?.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
paidOffModal.closeModal();
|
|
||||||
setIsPaidOffLoading(false);
|
|
||||||
};
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -426,24 +388,6 @@ const ExpenseRequestContent = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showPaidOffButton && (
|
|
||||||
<RequirePermission permissions='lti.expense.create.realization'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='success'
|
|
||||||
onClick={paidOffClickHandler}
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:check-circle-outline'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
Tandai Lunas
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.expense.update'>
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
@@ -589,19 +533,6 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th>Status Lunas</th>
|
|
||||||
<th>:</th>
|
|
||||||
<td>
|
|
||||||
<StatusBadge
|
|
||||||
color={initialValues?.is_paid ? 'primary' : 'warning'}
|
|
||||||
text={initialValues?.is_paid ? 'Lunas' : 'Belum Lunas'}
|
|
||||||
className={{
|
|
||||||
badge: 'w-fit whitespace-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Dokumen Pengajuan</th>
|
<th>Dokumen Pengajuan</th>
|
||||||
<th>:</th>
|
<th>:</th>
|
||||||
@@ -617,15 +548,21 @@ const ExpenseRequestContent = ({
|
|||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
{initialValues?.documents.map(
|
{initialValues?.documents.map(
|
||||||
(requestDocument, requestDocumentIdx) => {
|
(requestDocument, requestDocumentIdx) => {
|
||||||
|
const path = requestDocument.path.startsWith(
|
||||||
|
'/'
|
||||||
|
)
|
||||||
|
? requestDocument.path.slice(1)
|
||||||
|
: requestDocument.path;
|
||||||
|
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
|
||||||
return (
|
return (
|
||||||
<li key={requestDocumentIdx}>
|
<li key={requestDocumentIdx}>
|
||||||
<Link
|
<Link
|
||||||
href={requestDocument.path}
|
href={documentUrl}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-blue-500 underline'
|
className='text-blue-500 underline'
|
||||||
>
|
>
|
||||||
{requestDocument.name}{' '}
|
{requestDocument.path}{' '}
|
||||||
<Icon
|
<Icon
|
||||||
icon='cuida:open-in-new-tab-outline'
|
icon='cuida:open-in-new-tab-outline'
|
||||||
width={12}
|
width={12}
|
||||||
@@ -821,21 +758,6 @@ const ExpenseRequestContent = ({
|
|||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={paidOffModal.ref}
|
|
||||||
type='success'
|
|
||||||
text='Apakah anda yakin ingin menandai biaya operasional ini sebagai lunas?'
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
color: 'success',
|
|
||||||
isLoading: isPaidOffLoading,
|
|
||||||
onClick: confirmationModalPaidOffClickHandler,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,60 +3,26 @@ import * as yup from 'yup';
|
|||||||
export type ExpensesFilterType = {
|
export type ExpensesFilterType = {
|
||||||
transaction_date: string | null;
|
transaction_date: string | null;
|
||||||
realization_date: string | null;
|
realization_date: string | null;
|
||||||
location: { value: number; label: string } | null;
|
location_id: string | null;
|
||||||
vendor: { value: number; label: string } | null;
|
vendor_id: string | null;
|
||||||
category: { value: string; label: string } | null;
|
|
||||||
approval_status: { value: string; label: string } | null;
|
|
||||||
realization_status: { value: string; label: string } | null;
|
|
||||||
project_flock: { value: number; label: string } | null;
|
|
||||||
project_flock_kandang: { value: number; label: string } | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpensesFilterSchema = yup.object({
|
export const ExpensesFilterSchema = yup.object({
|
||||||
transaction_date: yup.string().nullable(),
|
transaction_date: yup.string().nullable(),
|
||||||
realization_date: yup.string().nullable(),
|
realization_date: yup
|
||||||
location: yup
|
.string()
|
||||||
.object({
|
.nullable()
|
||||||
value: yup.number().required(),
|
.test(
|
||||||
label: yup.string().required(),
|
'is-greater-or-equal-transaction',
|
||||||
})
|
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
|
||||||
.nullable(),
|
function (value) {
|
||||||
vendor: yup
|
const { transaction_date } = this.parent;
|
||||||
.object({
|
if (!transaction_date || !value) return true;
|
||||||
value: yup.number().required(),
|
return new Date(value) >= new Date(transaction_date);
|
||||||
label: yup.string().required(),
|
}
|
||||||
})
|
),
|
||||||
.nullable(),
|
location_id: yup.string().nullable(),
|
||||||
category: yup
|
vendor_id: yup.string().nullable(),
|
||||||
.object({
|
|
||||||
value: yup.string().required(),
|
|
||||||
label: yup.string().required(),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
approval_status: yup
|
|
||||||
.object({
|
|
||||||
value: yup.string().required(),
|
|
||||||
label: yup.string().required(),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
realization_status: yup
|
|
||||||
.object({
|
|
||||||
value: yup.string().required(),
|
|
||||||
label: yup.string().required(),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
project_flock: yup
|
|
||||||
.object({
|
|
||||||
value: yup.number().required(),
|
|
||||||
label: yup.string().required(),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
project_flock_kandang: yup
|
|
||||||
.object({
|
|
||||||
value: yup.number().required(),
|
|
||||||
label: yup.string().required(),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
import { RefObject } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -11,11 +11,8 @@ import SelectInput from '@/components/input/SelectInput';
|
|||||||
|
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import {
|
import {
|
||||||
ExpensesFilterSchema,
|
ExpensesFilterSchema,
|
||||||
ExpensesFilterValues,
|
ExpensesFilterValues,
|
||||||
@@ -34,143 +31,64 @@ const ExpensesFilterModal = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: ExpensesFilterModalProps) => {
|
}: ExpensesFilterModalProps) => {
|
||||||
const [selectedLocationId, setSelectedLocationId] = useState<string>(
|
|
||||||
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
|
||||||
);
|
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const categoryOptions = [
|
|
||||||
{ value: 'BOP', label: 'BOP' },
|
|
||||||
{ value: 'NON-BOP', label: 'NON-BOP' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const approvalStatusOptions = [
|
|
||||||
{ value: 'HEAD_AREA', label: 'Approval Head Area' },
|
|
||||||
{ value: 'UNIT_VICE_PRESIDENT', label: 'Approval Unit Vice President' },
|
|
||||||
{ value: 'FINANCE', label: 'Approval Finance' },
|
|
||||||
{ value: 'REALISASI', label: 'Realisasi' },
|
|
||||||
{ value: 'SELESAI', label: 'Selesai' },
|
|
||||||
{ value: 'DITOLAK', label: 'Ditolak' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const realizationStatusOptions = [
|
|
||||||
{ value: 'NOT_REALIZED', label: 'Belum Realisasi' },
|
|
||||||
{ value: 'REALIZED', label: 'Sudah Realisasi' },
|
|
||||||
{ value: 'REJECTED', label: 'Ditolak' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
loadMore: loadMoreLocations,
|
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: vendorOptions,
|
options: vendorOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
loadMore: loadMoreVendors,
|
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setProjectFlockInputValue,
|
|
||||||
rawData: projectFlocksRawData,
|
|
||||||
options: projectFlockOptions,
|
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
|
||||||
loadMore: loadMoreProjectFlocks,
|
|
||||||
} = useSelect<ProjectFlock>(
|
|
||||||
ProjectFlockApi.basePath,
|
|
||||||
'id',
|
|
||||||
'flock_name',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
location_id: selectedLocationId || '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const formik = useFormik<ExpensesFilterValues>({
|
const formik = useFormik<ExpensesFilterValues>({
|
||||||
enableReinitialize: true,
|
|
||||||
initialValues: initialValues || {
|
initialValues: initialValues || {
|
||||||
transaction_date: null,
|
transaction_date: null,
|
||||||
realization_date: null,
|
realization_date: null,
|
||||||
location: null,
|
location_id: null,
|
||||||
vendor: null,
|
vendor_id: null,
|
||||||
category: null,
|
|
||||||
approval_status: null,
|
|
||||||
realization_status: null,
|
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
},
|
||||||
validationSchema: ExpensesFilterSchema,
|
validationSchema: ExpensesFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
onSubmit?.(values);
|
onSubmit?.(values);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
});
|
onReset: () => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedLocationId(
|
|
||||||
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
|
||||||
);
|
|
||||||
}, [initialValues?.location]);
|
|
||||||
|
|
||||||
const { resetForm } = formik;
|
|
||||||
|
|
||||||
const formikResetHandler = useCallback(() => {
|
|
||||||
resetForm({
|
|
||||||
values: {
|
|
||||||
transaction_date: null,
|
|
||||||
realization_date: null,
|
|
||||||
location: null,
|
|
||||||
vendor: null,
|
|
||||||
category: null,
|
|
||||||
approval_status: null,
|
|
||||||
realization_status: null,
|
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setSelectedLocationId('');
|
|
||||||
onReset?.();
|
onReset?.();
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
}, [resetForm, onReset, closeModalHandler]);
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationValue = formik.values.location_id
|
||||||
|
? locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const vendorValue = formik.values.vendor_id
|
||||||
|
? vendorOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.vendor_id
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
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,9 +121,11 @@ const ExpensesFilterModal = ({
|
|||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='py-2 text-xs font-semibold'>Tanggal</span>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='transaction_date'
|
name='transaction_date'
|
||||||
label='Tanggal Transaksi'
|
|
||||||
placeholder='Tanggal Transaksi'
|
placeholder='Tanggal Transaksi'
|
||||||
value={formik.values.transaction_date || ''}
|
value={formik.values.transaction_date || ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
@@ -215,10 +135,9 @@ const ExpensesFilterModal = ({
|
|||||||
!!formik.errors.transaction_date
|
!!formik.errors.transaction_date
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
<DateInput
|
<DateInput
|
||||||
name='realization_date'
|
name='realization_date'
|
||||||
label='Tanggal Realisasi'
|
|
||||||
placeholder='Tanggal Realisasi'
|
placeholder='Tanggal Realisasi'
|
||||||
value={formik.values.realization_date || ''}
|
value={formik.values.realization_date || ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
@@ -228,16 +147,23 @@ const ExpensesFilterModal = ({
|
|||||||
!!formik.errors.realization_date
|
!!formik.errors.realization_date
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
{formik.touched.realization_date &&
|
||||||
|
formik.errors.realization_date && (
|
||||||
|
<span className='text-xs text-error'>
|
||||||
|
{formik.errors.realization_date}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={formik.values.location}
|
value={locationValue}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -247,87 +173,14 @@ const ExpensesFilterModal = ({
|
|||||||
label='Vendor'
|
label='Vendor'
|
||||||
placeholder='Pilih Vendor'
|
placeholder='Pilih Vendor'
|
||||||
options={vendorOptions}
|
options={vendorOptions}
|
||||||
value={formik.values.vendor}
|
value={vendorValue}
|
||||||
onChange={vendorChangeHandler}
|
onChange={vendorChangeHandler}
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
isLoading={isLoadingVendorOptions}
|
isLoading={isLoadingVendorOptions}
|
||||||
onMenuScrollToBottom={loadMoreVendors}
|
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Kategori'
|
|
||||||
placeholder='Pilih Kategori'
|
|
||||||
options={categoryOptions}
|
|
||||||
value={formik.values.category}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue('category', val as OptionType | null)
|
|
||||||
}
|
|
||||||
isClearable
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Status BOP'
|
|
||||||
placeholder='Pilih Status BOP'
|
|
||||||
options={approvalStatusOptions}
|
|
||||||
value={formik.values.approval_status}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue('approval_status', val as OptionType | null)
|
|
||||||
}
|
|
||||||
isClearable
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Status Pencairan'
|
|
||||||
placeholder='Pilih Status Pencairan'
|
|
||||||
options={realizationStatusOptions}
|
|
||||||
value={formik.values.realization_status}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'realization_status',
|
|
||||||
val as OptionType | null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isClearable
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Project Flock'
|
|
||||||
placeholder='Pilih Project Flock'
|
|
||||||
options={projectFlockOptions}
|
|
||||||
value={formik.values.project_flock}
|
|
||||||
onChange={(val) => {
|
|
||||||
formik.setFieldValue('project_flock', val as OptionType | null);
|
|
||||||
formik.setFieldValue('project_flock_kandang', null);
|
|
||||||
}}
|
|
||||||
onInputChange={setProjectFlockInputValue}
|
|
||||||
isLoading={isLoadingProjectFlockOptions}
|
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
|
||||||
isClearable
|
|
||||||
isSearchable={true}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Kandang'
|
|
||||||
placeholder='Pilih Kandang'
|
|
||||||
options={projectFlockKandangOptions}
|
|
||||||
value={formik.values.project_flock_kandang}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'project_flock_kandang',
|
|
||||||
val as OptionType | null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isClearable
|
|
||||||
isDisabled={!formik.values.project_flock}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
import {
|
useCallback,
|
||||||
CellContext,
|
useEffect,
|
||||||
ColumnDef,
|
useMemo,
|
||||||
SortingState,
|
useRef,
|
||||||
Updater,
|
useState,
|
||||||
} from '@tanstack/react-table';
|
} from 'react';
|
||||||
|
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -38,7 +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 {
|
import {
|
||||||
FinanceTableFilterSchema,
|
FinanceTableFilterSchema,
|
||||||
FinanceTableFilterValues,
|
FinanceTableFilterValues,
|
||||||
@@ -175,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,
|
||||||
@@ -183,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',
|
||||||
@@ -203,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 =====
|
||||||
@@ -239,7 +235,7 @@ const FinanceTable = () => {
|
|||||||
// ===== 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: '',
|
||||||
@@ -249,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();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,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
|
||||||
@@ -402,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;
|
||||||
@@ -482,74 +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 resetFilterHandler = () => {
|
||||||
|
setSelectedTransactionType(null);
|
||||||
|
setSelectedBank(null);
|
||||||
|
setSelectedCustomerId(null);
|
||||||
|
setSelectedSupplierId(null);
|
||||||
|
setSelectedSortBy(null);
|
||||||
|
|
||||||
|
filterFormik.resetForm();
|
||||||
|
|
||||||
|
updateFilter('search', '');
|
||||||
|
resetSearchValue();
|
||||||
|
updateFilter('transactionTypes', '');
|
||||||
|
updateFilter('bankIds', '');
|
||||||
|
updateFilter('customerIds', '');
|
||||||
|
updateFilter('supplierIds', '');
|
||||||
|
updateFilter('sortBy', '');
|
||||||
|
updateFilter('startDate', '');
|
||||||
|
updateFilter('endDate', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -568,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>;
|
||||||
@@ -582,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('_')
|
||||||
@@ -592,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>;
|
||||||
@@ -602,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>;
|
||||||
@@ -626,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',
|
||||||
@@ -684,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'>
|
||||||
@@ -745,20 +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',
|
||||||
|
{
|
||||||
|
'border-primary-gradient text-primary': hasFilters,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
Filter
|
||||||
|
{hasFilters && (
|
||||||
|
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
||||||
|
{activeFiltersCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -794,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',
|
||||||
@@ -930,9 +874,19 @@ const FinanceTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
filterFormik.resetForm();
|
||||||
|
setSelectedTransactionType(null);
|
||||||
|
setSelectedBank(null);
|
||||||
|
setSelectedCustomerId(null);
|
||||||
|
setSelectedSupplierId(null);
|
||||||
|
setSelectedSortBy(null);
|
||||||
|
resetFilterHandler();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import useSWR from 'swr';
|
import { usePathname } from 'next/navigation';
|
||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -24,6 +25,7 @@ import { cn, formatNumber, formatDate, formatCurrency } from '@/lib/helper';
|
|||||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -98,31 +100,25 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const InventoryAdjustmentTable = () => {
|
const InventoryAdjustmentTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<{
|
} = useTableFilter({
|
||||||
search: string;
|
|
||||||
productCategorySort: string;
|
|
||||||
productSort: string;
|
|
||||||
warehouseSort: string;
|
|
||||||
stockSort: string;
|
|
||||||
productFilter?: OptionType<string>;
|
|
||||||
warehouseFilter?: OptionType<string>;
|
|
||||||
transactionTypeFilter?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productCategorySort: '',
|
productCategorySort: '',
|
||||||
productSort: '',
|
productSort: '',
|
||||||
warehouseSort: '',
|
warehouseSort: '',
|
||||||
stockSort: '',
|
stockSort: '',
|
||||||
productFilter: undefined,
|
productFilter: '',
|
||||||
warehouseFilter: undefined,
|
warehouseFilter: '',
|
||||||
transactionTypeFilter: undefined,
|
transactionTypeFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -135,8 +131,6 @@ const InventoryAdjustmentTable = () => {
|
|||||||
warehouseFilter: 'warehouse_id',
|
warehouseFilter: 'warehouse_id',
|
||||||
transactionTypeFilter: 'transaction_type',
|
transactionTypeFilter: 'transaction_type',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'inventory-adjustment-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -145,27 +139,22 @@ const InventoryAdjustmentTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<AdjustmentFilterType>({
|
const formik = useFormik<AdjustmentFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product: tableFilterState.productFilter,
|
product_id: null,
|
||||||
warehouse: tableFilterState.warehouseFilter,
|
warehouse: null,
|
||||||
transaction_type: tableFilterState.transactionTypeFilter,
|
transaction_type: null,
|
||||||
},
|
},
|
||||||
validationSchema: AdjustmentFilterSchema,
|
validationSchema: AdjustmentFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product || undefined, true);
|
updateFilter('productFilter', values.product_id || '');
|
||||||
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
updateFilter('warehouseFilter', String(values.warehouse?.value) || '');
|
||||||
updateFilter(
|
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
||||||
'transactionTypeFilter',
|
|
||||||
values.transaction_type || undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', undefined, true);
|
updateFilter('productFilter', '');
|
||||||
updateFilter('warehouseFilter', undefined, true);
|
updateFilter('warehouseFilter', '');
|
||||||
updateFilter('transactionTypeFilter', undefined, true);
|
updateFilter('transactionTypeFilter', '');
|
||||||
filterModal.closeModal();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,9 +193,14 @@ const InventoryAdjustmentTable = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterProductChange = useCallback(
|
||||||
formik.setFieldValue('product', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
};
|
const product = val as OptionType | null;
|
||||||
|
const productId = product?.value ? String(product.value) : null;
|
||||||
|
formik.setFieldValue('product_id', productId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
const handleFilterWarehouseChange = (
|
const handleFilterWarehouseChange = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
@@ -214,20 +208,38 @@ const InventoryAdjustmentTable = () => {
|
|||||||
formik.setFieldValue('warehouse', val);
|
formik.setFieldValue('warehouse', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterTransactionTypeChange = (
|
const handleFilterTransactionTypeChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const type = val as OptionType | null;
|
||||||
formik.setFieldValue('transaction_type', val);
|
const typeValue = type?.value ? String(type.value) : null;
|
||||||
};
|
formik.setFieldValue('transaction_type', typeValue);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const productIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.product_id) return null;
|
||||||
|
return (
|
||||||
|
productOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.product_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.product_id, productOptions]);
|
||||||
|
|
||||||
|
const transactionTypeValue = useMemo(() => {
|
||||||
|
if (!formik.values.transaction_type) return null;
|
||||||
|
return (
|
||||||
|
transactionTypeOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.transaction_type
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.transaction_type, transactionTypeOptions]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
formik.setValues({
|
|
||||||
product: tableFilterState.productFilter ?? undefined,
|
|
||||||
warehouse: tableFilterState.warehouseFilter ?? undefined,
|
|
||||||
transaction_type: tableFilterState.transactionTypeFilter ?? undefined,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -264,8 +276,17 @@ const InventoryAdjustmentTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('inventory-adjustment-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
||||||
@@ -486,8 +507,6 @@ const InventoryAdjustmentTable = () => {
|
|||||||
'productSort',
|
'productSort',
|
||||||
'warehouseSort',
|
'warehouseSort',
|
||||||
'stockSort',
|
'stockSort',
|
||||||
'productName',
|
|
||||||
'warehouseName',
|
|
||||||
]}
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -577,7 +596,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={formik.values.product}
|
value={productIdValue}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -601,7 +620,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
label='Tipe Transaksi'
|
label='Tipe Transaksi'
|
||||||
placeholder='Pilih Tipe Transaksi'
|
placeholder='Pilih Tipe Transaksi'
|
||||||
options={transactionTypeOptions}
|
options={transactionTypeOptions}
|
||||||
value={formik.values.transaction_type}
|
value={transactionTypeValue}
|
||||||
onChange={handleFilterTransactionTypeChange}
|
onChange={handleFilterTransactionTypeChange}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -611,9 +630,13 @@ const InventoryAdjustmentTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
|
import { string, object } from 'yup';
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const AdjustmentFilterSchema = Yup.object().shape({
|
export const AdjustmentFilterSchema = object().shape({
|
||||||
product: Yup.object({
|
product_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
warehouse_id: string().nullable(),
|
||||||
label: Yup.string().nullable(),
|
transaction_type: string().nullable(),
|
||||||
}).nullable(),
|
|
||||||
warehouse: Yup.object({
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
transaction_type: Yup.object({
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AdjustmentFilterType = {
|
export type AdjustmentFilterType = {
|
||||||
product?: OptionType<string>;
|
product_id: string | null;
|
||||||
warehouse?: OptionType<string>;
|
transaction_type: string | null;
|
||||||
transaction_type?: OptionType<string>;
|
warehouse: OptionType<number> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
import useSWR from 'swr';
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
@@ -13,6 +20,7 @@ import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -100,21 +108,20 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MovementTable = () => {
|
const MovementTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<{
|
} = useTableFilter({
|
||||||
search: string;
|
|
||||||
productFilter?: OptionType<string>;
|
|
||||||
warehouseFilter?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productFilter: undefined,
|
productFilter: '',
|
||||||
warehouseFilter: undefined,
|
warehouseFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -122,8 +129,6 @@ const MovementTable = () => {
|
|||||||
productFilter: 'product_id',
|
productFilter: 'product_id',
|
||||||
warehouseFilter: 'warehouse_id',
|
warehouseFilter: 'warehouse_id',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'movement-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -132,20 +137,19 @@ const MovementTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<MovementFilterType>({
|
const formik = useFormik<MovementFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product: tableFilterState.productFilter,
|
product_id: null,
|
||||||
warehouse: tableFilterState.warehouseFilter,
|
warehouse_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: MovementFilterSchema,
|
validationSchema: MovementFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product || undefined, true);
|
updateFilter('productFilter', values.product_id || '');
|
||||||
updateFilter('warehouseFilter', values.warehouse || undefined, true);
|
updateFilter('warehouseFilter', values.warehouse_id || '');
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('productFilter', undefined, true);
|
updateFilter('productFilter', '');
|
||||||
updateFilter('warehouseFilter', undefined, true);
|
updateFilter('warehouseFilter', '');
|
||||||
filterModal.closeModal();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,23 +180,47 @@ const MovementTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterProductChange = useCallback(
|
||||||
formik.setFieldValue('product', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
};
|
const product = val as OptionType | null;
|
||||||
|
const productId = product?.value ? String(product.value) : null;
|
||||||
|
formik.setFieldValue('product_id', productId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
const handleFilterWarehouseChange = (
|
const handleFilterWarehouseChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const warehouse = val as OptionType | null;
|
||||||
formik.setFieldValue('warehouse', val);
|
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
||||||
};
|
formik.setFieldValue('warehouse_id', warehouseId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const productIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.product_id) return null;
|
||||||
|
return (
|
||||||
|
productOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.product_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.product_id, productOptions]);
|
||||||
|
|
||||||
|
const warehouseIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.warehouse_id) return null;
|
||||||
|
return (
|
||||||
|
warehouseOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.warehouse_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.warehouse_id, warehouseOptions]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
formik.setValues({
|
|
||||||
product: tableFilterState.productFilter ?? undefined,
|
|
||||||
warehouse: tableFilterState.warehouseFilter ?? undefined,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -227,8 +255,17 @@ const MovementTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('movement-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
||||||
@@ -427,7 +464,7 @@ const MovementTable = () => {
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
placeholder='Pilih Produk'
|
placeholder='Pilih Produk'
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
value={formik.values.product}
|
value={productIdValue}
|
||||||
onChange={handleFilterProductChange}
|
onChange={handleFilterProductChange}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
@@ -439,7 +476,7 @@ const MovementTable = () => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih Gudang'
|
placeholder='Pilih Gudang'
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
value={formik.values.warehouse}
|
value={warehouseIdValue}
|
||||||
onChange={handleFilterWarehouseChange}
|
onChange={handleFilterWarehouseChange}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
isLoading={isLoadingWarehouseOptions}
|
isLoading={isLoadingWarehouseOptions}
|
||||||
@@ -452,9 +489,13 @@ const MovementTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const MovementFilterSchema = Yup.object().shape({
|
export const MovementFilterSchema = object().shape({
|
||||||
product: Yup.object({
|
product_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
warehouse_id: string().nullable(),
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
warehouse: Yup.object({
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MovementFilterType = {
|
export type MovementFilterType = {
|
||||||
product?: OptionType<string>;
|
product_id: string | null;
|
||||||
warehouse?: OptionType<string>;
|
warehouse_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,7 +182,6 @@ 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'>
|
||||||
@@ -251,7 +199,7 @@ const InventoryProductTable = () => {
|
|||||||
</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'
|
||||||
@@ -259,11 +207,7 @@ const InventoryProductTable = () => {
|
|||||||
value={tableFilterState.search ?? ''}
|
value={tableFilterState.search ?? ''}
|
||||||
onChange={searchChangeHandler}
|
onChange={searchChangeHandler}
|
||||||
startAdornment={
|
startAdornment={
|
||||||
<Icon
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||||
icon='heroicons:magnifying-glass'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
@@ -272,12 +216,6 @@ const InventoryProductTable = () => {
|
|||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
'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>
|
</div>
|
||||||
|
|
||||||
@@ -334,62 +272,6 @@ const InventoryProductTable = () => {
|
|||||||
)}
|
)}
|
||||||
</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,20 +1,25 @@
|
|||||||
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>[] = (
|
const StockLogTable = ({
|
||||||
warehouseName
|
stockLogs,
|
||||||
) => [
|
}: {
|
||||||
|
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title='Informasi Stock Produk'
|
||||||
|
collapsible
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table<StockLog>
|
||||||
|
data={stockLogs}
|
||||||
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'ID',
|
header: 'ID',
|
||||||
accessorKey: 'id',
|
accessorKey: 'id',
|
||||||
@@ -29,7 +34,6 @@ const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
|||||||
{
|
{
|
||||||
header: 'Gudang',
|
header: 'Gudang',
|
||||||
accessorKey: 'warehouse_name',
|
accessorKey: 'warehouse_name',
|
||||||
cell: warehouseName,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Stock Akhir',
|
header: 'Stock Akhir',
|
||||||
@@ -72,100 +76,9 @@ const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
|||||||
header: 'Oleh',
|
header: 'Oleh',
|
||||||
accessorKey: 'created_user.name',
|
accessorKey: 'created_user.name',
|
||||||
},
|
},
|
||||||
];
|
]}
|
||||||
|
|
||||||
const StockLogTable = ({
|
|
||||||
productWarehouse,
|
|
||||||
}: {
|
|
||||||
productWarehouse: ProductWarehouseStock;
|
|
||||||
}) => {
|
|
||||||
const [isExportLoading, setIsExportLoading] = useState(false);
|
|
||||||
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setHasBeenVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.1 }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (containerRef.current) observer.observe(containerRef.current);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
} = useTableFilter({
|
|
||||||
initial: {
|
|
||||||
product_warehouse_id: productWarehouse.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleExportExcel = async () => {
|
|
||||||
setIsExportLoading(true);
|
|
||||||
try {
|
|
||||||
await StockLogApi.exportToExcel(
|
|
||||||
productWarehouse.warehouse_name,
|
|
||||||
getTableFilterQueryString()
|
|
||||||
);
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExportLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
|
||||||
hasBeenVisible
|
|
||||||
? `${StockLogApi.basePath}${getTableFilterQueryString()}`
|
|
||||||
: null,
|
|
||||||
StockLogApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const stockLogs = isResponseSuccess(stockLogsResponse)
|
|
||||||
? stockLogsResponse.data
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={containerRef}>
|
|
||||||
<Card
|
|
||||||
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
|
||||||
collapsible
|
|
||||||
variant='bordered'
|
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
containerClassName: 'mt-6',
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-end px-6 pt-4'>
|
|
||||||
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
|
||||||
<FileDown size={16} />
|
|
||||||
Export Excel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Table<StockLog>
|
|
||||||
data={stockLogs}
|
|
||||||
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
|
||||||
page={tableFilterState.page ?? 0}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
onPageChange={setPage}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
isLoading={isLoadingStockLogs}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(stockLogsResponse)
|
|
||||||
? stockLogsResponse.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
className={{
|
|
||||||
containerClassName: 'mt-4 mb-0',
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
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',
|
||||||
@@ -177,7 +90,6 @@ const StockLogTable = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
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>[] = [
|
const StockProductWarehouseTable = ({
|
||||||
|
productWarehouseStock,
|
||||||
|
}: {
|
||||||
|
productWarehouseStock?: ProductWarehouseStock[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title='Informasi Gudang'
|
||||||
|
collapsible
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table<ProductWarehouseStock>
|
||||||
|
data={productWarehouseStock ?? []}
|
||||||
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Nama Gudang',
|
header: 'Nama Gudang',
|
||||||
accessorKey: 'warehouse_name',
|
accessorKey: 'warehouse_name',
|
||||||
@@ -28,34 +42,9 @@ const stockProductWarehouseTableColumns: ColumnDef<ProductWarehouseStock>[] = [
|
|||||||
return formatNumber(props.row.original.current_stock);
|
return formatNumber(props.row.original.current_stock);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]}
|
||||||
|
|
||||||
const StockProductWarehouseTable = ({
|
|
||||||
productWarehouseStock,
|
|
||||||
}: {
|
|
||||||
productWarehouseStock?: ProductWarehouseStock[];
|
|
||||||
}) => {
|
|
||||||
const { state: tableFilterState, setPage, setPageSize } = useTableFilter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title='Informasi Gudang'
|
|
||||||
collapsible
|
|
||||||
variant='bordered'
|
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
containerClassName: 'mt-6',
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<ProductWarehouseStock>
|
|
||||||
data={productWarehouseStock ?? []}
|
|
||||||
columns={stockProductWarehouseTableColumns}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
page={tableFilterState.page ?? 0}
|
|
||||||
totalItems={productWarehouseStock?.length ?? 0}
|
|
||||||
onPageChange={setPage}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
className={{
|
|
||||||
containerClassName: 'mt-6 mb-0',
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
|||||||
@@ -849,11 +849,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
|||||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||||
disabled={deliveryRejected}
|
disabled={deliveryRejected}
|
||||||
>
|
>
|
||||||
{marketing?.data?.latest_approval?.step_number === 1 &&
|
Approve
|
||||||
'Approve'}
|
|
||||||
|
|
||||||
{marketing?.data?.latest_approval?.step_number === 2 &&
|
|
||||||
'Deliver Item'}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useCallback, useMemo } from 'react';
|
import { RefObject, useMemo } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -17,31 +17,20 @@ import {
|
|||||||
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
|
||||||
import { Product } from '@/types/api/master-data/product';
|
|
||||||
|
|
||||||
interface MarketingFilterModal {
|
interface MarketingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
onSubmit?: (values: MarketingFilter) => void;
|
onSubmit?: (values: MarketingFilter) => void;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
initialValues?: {
|
|
||||||
product_ids: OptionType<number>[];
|
|
||||||
status: OptionType<string> | null;
|
|
||||||
customer: OptionType<number> | null;
|
|
||||||
project_flock: OptionType<number> | null;
|
|
||||||
project_flock_kandang: OptionType<number> | null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarketingFilterModal = ({
|
const MarketingFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
initialValues,
|
|
||||||
}: MarketingFilterModal) => {
|
}: MarketingFilterModal) => {
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
@@ -49,13 +38,36 @@ const MarketingFilterModal = ({
|
|||||||
|
|
||||||
// ===== OPTIONS =====
|
// ===== OPTIONS =====
|
||||||
const {
|
const {
|
||||||
options: productsOptions,
|
rawData: productsRawData,
|
||||||
isLoadingOptions: isLoadingProductsOptions,
|
isLoadingOptions: isLoadingProductsOptions,
|
||||||
setInputValue: setProductsInputValue,
|
setInputValue: setProductsInputValue,
|
||||||
loadMore: loadMoreProducts,
|
loadMore: loadMoreProducts,
|
||||||
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
} = useSelect<BaseMarketing>(
|
||||||
include_all: 'true',
|
MarketingApi.basePath,
|
||||||
|
'id',
|
||||||
|
'so_number',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
|
const productsOptions = useMemo(() => {
|
||||||
|
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
|
||||||
|
|
||||||
|
const productsMap = new Map<number, { value: number; label: string }>();
|
||||||
|
|
||||||
|
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
|
||||||
|
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
|
||||||
|
const product = so.product_warehouse?.product;
|
||||||
|
if (product?.id && product?.name) {
|
||||||
|
productsMap.set(product.id, {
|
||||||
|
value: product.id,
|
||||||
|
label: product.name,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(productsMap.values());
|
||||||
|
}, [productsRawData]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: customersOptions,
|
options: customersOptions,
|
||||||
@@ -66,19 +78,6 @@ const MarketingFilterModal = ({
|
|||||||
has_marketing: 'true',
|
has_marketing: 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
options: projectFlockOptions,
|
|
||||||
rawData: projectFlocksRawData,
|
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
|
||||||
setInputValue: setProjectFlockInputValue,
|
|
||||||
loadMore: loadMoreProjectFlocks,
|
|
||||||
} = useSelect<ProjectFlock>(
|
|
||||||
ProjectFlockApi.basePath,
|
|
||||||
'id',
|
|
||||||
'flock_name',
|
|
||||||
'search'
|
|
||||||
);
|
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
...MARKETING_APPROVAL_LINE.map((item) => ({
|
...MARKETING_APPROVAL_LINE.map((item) => ({
|
||||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||||
@@ -88,29 +87,18 @@ const MarketingFilterModal = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const formik = useFormik<MarketingFilterFormValues>({
|
const formik = useFormik<MarketingFilterFormValues>({
|
||||||
initialValues: initialValues || {
|
initialValues: {
|
||||||
product_ids: [],
|
product_ids: [],
|
||||||
status: null,
|
status: null,
|
||||||
customer: null,
|
customer: null,
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
},
|
||||||
validationSchema: MarketingFilterSchema,
|
validationSchema: MarketingFilterSchema,
|
||||||
|
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues: MarketingFilter = {
|
const formattedValues: MarketingFilter = {
|
||||||
product_ids: values.product_ids.map((item) => Number(item.value)),
|
product_ids: values.product_ids.map((item) => Number(item.value)),
|
||||||
product_names: values.product_ids.map((item) => item.label),
|
|
||||||
status: values.status?.value.toString() || '',
|
status: values.status?.value.toString() || '',
|
||||||
status_name: values.status?.label || '-',
|
|
||||||
customer_id: Number(values.customer?.value),
|
customer_id: Number(values.customer?.value),
|
||||||
customer_name: values.customer?.label || '-',
|
|
||||||
project_flock_id: values.project_flock?.value || undefined,
|
|
||||||
project_flock_name: values.project_flock?.label,
|
|
||||||
project_flock_kandang_id:
|
|
||||||
Number(values.project_flock_kandang?.value) || undefined,
|
|
||||||
project_flock_kandang_name:
|
|
||||||
values.project_flock_kandang?.label || undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues);
|
||||||
@@ -123,22 +111,6 @@ const MarketingFilterModal = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resetForm } = formik;
|
|
||||||
|
|
||||||
const formikResetHandler = useCallback(() => {
|
|
||||||
resetForm({
|
|
||||||
values: {
|
|
||||||
product_ids: [],
|
|
||||||
status: null,
|
|
||||||
customer: null,
|
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
onReset?.();
|
|
||||||
closeModalHandler();
|
|
||||||
}, [resetForm, onReset, closeModalHandler]);
|
|
||||||
|
|
||||||
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('product_ids', val as OptionType[]);
|
formik.setFieldValue('product_ids', val as OptionType[]);
|
||||||
};
|
};
|
||||||
@@ -154,27 +126,6 @@ const MarketingFilterModal = ({
|
|||||||
formik.setFieldValue('status', val as OptionType);
|
formik.setFieldValue('status', val as OptionType);
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectFlockKandangOptions = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!formik.values.project_flock ||
|
|
||||||
!projectFlocksRawData ||
|
|
||||||
!isResponseSuccess(projectFlocksRawData)
|
|
||||||
) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedProjectFlock = projectFlocksRawData.data.find(
|
|
||||||
(item) => item.id === formik.values.project_flock?.value
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
selectedProjectFlock?.kandangs?.map((item) => ({
|
|
||||||
value: item.project_flock_kandang_id,
|
|
||||||
label: item.name,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [formik.values.project_flock, projectFlocksRawData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -184,7 +135,7 @@ const MarketingFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formikResetHandler}
|
onReset={formik.handleReset}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -241,37 +192,6 @@ const MarketingFilterModal = ({
|
|||||||
onInputChange={setCustomersInputValue}
|
onInputChange={setCustomersInputValue}
|
||||||
onMenuScrollToBottom={loadMoreCustomers}
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
|
||||||
label='Project Flock'
|
|
||||||
isClearable
|
|
||||||
placeholder='Pilih Project Flock'
|
|
||||||
options={projectFlockOptions}
|
|
||||||
isLoading={isLoadingProjectFlockOptions}
|
|
||||||
value={formik.values.project_flock}
|
|
||||||
onChange={(val) => {
|
|
||||||
formik.setFieldValue(
|
|
||||||
'project_flock',
|
|
||||||
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
|
||||||
);
|
|
||||||
formik.setFieldValue('project_flock_kandang', null);
|
|
||||||
}}
|
|
||||||
onInputChange={setProjectFlockInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
|
||||||
/>
|
|
||||||
<SelectInput
|
|
||||||
label='Kandang'
|
|
||||||
isClearable
|
|
||||||
placeholder='Pilih Kandang'
|
|
||||||
options={projectFlockKandangOptions}
|
|
||||||
value={formik.values.project_flock_kandang}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'project_flock_kandang',
|
|
||||||
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isDisabled={!formik.values.project_flock}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -2,39 +2,26 @@
|
|||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import TextArea from '@/components/input/TextArea';
|
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import {
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
getErrorMessage,
|
|
||||||
isResponseError,
|
|
||||||
isResponseSuccess,
|
|
||||||
} from '@/lib/api-helper';
|
|
||||||
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
MarketingApi,
|
MarketingApi,
|
||||||
SalesOrderApi,
|
SalesOrderApi,
|
||||||
} from '@/services/api/marketing/marketing';
|
} from '@/services/api/marketing/marketing';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import {
|
import {
|
||||||
BaseSalesOrder,
|
BaseSalesOrder,
|
||||||
Marketing,
|
Marketing,
|
||||||
MarketingFilter,
|
MarketingFilter,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import {
|
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
||||||
CellContext,
|
|
||||||
ColumnDef,
|
|
||||||
Row,
|
|
||||||
SortingState,
|
|
||||||
Updater,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
@@ -167,21 +154,12 @@ const MarketingTable = () => {
|
|||||||
);
|
);
|
||||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
const [bulkDeliveryDate, setBulkDeliveryDate] = useState('');
|
|
||||||
const [bulkDeliveryNotes, setBulkDeliveryNotes] = useState('');
|
|
||||||
const [isSubmittingBulkDelivery, setIsSubmittingBulkDelivery] =
|
|
||||||
useState(false);
|
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
|
||||||
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
|
||||||
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const confirmationModal = useModal();
|
const confirmationModal = useModal();
|
||||||
const productsModal = useModal();
|
const productsModal = useModal();
|
||||||
const deliveryModal = useModal();
|
const deliveryModal = useModal();
|
||||||
const bulkDeliveryModal = useModal();
|
|
||||||
const exportProgressInputModal = useModal();
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -194,17 +172,8 @@ const MarketingTable = () => {
|
|||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
product_ids: '',
|
product_ids: '',
|
||||||
product_names: '',
|
|
||||||
status: '',
|
status: '',
|
||||||
status_name: '',
|
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
customer_name: '',
|
|
||||||
project_flock_id: '',
|
|
||||||
project_flock_name: '',
|
|
||||||
project_flock_kandang_id: '',
|
|
||||||
project_flock_kandang_name: '',
|
|
||||||
sort_by: '',
|
|
||||||
order_by: '',
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -212,43 +181,9 @@ const MarketingTable = () => {
|
|||||||
product_ids: 'product_ids',
|
product_ids: 'product_ids',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
customer_id: 'customer_id',
|
customer_id: 'customer_id',
|
||||||
project_flock_id: 'project_flock_id',
|
|
||||||
project_flock_kandang_id: 'project_flock_kandang_id',
|
|
||||||
sort_by: 'sort_by',
|
|
||||||
order_by: 'sort_order',
|
|
||||||
},
|
},
|
||||||
excludeKeysFromUrl: [
|
|
||||||
'product_names',
|
|
||||||
'status_name',
|
|
||||||
'customer_name',
|
|
||||||
'project_flock_name',
|
|
||||||
'project_flock_kandang_name',
|
|
||||||
],
|
|
||||||
|
|
||||||
persist: true,
|
|
||||||
storeName: 'marketing-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sorting: SortingState = tableFilterState.sort_by
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: tableFilterState.sort_by,
|
|
||||||
desc: tableFilterState.order_by === 'desc',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
||||||
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
||||||
if (next.length > 0) {
|
|
||||||
updateFilter('sort_by', next[0].id, true);
|
|
||||||
updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
|
|
||||||
} else {
|
|
||||||
updateFilter('sort_by', '', true);
|
|
||||||
updateFilter('order_by', '', true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
data: marketing,
|
data: marketing,
|
||||||
@@ -263,64 +198,26 @@ const MarketingTable = () => {
|
|||||||
const filterSubmitHandler = (values: MarketingFilter) => {
|
const filterSubmitHandler = (values: MarketingFilter) => {
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'product_ids',
|
'product_ids',
|
||||||
values.product_ids?.map((item) => item.toString()).join(','),
|
values.product_ids?.map((item) => item.toString()).join(',')
|
||||||
true
|
|
||||||
);
|
);
|
||||||
updateFilter('product_names', values.product_names?.join(','));
|
updateFilter('status', values.status ? values.status.toString() : '');
|
||||||
updateFilter('status', values.status ? values.status.toString() : '', true);
|
|
||||||
updateFilter('status_name', values.status_name, true);
|
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'customer_id',
|
'customer_id',
|
||||||
values.customer_id ? values.customer_id.toString() : '',
|
values.customer_id ? values.customer_id.toString() : ''
|
||||||
true
|
|
||||||
);
|
|
||||||
updateFilter('customer_name', values.customer_name, true);
|
|
||||||
updateFilter(
|
|
||||||
'project_flock_id',
|
|
||||||
values.project_flock_id ? values.project_flock_id.toString() : '',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
updateFilter('project_flock_name', values.project_flock_name ?? '', true);
|
|
||||||
updateFilter(
|
|
||||||
'project_flock_kandang_id',
|
|
||||||
values.project_flock_kandang_id
|
|
||||||
? values.project_flock_kandang_id.toString()
|
|
||||||
: '',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
updateFilter(
|
|
||||||
'project_flock_kandang_name',
|
|
||||||
values.project_flock_kandang_name ?? '',
|
|
||||||
true
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
|
||||||
const [isDeliveryLoading, setIsDeliveryLoading] = useState(false);
|
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
updateFilter('product_ids', '', true);
|
updateFilter('product_ids', '');
|
||||||
updateFilter('product_names', '', true);
|
updateFilter('status', '');
|
||||||
updateFilter('status', '', true);
|
updateFilter('customer_id', '');
|
||||||
updateFilter('status_name', '', true);
|
|
||||||
updateFilter('customer_id', '', true);
|
|
||||||
updateFilter('customer_name', '', true);
|
|
||||||
updateFilter('project_flock_id', '', true);
|
|
||||||
updateFilter('project_flock_name', '', true);
|
|
||||||
updateFilter('project_flock_kandang_id', '', true);
|
|
||||||
updateFilter('project_flock_kandang_name', '', true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
setApproveAction('APPROVED');
|
setApproveAction('APPROVED');
|
||||||
|
|
||||||
if (selectedApprovalStep === 2) {
|
|
||||||
bulkDeliveryModal.openModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -329,13 +226,10 @@ const MarketingTable = () => {
|
|||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const productsClickHandler = useCallback(
|
const productsClickHandler = (item: Marketing) => {
|
||||||
(item: Marketing) => {
|
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
productsModal.openModal();
|
productsModal.openModal();
|
||||||
},
|
};
|
||||||
[productsModal]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMarketingHandler = async () => {
|
const deleteMarketingHandler = async () => {
|
||||||
const deleteMarketingRes = await MarketingApi.delete(
|
const deleteMarketingRes = await MarketingApi.delete(
|
||||||
@@ -357,115 +251,44 @@ 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.');
|
|
||||||
confirmationModal.closeModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
idsToProcess,
|
||||||
approveAction,
|
approveAction,
|
||||||
notes
|
notes
|
||||||
@@ -476,107 +299,27 @@ const MarketingTable = () => {
|
|||||||
toast.success(approveMarketingRes?.message as string);
|
toast.success(approveMarketingRes?.message as string);
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}
|
}
|
||||||
|
if (isResponseError(approveMarketingRes)) {
|
||||||
|
confirmationModal.closeModal();
|
||||||
|
toast.error(approveMarketingRes?.message as string);
|
||||||
|
}
|
||||||
refreshMarketing();
|
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({});
|
|
||||||
refreshMarketing();
|
|
||||||
} finally {
|
|
||||||
setIsSubmittingBulkDelivery(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||||
setIsDeliveryLoading(true);
|
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
||||||
try {
|
|
||||||
const res = await SalesOrderApi.delivery(
|
|
||||||
selectedItem?.id as number,
|
|
||||||
notes
|
|
||||||
);
|
|
||||||
deliveryModal.closeModal();
|
deliveryModal.closeModal();
|
||||||
toast.success(res?.message as string);
|
toast.success(res?.message as string);
|
||||||
refreshMarketing?.();
|
refreshMarketing?.();
|
||||||
router.push(
|
router.push(
|
||||||
`/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}`
|
`/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;
|
||||||
const isSelectableStep =
|
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
||||||
approval?.step_number === 1 || approval?.step_number === 2;
|
};
|
||||||
|
|
||||||
if (!isSelectableStep || approval?.action === 'REJECTED') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedApprovalStep === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return approval?.step_number === selectedApprovalStep;
|
|
||||||
},
|
|
||||||
[selectedApprovalStep]
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
const exportToExcelHandler = async () => {
|
||||||
setIsLoadingExportingToExcel(true);
|
setIsLoadingExportingToExcel(true);
|
||||||
@@ -586,53 +329,6 @@ const MarketingTable = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetExportProgressForm = () => {
|
|
||||||
setExportProgressStartDate('');
|
|
||||||
setExportProgressEndDate('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportProgressStartDateChangeHandler: ChangeEventHandler<
|
|
||||||
HTMLInputElement
|
|
||||||
> = (e) => {
|
|
||||||
setExportProgressStartDate(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportProgressEndDateChangeHandler: ChangeEventHandler<
|
|
||||||
HTMLInputElement
|
|
||||||
> = (e) => {
|
|
||||||
setExportProgressEndDate(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportProgressInputToExcelClickHandler = () => {
|
|
||||||
resetExportProgressForm();
|
|
||||||
exportProgressInputModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitExportProgressInputHandler = async () => {
|
|
||||||
if (!exportProgressStartDate || !exportProgressEndDate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsExportProgressLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await MarketingApi.exportInputProgressToExcel(
|
|
||||||
exportProgressStartDate,
|
|
||||||
exportProgressEndDate
|
|
||||||
);
|
|
||||||
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
toast.success('Ekspor berhasil');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsExportProgressLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -640,22 +336,7 @@ const MarketingTable = () => {
|
|||||||
size: 1,
|
size: 1,
|
||||||
header: ({ table }) => {
|
header: ({ table }) => {
|
||||||
const allRows = table.getRowModel().rows;
|
const allRows = table.getRowModel().rows;
|
||||||
const stepForBulkSelection =
|
const selectableRows = allRows.filter(getRowCanSelect);
|
||||||
selectedApprovalStep ??
|
|
||||||
allRows.find(getRowCanSelect)?.original.latest_approval.step_number;
|
|
||||||
const selectableRows = allRows.filter((row) => {
|
|
||||||
if (!getRowCanSelect(row)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stepForBulkSelection) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
row.original.latest_approval.step_number === stepForBulkSelection
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
selectableRows.length > 0 &&
|
selectableRows.length > 0 &&
|
||||||
@@ -697,7 +378,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'so_number',
|
accessorKey: 'so_do_number',
|
||||||
header: 'No. Order',
|
header: 'No. Order',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
return props.row.original.do_number
|
return props.row.original.do_number
|
||||||
@@ -713,7 +394,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'approval.step_name',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -748,12 +429,10 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer',
|
accessorKey: 'customer.name',
|
||||||
header: 'Customer',
|
header: 'Customer',
|
||||||
cell: (props) => props.row.original.customer.name,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'grand_total',
|
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.sales_order
|
row.sales_order
|
||||||
?.map((product) => product.total_price)
|
?.map((product) => product.total_price)
|
||||||
@@ -770,7 +449,6 @@ const MarketingTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'marketing_products.length',
|
accessorKey: 'marketing_products.length',
|
||||||
header: 'Product Details',
|
header: 'Product Details',
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
if (props?.row?.original?.sales_order?.length) {
|
if (props?.row?.original?.sales_order?.length) {
|
||||||
if (props?.row?.original?.sales_order?.length > 1) {
|
if (props?.row?.original?.sales_order?.length > 1) {
|
||||||
@@ -792,14 +470,6 @@ const MarketingTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
header: 'Tanggal Dibuat',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.created_at
|
|
||||||
? formatDate(props.row.original.created_at, 'DD MMM yyyy')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
maxSize: 80,
|
maxSize: 80,
|
||||||
@@ -834,13 +504,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [
|
}, []);
|
||||||
deleteModal,
|
|
||||||
deliveryModal,
|
|
||||||
getRowCanSelect,
|
|
||||||
productsClickHandler,
|
|
||||||
selectedApprovalStep,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -863,7 +527,7 @@ const MarketingTable = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
{idsToProcess.length > 0 && (
|
{idsToProcess.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px' />
|
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px'></div>
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
@@ -877,7 +541,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Reject ({idsToProcess.length} Item)
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
@@ -893,7 +557,7 @@ const MarketingTable = () => {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Approve ({idsToProcess.length} Item)
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
@@ -902,18 +566,7 @@ const MarketingTable = () => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={[
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
'page',
|
|
||||||
'pageSize',
|
|
||||||
'search',
|
|
||||||
'product_names',
|
|
||||||
'status_name',
|
|
||||||
'customer_name',
|
|
||||||
'project_flock_name',
|
|
||||||
'project_flock_kandang_name',
|
|
||||||
'sort_by',
|
|
||||||
'order_by',
|
|
||||||
]}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
}}
|
}}
|
||||||
@@ -959,17 +612,7 @@ const MarketingTable = () => {
|
|||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Ekspor ke Excel
|
Export to Excel
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={exportProgressInputToExcelClickHandler}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Ekspor Input Progress (Excel)
|
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -1003,9 +646,6 @@ const MarketingTable = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pageSize={tableFilterState.pageSize}
|
pageSize={tableFilterState.pageSize}
|
||||||
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
||||||
sorting={sorting}
|
|
||||||
setSorting={handleSortingChange}
|
|
||||||
manualSorting
|
|
||||||
totalItems={
|
totalItems={
|
||||||
isResponseSuccess(marketing)
|
isResponseSuccess(marketing)
|
||||||
? marketing?.meta?.total_results
|
? marketing?.meta?.total_results
|
||||||
@@ -1037,16 +677,14 @@ const MarketingTable = () => {
|
|||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={confirmationModal.ref}
|
ref={confirmationModal.ref}
|
||||||
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
||||||
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan tahap ${selectedApprovalStep ?? '-'} (${idsToProcess.length} data)?`}
|
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${idsToProcess.length} data)?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
isLoading: isApproveLoading,
|
|
||||||
onClick: confirmationModal.closeModal,
|
onClick: confirmationModal.closeModal,
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
||||||
isLoading: isApproveLoading,
|
|
||||||
onClick: approveMarketingHandler,
|
onClick: approveMarketingHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1070,169 +708,14 @@ const MarketingTable = () => {
|
|||||||
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
isLoading: isDeliveryLoading,
|
|
||||||
}}
|
}}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
text: 'Ya',
|
text: 'Ya',
|
||||||
color: 'success',
|
color: 'success',
|
||||||
isLoading: isDeliveryLoading,
|
|
||||||
onClick: confirmationModalDeliveryClickHandler,
|
onClick: confirmationModalDeliveryClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
|
||||||
ref={bulkDeliveryModal.ref}
|
|
||||||
className={{
|
|
||||||
modalBox: 'max-w-lg rounded-lg p-0',
|
|
||||||
}}
|
|
||||||
closeOnBackdrop
|
|
||||||
>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
|
||||||
<h4 className='text-sm font-semibold text-base-content'>
|
|
||||||
Bulk Approve Delivery
|
|
||||||
</h4>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
bulkDeliveryModal.closeModal();
|
|
||||||
setBulkDeliveryDate('');
|
|
||||||
setBulkDeliveryNotes('');
|
|
||||||
}}
|
|
||||||
className='p-1'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:close' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
|
||||||
<p className='text-sm text-base-content/70'>
|
|
||||||
Pilih tanggal pengiriman untuk approve {idsToProcess.length} data
|
|
||||||
penjualan tahap 2.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<DateInput
|
|
||||||
name='bulk_delivery_date'
|
|
||||||
label='Tanggal Pengiriman'
|
|
||||||
value={bulkDeliveryDate}
|
|
||||||
onChange={bulkDeliveryDateChangeHandler}
|
|
||||||
isNestedModal
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextArea
|
|
||||||
name='bulk_delivery_notes'
|
|
||||||
label='Catatan'
|
|
||||||
placeholder='Masukkan catatan approval...'
|
|
||||||
value={bulkDeliveryNotes}
|
|
||||||
onChange={bulkDeliveryNotesChangeHandler}
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
disabled={isSubmittingBulkDelivery}
|
|
||||||
onClick={() => {
|
|
||||||
bulkDeliveryModal.closeModal();
|
|
||||||
setBulkDeliveryDate('');
|
|
||||||
setBulkDeliveryNotes('');
|
|
||||||
}}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color='success'
|
|
||||||
isLoading={isSubmittingBulkDelivery}
|
|
||||||
disabled={isSubmittingBulkDelivery}
|
|
||||||
onClick={() =>
|
|
||||||
submitBulkDeliveryApprovalHandler(
|
|
||||||
idsToProcess,
|
|
||||||
bulkDeliveryDate,
|
|
||||||
bulkDeliveryNotes
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
|
||||||
ref={exportProgressInputModal.ref}
|
|
||||||
className={{
|
|
||||||
modalBox: 'max-w-lg rounded-lg p-0',
|
|
||||||
}}
|
|
||||||
closeOnBackdrop
|
|
||||||
>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
|
||||||
<h4 className='text-sm font-semibold text-base-content'>
|
|
||||||
Ekspor Input Progress
|
|
||||||
</h4>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
}}
|
|
||||||
className='p-1'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:close' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
|
||||||
<DateInput
|
|
||||||
name='export_progress_start_date'
|
|
||||||
label='Tanggal Mulai'
|
|
||||||
value={exportProgressStartDate}
|
|
||||||
onChange={exportProgressStartDateChangeHandler}
|
|
||||||
isNestedModal
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DateInput
|
|
||||||
name='export_progress_end_date'
|
|
||||||
label='Tanggal Selesai'
|
|
||||||
value={exportProgressEndDate}
|
|
||||||
onChange={exportProgressEndDateChangeHandler}
|
|
||||||
isNestedModal
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
}}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color='success'
|
|
||||||
onClick={submitExportProgressInputHandler}
|
|
||||||
isLoading={isExportProgressLoading}
|
|
||||||
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
ref={productsModal.ref}
|
ref={productsModal.ref}
|
||||||
className={{
|
className={{
|
||||||
@@ -1294,7 +777,6 @@ const MarketingTable = () => {
|
|||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
initialValues={marketingFilterInitialValues}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ const SalesOrderFormModal = ({
|
|||||||
})
|
})
|
||||||
.filter((item) => Boolean(item)),
|
.filter((item) => Boolean(item)),
|
||||||
} as UpdateDeliveryOrderPayload);
|
} as UpdateDeliveryOrderPayload);
|
||||||
|
|
||||||
switch (modalAction) {
|
switch (modalAction) {
|
||||||
case 'add':
|
case 'add':
|
||||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||||
@@ -262,7 +261,11 @@ const SalesOrderFormModal = ({
|
|||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
const { formErrorList, setFormErrorList, close, handleFormSubmit } =
|
||||||
useFormikErrorList(formik);
|
useFormikErrorList(formik, {
|
||||||
|
onAfterSubmit: () => {
|
||||||
|
router.push('/marketing');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ================== FORM REPEATER HANDLER ==================
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ export const MarketingFilterSchema = object({
|
|||||||
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
||||||
status: mixed<OptionType<string>>().nullable(),
|
status: mixed<OptionType<string>>().nullable(),
|
||||||
customer: mixed<OptionType<number>>().nullable(),
|
customer: mixed<OptionType<number>>().nullable(),
|
||||||
project_flock: mixed<OptionType<number>>().nullable(),
|
|
||||||
project_flock_kandang: mixed<OptionType<number>>().nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MarketingFilterFormValues = {
|
export type MarketingFilterFormValues = {
|
||||||
product_ids: OptionType<number>[];
|
product_ids: OptionType<number>[];
|
||||||
status: OptionType<string> | null;
|
status: OptionType<string> | null;
|
||||||
customer: OptionType<number> | null;
|
customer: OptionType<number> | null;
|
||||||
project_flock: OptionType<number> | null;
|
|
||||||
project_flock_kandang: OptionType<number> | null;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
|||||||
.required('Pengiriman wajib diisi!')
|
.required('Pengiriman wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
'at-least-one-valid-row',
|
'at-least-one-valid-row',
|
||||||
'Seluruh data pengiriman harus diisi lengkap!',
|
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
|
||||||
function (items) {
|
function (items) {
|
||||||
if (!items || items.length === 0) return false;
|
if (!items || items.length === 0) return false;
|
||||||
|
|
||||||
// VALIDASI: seluruh item harus valid full
|
// VALIDASI: minimal 1 item valid full
|
||||||
const itemSchema = DeliveryOrderProductSchema;
|
const itemSchema = DeliveryOrderProductSchema;
|
||||||
|
|
||||||
const hasValidItem = items.every((item) => {
|
const hasValidItem = items.some((item) => {
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
return itemSchema.isValidSync(item, { abortEarly: true });
|
return itemSchema.isValidSync(item, { abortEarly: true });
|
||||||
});
|
});
|
||||||
@@ -123,17 +123,8 @@ export const SalesProductToFieldValues = (
|
|||||||
total_price: product.total_price,
|
total_price: product.total_price,
|
||||||
marketing_type: product.marketing_type
|
marketing_type: product.marketing_type
|
||||||
? {
|
? {
|
||||||
value:
|
value: product.marketing_type,
|
||||||
product.marketing_type === 'AYAM' ||
|
label: formatTitleCase(product.marketing_type),
|
||||||
product.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM,AYAM_PULLET'
|
|
||||||
: product.marketing_type,
|
|
||||||
label: formatTitleCase(
|
|
||||||
product.marketing_type === 'AYAM' ||
|
|
||||||
product.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM'
|
|
||||||
: product.marketing_type
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: product.convertion_unit
|
convertion_unit: product.convertion_unit
|
||||||
@@ -153,9 +144,7 @@ export const DeliveryProductToFieldValues = (
|
|||||||
delivery: BaseDeliveryOrder
|
delivery: BaseDeliveryOrder
|
||||||
): DeliveryOrderProductFormValues[] => {
|
): DeliveryOrderProductFormValues[] => {
|
||||||
const data = delivery.deliveries.map((item) => {
|
const data = delivery.deliveries.map((item) => {
|
||||||
const salesOrder =
|
const salesOrder = salesOrders.find(
|
||||||
salesOrders.find((so) => so.id === item.marketing_product_id) ??
|
|
||||||
salesOrders.find(
|
|
||||||
(so) => so.product_warehouse.id === item.product_warehouse.id
|
(so) => so.product_warehouse.id === item.product_warehouse.id
|
||||||
);
|
);
|
||||||
const warehouseOption = {
|
const warehouseOption = {
|
||||||
@@ -191,20 +180,11 @@ export const DeliveryProductToFieldValues = (
|
|||||||
vehicle_number: item.vehicle_number,
|
vehicle_number: item.vehicle_number,
|
||||||
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
||||||
do_number: delivery.do_number,
|
do_number: delivery.do_number,
|
||||||
marketing_product_id: item.marketing_product_id ?? salesOrder?.id,
|
marketing_product_id: salesOrder?.id,
|
||||||
marketing_type: salesOrder?.marketing_type
|
marketing_type: salesOrder?.marketing_type
|
||||||
? {
|
? {
|
||||||
value:
|
value: salesOrder?.marketing_type,
|
||||||
salesOrder?.marketing_type === 'AYAM' ||
|
label: formatTitleCase(salesOrder?.marketing_type),
|
||||||
salesOrder?.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM,AYAM_PULLET'
|
|
||||||
: salesOrder?.marketing_type,
|
|
||||||
label: formatTitleCase(
|
|
||||||
salesOrder?.marketing_type === 'AYAM' ||
|
|
||||||
salesOrder?.marketing_type === 'AYAM_PULLET'
|
|
||||||
? 'AYAM'
|
|
||||||
: salesOrder?.marketing_type
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
convertion_unit: salesOrder?.convertion_unit
|
convertion_unit: salesOrder?.convertion_unit
|
||||||
@@ -214,7 +194,7 @@ export const DeliveryProductToFieldValues = (
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
marketing_product: {
|
marketing_product: {
|
||||||
id: item.marketing_product_id ?? salesOrder?.id,
|
id: salesOrder?.id,
|
||||||
vehicle_number: item.vehicle_number,
|
vehicle_number: item.vehicle_number,
|
||||||
warehouse_id: item.product_warehouse.warehouse.id,
|
warehouse_id: item.product_warehouse.warehouse.id,
|
||||||
warehouse: warehouseOption,
|
warehouse: warehouseOption,
|
||||||
|
|||||||
+19
-19
@@ -146,6 +146,15 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ============ Fetch Data ============
|
// ============ Fetch Data ============
|
||||||
|
const { data: productData } = useSWR(
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.basePath + '/' + selectedProduct?.value
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.getSingle(Number(selectedProduct?.value))
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
// Options Week dari minggu 1 - 22
|
// Options Week dari minggu 1 - 22
|
||||||
// const optionsWeek = useMemo(() => {
|
// const optionsWeek = useMemo(() => {
|
||||||
@@ -181,15 +190,8 @@ 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.marketing_product_id === marketingProductId
|
|
||||||
) ??
|
|
||||||
doItem.deliveries.find(
|
|
||||||
(d) =>
|
(d) =>
|
||||||
d.product_warehouse.id ===
|
d.product_warehouse.id ===
|
||||||
initialValues?.marketing_product?.product_warehouse_id
|
initialValues?.marketing_product?.product_warehouse_id
|
||||||
@@ -401,10 +403,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
if (
|
if (!Boolean(initialValues.qty)) {
|
||||||
!Boolean(initialValues.qty) &&
|
|
||||||
!Boolean(initialValues.marketing_product_id)
|
|
||||||
) {
|
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
} else {
|
} else {
|
||||||
setFormikValues({
|
setFormikValues({
|
||||||
@@ -414,7 +413,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
});
|
});
|
||||||
if (initialValues?.marketing_product_id) {
|
if (initialValues?.marketing_product_id) {
|
||||||
setSelectedProduct({
|
setSelectedProduct({
|
||||||
value: initialValues?.marketing_product_id,
|
value: initialValues?.id,
|
||||||
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
|
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`,
|
||||||
} as OptionType);
|
} as OptionType);
|
||||||
}
|
}
|
||||||
@@ -431,8 +430,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
handleBlurField(currentInput);
|
handleBlurField(currentInput);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'uom',
|
'uom',
|
||||||
initialValues?.marketing_product?.product_warehouse_data?.product?.uom
|
isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
|
||||||
?.name ?? ''
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -805,8 +803,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
endAdornment={
|
endAdornment={
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<span className='text-sm text-gray-500'>
|
<span className='text-sm text-gray-500'>
|
||||||
{initialValues?.marketing_product?.product_warehouse_data
|
{isResponseSuccess(productData)
|
||||||
?.product?.uom?.name ?? ''}
|
? productData?.data?.uom.name
|
||||||
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -817,8 +816,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty +
|
)?.qty +
|
||||||
' ' +
|
' ' +
|
||||||
(initialValues?.marketing_product?.product_warehouse_data
|
(isResponseSuccess(productData)
|
||||||
?.product?.uom?.name ?? '')
|
? productData?.data?.uom.name
|
||||||
|
: '')
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -252,11 +252,6 @@ const SalesOrderProductForm = ({
|
|||||||
setSelectedProductWarehouse(productWarehouse || null);
|
setSelectedProductWarehouse(productWarehouse || null);
|
||||||
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
formik.setFieldValue('product_warehouse_data', productWarehouse || null);
|
||||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||||
|
|
||||||
if (productWarehouse?.quantity) {
|
|
||||||
handleFieldChange('qty', productWarehouse?.quantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
||||||
if (
|
if (
|
||||||
productWarehouse?.week !== undefined &&
|
productWarehouse?.week !== undefined &&
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Qty</td>
|
<td className='text-sm px-4 py-3'>Qty</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.qty !== undefined && item.qty !== null && item.qty !== ''
|
{item.qty
|
||||||
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
@@ -273,7 +273,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Qty</td>
|
<td className='text-sm px-4 py-3'>Qty</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.qty !== undefined && item.qty !== null && item.qty !== ''
|
{item.qty
|
||||||
? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
|
? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,6 +20,8 @@ import { Area } from '@/types/api/master-data/area';
|
|||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AreasTable = () => {
|
const AreasTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const AreasTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'areas-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -134,8 +137,17 @@ const AreasTable = () => {
|
|||||||
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
|
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('areas-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,6 +20,8 @@ import { Bank } from '@/types/api/master-data/bank';
|
|||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BanksTable = () => {
|
const BanksTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const BanksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'banks-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -134,8 +137,17 @@ const BanksTable = () => {
|
|||||||
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
|
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('banks-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,6 +20,8 @@ import { Customer } from '@/types/api/master-data/customer';
|
|||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CustomersTable = () => {
|
const CustomersTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const CustomersTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'customers-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -136,8 +139,17 @@ const CustomersTable = () => {
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('customers-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -189,11 +201,6 @@ const CustomersTable = () => {
|
|||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
header: 'Email',
|
header: 'Email',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'bank_name',
|
|
||||||
header: 'Nama Bank',
|
|
||||||
cell: (props) => props.row.original.bank_name || '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props: CellContext<Customer, unknown>) => {
|
cell: (props: CellContext<Customer, unknown>) => {
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export const CustomerFormSchema = Yup.object({
|
|||||||
.email('Format email tidak valid!')
|
.email('Format email tidak valid!')
|
||||||
.required('Email wajib diisi!'),
|
.required('Email wajib diisi!'),
|
||||||
|
|
||||||
bank_name: Yup.string()
|
|
||||||
.min(3, 'Nama bank minimal 3 karakter!')
|
|
||||||
.required('Nama bank wajib diisi!'),
|
|
||||||
account_number: Yup.string()
|
account_number: Yup.string()
|
||||||
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
.matches(/^[0-9]+$/, 'Nomor rekening hanya boleh berisi angka!')
|
||||||
.required('Nomor rekening wajib diisi!'),
|
.required('Nomor rekening wajib diisi!'),
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ const CustomerForm = ({
|
|||||||
},
|
},
|
||||||
type: normalizeType(initialValues?.type),
|
type: normalizeType(initialValues?.type),
|
||||||
address: initialValues?.address ?? '',
|
address: initialValues?.address ?? '',
|
||||||
bank_name: initialValues?.bank_name ?? '',
|
|
||||||
account_number: initialValues?.account_number ?? '',
|
account_number: initialValues?.account_number ?? '',
|
||||||
};
|
};
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
@@ -165,7 +164,6 @@ const CustomerForm = ({
|
|||||||
pic_id: values.picId,
|
pic_id: values.picId,
|
||||||
type: (values.type as OptionType).value as string,
|
type: (values.type as OptionType).value as string,
|
||||||
address: values.address,
|
address: values.address,
|
||||||
bank_name: values.bank_name,
|
|
||||||
account_number: values.account_number,
|
account_number: values.account_number,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,22 +286,6 @@ const CustomerForm = ({
|
|||||||
errorMessage={formik.errors.phone}
|
errorMessage={formik.errors.phone}
|
||||||
readOnly={formType === 'detail'}
|
readOnly={formType === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label='Nama Bank'
|
|
||||||
name='bank_name'
|
|
||||||
placeholder='Masukkan nama bank customer'
|
|
||||||
value={formik.values.bank_name}
|
|
||||||
onChange={(e) =>
|
|
||||||
formik.setFieldValue('bank_name', e.target.value.toUpperCase())
|
|
||||||
}
|
|
||||||
onBlur={formik.handleBlur}
|
|
||||||
isError={
|
|
||||||
formik.touched.bank_name && Boolean(formik.errors.bank_name)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.bank_name}
|
|
||||||
readOnly={formType === 'detail'}
|
|
||||||
/>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label='Nomor Rekening'
|
label='Nomor Rekening'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,6 +20,8 @@ import { Flock } from '@/types/api/master-data/flock';
|
|||||||
import { FlockApi } from '@/services/api/master-data';
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FlockTable = () => {
|
const FlockTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,14 +114,12 @@ const FlockTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'flock-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -136,8 +139,17 @@ const FlockTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('flocks-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -28,6 +35,7 @@ import { User } from '@/types/api/api-general';
|
|||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import {
|
import {
|
||||||
KandangFilterSchema,
|
KandangFilterSchema,
|
||||||
KandangFilterType,
|
KandangFilterType,
|
||||||
@@ -114,21 +122,20 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const KandangsTable = () => {
|
const KandangsTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<{
|
} = useTableFilter({
|
||||||
search: string;
|
|
||||||
locationFilter?: OptionType<string>;
|
|
||||||
picFilter?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
locationFilter: undefined,
|
locationFilter: '',
|
||||||
picFilter: undefined,
|
picFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -136,8 +143,6 @@ const KandangsTable = () => {
|
|||||||
locationFilter: 'location_id',
|
locationFilter: 'location_id',
|
||||||
picFilter: 'pic_id',
|
picFilter: 'pic_id',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'kandangs-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -146,34 +151,22 @@ const KandangsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<KandangFilterType>({
|
const formik = useFormik<KandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
location: tableFilterState.locationFilter,
|
location_id: null,
|
||||||
pic: tableFilterState.picFilter,
|
pic_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: KandangFilterSchema,
|
validationSchema: KandangFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('locationFilter', values.location || undefined, true);
|
updateFilter('locationFilter', values.location_id || '');
|
||||||
updateFilter('picFilter', values.pic || undefined, true);
|
updateFilter('picFilter', values.pic_id || '');
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
});
|
onReset: () => {
|
||||||
|
updateFilter('locationFilter', '');
|
||||||
const formikResetHandler = () => {
|
updateFilter('picFilter', '');
|
||||||
updateFilter('locationFilter', undefined, true);
|
|
||||||
updateFilter('picFilter', undefined, true);
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
location: undefined,
|
|
||||||
pic: undefined,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const { setFieldValue } = formik;
|
|
||||||
|
|
||||||
// ===== LOCATION OPTIONS =====
|
// ===== LOCATION OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
@@ -201,15 +194,43 @@ const KandangsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterLocationChange = (
|
const handleFilterLocationChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const location = val as OptionType | null;
|
||||||
setFieldValue('location', val);
|
const locationId = location?.value ? String(location.value) : null;
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterPicChange = (val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('location_id', locationId);
|
||||||
setFieldValue('pic', val);
|
},
|
||||||
};
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterPicChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const pic = val as OptionType | null;
|
||||||
|
const picId = pic?.value ? String(pic.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('pic_id', picId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const locationIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.location_id) return null;
|
||||||
|
return (
|
||||||
|
locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.location_id, locationOptions]);
|
||||||
|
|
||||||
|
const picIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.pic_id) return null;
|
||||||
|
return (
|
||||||
|
picOptions.find((opt) => String(opt.value) === formik.values.pic_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.pic_id, picOptions]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -234,8 +255,17 @@ const KandangsTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('kandangs-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -445,13 +475,13 @@ const KandangsTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={formik.values.location}
|
value={locationIdValue}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
@@ -464,7 +494,7 @@ const KandangsTable = () => {
|
|||||||
label='PIC'
|
label='PIC'
|
||||||
placeholder='Pilih PIC'
|
placeholder='Pilih PIC'
|
||||||
options={picOptions}
|
options={picOptions}
|
||||||
value={formik.values.pic}
|
value={picIdValue}
|
||||||
onChange={handleFilterPicChange}
|
onChange={handleFilterPicChange}
|
||||||
onInputChange={setPicInputValue}
|
onInputChange={setPicInputValue}
|
||||||
isLoading={isLoadingPicOptions}
|
isLoading={isLoadingPicOptions}
|
||||||
@@ -480,14 +510,17 @@ const KandangsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={formikResetHandler}
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={!formik.isValid}
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const KandangFilterSchema = Yup.object().shape({
|
export const KandangFilterSchema = object().shape({
|
||||||
location: Yup.object({
|
location_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
pic_id: string().nullable(),
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
|
|
||||||
pic: Yup.object({
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type KandangFilterType = {
|
export type KandangFilterType = {
|
||||||
location?: OptionType<string>;
|
location_id: string | null;
|
||||||
pic?: OptionType<string>;
|
pic_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -25,6 +32,7 @@ import { Area } from '@/types/api/master-data/area';
|
|||||||
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import {
|
import {
|
||||||
LocationFilterSchema,
|
LocationFilterSchema,
|
||||||
LocationFilterType,
|
LocationFilterType,
|
||||||
@@ -110,27 +118,25 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LocationsTable = () => {
|
const LocationsTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<{
|
} = useTableFilter({
|
||||||
search: string;
|
|
||||||
areaFilter?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
areaFilter: undefined,
|
areaFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
areaFilter: 'area_id',
|
areaFilter: 'area_id',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'locations-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -139,28 +145,19 @@ const LocationsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<LocationFilterType>({
|
const formik = useFormik<LocationFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area: tableFilterState.areaFilter,
|
area_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: LocationFilterSchema,
|
validationSchema: LocationFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area || undefined, true);
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
});
|
onReset: () => {
|
||||||
|
updateFilter('areaFilter', '');
|
||||||
const formikResetHandler = () => {
|
|
||||||
updateFilter('areaFilter', undefined, true);
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
area: undefined,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== AREA OPTIONS =====
|
// ===== AREA OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setAreaInputValue,
|
setInputValue: setAreaInputValue,
|
||||||
@@ -175,9 +172,24 @@ const LocationsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterAreaChange = useCallback(
|
||||||
formik.setFieldValue('area', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
};
|
const area = val as OptionType | null;
|
||||||
|
const areaId = area?.value ? String(area.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('area_id', areaId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const areaIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.area_id) return null;
|
||||||
|
return (
|
||||||
|
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.area_id, areaOptions]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -200,10 +212,19 @@ const LocationsTable = () => {
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('locations-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -404,13 +425,13 @@ const LocationsTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={formik.values.area}
|
value={areaIdValue}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -426,7 +447,10 @@ const LocationsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={formikResetHandler}
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const LocationFilterSchema = Yup.object().shape({
|
export const LocationFilterSchema = object().shape({
|
||||||
area: Yup.object({
|
area_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LocationFilterType = {
|
export type LocationFilterType = {
|
||||||
area?: OptionType<string>;
|
area_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
|
|||||||
import { NonstockApi } from '@/services/api/master-data';
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NonstocksTable = () => {
|
const NonstocksTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -109,16 +114,22 @@ const NonstocksTable = () => {
|
|||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: searchValue,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'nonstock-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('nonstocks-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -137,7 +148,8 @@ const NonstocksTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -20,6 +21,7 @@ import { ProductCategory } from '@/types/api/master-data/product-category';
|
|||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProductCategoryTable = () => {
|
const ProductCategoryTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -115,10 +120,12 @@ const ProductCategoryTable = () => {
|
|||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'product-category-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -137,7 +144,8 @@ const ProductCategoryTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -206,6 +214,10 @@ const ProductCategoryTable = () => {
|
|||||||
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('product-category-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -26,6 +33,7 @@ import { ProductApi, ProductCategoryApi } from '@/services/api/master-data';
|
|||||||
import { formatCurrency } from '@/lib/helper';
|
import { formatCurrency } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import {
|
import {
|
||||||
ProductFilterSchema,
|
ProductFilterSchema,
|
||||||
ProductFilterType,
|
ProductFilterType,
|
||||||
@@ -111,27 +119,25 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProductsTable = () => {
|
const ProductsTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<{
|
} = useTableFilter({
|
||||||
search: string;
|
|
||||||
productCategoryFilter?: OptionType<string>;
|
|
||||||
}>({
|
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
productCategoryFilter: undefined,
|
productCategoryFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
productCategoryFilter: 'product_category_id',
|
productCategoryFilter: 'product_category_id',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'product-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -140,32 +146,19 @@ const ProductsTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<ProductFilterType>({
|
const formik = useFormik<ProductFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
product_category: tableFilterState.productCategoryFilter,
|
product_category_id: null,
|
||||||
},
|
},
|
||||||
validationSchema: ProductFilterSchema,
|
validationSchema: ProductFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter(
|
updateFilter('productCategoryFilter', values.product_category_id || '');
|
||||||
'productCategoryFilter',
|
|
||||||
values.product_category || undefined,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
});
|
onReset: () => {
|
||||||
|
updateFilter('productCategoryFilter', '');
|
||||||
const formikResetHandler = () => {
|
|
||||||
updateFilter('productCategoryFilter', undefined, true);
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
product_category: undefined,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== PRODUCT CATEGORY OPTIONS =====
|
// ===== PRODUCT CATEGORY OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setProductCategoryInputValue,
|
setInputValue: setProductCategoryInputValue,
|
||||||
@@ -180,11 +173,25 @@ const ProductsTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterProductCategoryChange = (
|
const handleFilterProductCategoryChange = useCallback(
|
||||||
val: OptionType | OptionType[] | null
|
(val: OptionType | OptionType[] | null) => {
|
||||||
) => {
|
const category = val as OptionType | null;
|
||||||
formik.setFieldValue('product_category', val);
|
const categoryId = category?.value ? String(category.value) : null;
|
||||||
};
|
|
||||||
|
formik.setFieldValue('product_category_id', categoryId);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const productCategoryIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.product_category_id) return null;
|
||||||
|
return (
|
||||||
|
productCategoryOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.product_category_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.product_category_id, productCategoryOptions]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -192,6 +199,10 @@ const ProductsTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -209,8 +220,13 @@ const ProductsTable = () => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('product-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -461,13 +477,13 @@ const ProductsTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Kategori Produk'
|
label='Kategori Produk'
|
||||||
placeholder='Pilih Kategori Produk'
|
placeholder='Pilih Kategori Produk'
|
||||||
options={productCategoryOptions}
|
options={productCategoryOptions}
|
||||||
value={formik.values.product_category}
|
value={productCategoryIdValue}
|
||||||
onChange={handleFilterProductCategoryChange}
|
onChange={handleFilterProductCategoryChange}
|
||||||
onInputChange={setProductCategoryInputValue}
|
onInputChange={setProductCategoryInputValue}
|
||||||
isLoading={isLoadingProductCategoryOptions}
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
@@ -483,7 +499,10 @@ const ProductsTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={formikResetHandler}
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const ProductFilterSchema = Yup.object().shape({
|
export const ProductFilterSchema = object().shape({
|
||||||
product_category: Yup.object({
|
product_category_id: string().nullable(),
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProductFilterType = {
|
export type ProductFilterType = {
|
||||||
product_category?: OptionType<string>;
|
product_category_id: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,44 +128,27 @@ const ProductionStandardTable = () => {
|
|||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
projectCategoryFilter: 'project_category',
|
projectCategoryFilter: 'project_category',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'production-standard-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
// ===== FILTER INITIAL VALUES (derived from persisted state) =====
|
|
||||||
const filterInitialValues = useMemo<ProductionStandardFilterType>(
|
|
||||||
() => ({
|
|
||||||
project_category: tableFilterState.projectCategoryFilter || null,
|
|
||||||
}),
|
|
||||||
[tableFilterState.projectCategoryFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<ProductionStandardFilterType>({
|
const formik = useFormik<ProductionStandardFilterType>({
|
||||||
initialValues: filterInitialValues,
|
initialValues: {
|
||||||
|
project_category: null,
|
||||||
|
},
|
||||||
validationSchema: ProductionStandardFilterSchema,
|
validationSchema: ProductionStandardFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('projectCategoryFilter', values.project_category || '');
|
updateFilter('projectCategoryFilter', values.project_category || '');
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
});
|
onReset: () => {
|
||||||
|
updateFilter('projectCategoryFilter', '');
|
||||||
const formikResetHandler = () => {
|
|
||||||
updateFilter('projectCategoryFilter', '', true);
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
project_category: null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
||||||
const projectCategoryOptions = useMemo(
|
const projectCategoryOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -398,7 +381,7 @@ const ProductionStandardTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Kategori'
|
label='Kategori'
|
||||||
@@ -414,9 +397,13 @@ const ProductionStandardTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -29,7 +30,7 @@ import { Supplier } from '@/types/api/master-data/supplier';
|
|||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import {
|
import {
|
||||||
SupplierFilterSchema,
|
SupplierFilterSchema,
|
||||||
SupplierFilterType,
|
SupplierFilterType,
|
||||||
@@ -116,21 +117,20 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SuppliersTable = () => {
|
const SuppliersTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<{
|
} = useTableFilter({
|
||||||
search: string;
|
|
||||||
categoryFilter?: OptionType<string>;
|
|
||||||
flagFilter?: string;
|
|
||||||
}>({
|
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
categoryFilter: undefined,
|
categoryFilter: '',
|
||||||
flagFilter: undefined,
|
flagFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -138,8 +138,6 @@ const SuppliersTable = () => {
|
|||||||
categoryFilter: 'category_id',
|
categoryFilter: 'category_id',
|
||||||
flagFilter: 'flag',
|
flagFilter: 'flag',
|
||||||
},
|
},
|
||||||
persist: true,
|
|
||||||
storeName: 'supplier-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
@@ -148,33 +146,26 @@ const SuppliersTable = () => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<SupplierFilterType>({
|
const formik = useFormik<SupplierFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
category: tableFilterState.categoryFilter,
|
category_id: null,
|
||||||
flag: tableFilterState.flagFilter === 'EKSPEDISI',
|
flag: false,
|
||||||
},
|
},
|
||||||
validationSchema: SupplierFilterSchema,
|
validationSchema: SupplierFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('categoryFilter', values.category || undefined, true);
|
updateFilter('categoryFilter', values.category_id || '');
|
||||||
updateFilter('flagFilter', values.flag === true ? 'EKSPEDISI' : '', true);
|
updateFilter(
|
||||||
|
'flagFilter',
|
||||||
|
values.flag === true ? 'EKSPEDISI' : values.flag === false ? '' : ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
});
|
onReset: () => {
|
||||||
|
updateFilter('categoryFilter', '');
|
||||||
const formikResetHandler = () => {
|
updateFilter('flagFilter', '');
|
||||||
updateFilter('categoryFilter', undefined, true);
|
formik.setFieldValue('flag', false);
|
||||||
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', '');
|
||||||
const formikResetHandler = () => {
|
updateFilter('activeProjectFlockFilter', '');
|
||||||
updateFilter('areaFilter', '', true);
|
formik.setFieldValue('active_project_flock', false);
|
||||||
updateFilter('activeProjectFlockFilter', '', true);
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
area_id: null,
|
|
||||||
active_project_flock: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
filterModal.closeModal();
|
const { setFieldValue } = formik;
|
||||||
};
|
|
||||||
|
|
||||||
// ===== AREA OPTIONS =====
|
// ===== AREA OPTIONS =====
|
||||||
const {
|
const {
|
||||||
@@ -243,6 +243,26 @@ const WarehousesTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filterModal.open) {
|
||||||
|
const activeProjectFlockValue =
|
||||||
|
tableFilterState.activeProjectFlockFilter === 'true' ? true : false; // Default ke false (Semua Kandang)
|
||||||
|
setFieldValue('active_project_flock', activeProjectFlockValue);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
filterModal.open,
|
||||||
|
tableFilterState.activeProjectFlockFilter,
|
||||||
|
setFieldValue,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('warehouses-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -261,7 +281,8 @@ const WarehousesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -486,7 +507,7 @@ const WarehousesTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
@@ -517,7 +538,10 @@ const WarehousesTable = () => {
|
|||||||
type='button'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={formikResetHandler}
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useModal } from '@/components/Modal';
|
|||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import Dropdown from '@/components/Dropdown';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
@@ -22,6 +23,7 @@ import { Icon } from '@iconify/react';
|
|||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -43,7 +45,6 @@ import {
|
|||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import NumberInput from '@/components/input/NumberInput';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -147,6 +148,7 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
||||||
@@ -172,9 +174,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
kandang_id: '',
|
kandang_id: '',
|
||||||
category: '',
|
category: '',
|
||||||
period: '',
|
period: '',
|
||||||
area_name: '',
|
|
||||||
location_name: '',
|
|
||||||
kandang_name: '',
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -186,11 +185,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
category: 'category',
|
category: 'category',
|
||||||
period: 'period',
|
period: 'period',
|
||||||
},
|
},
|
||||||
excludeKeysFromUrl: ['area_name', 'location_name', 'kandang_name'],
|
|
||||||
persist: true,
|
|
||||||
storeName: 'project-flock-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// ===== State =====
|
// ===== State =====
|
||||||
@@ -211,7 +206,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
|
useState(false);
|
||||||
const {
|
const {
|
||||||
isChickinApproveModalOpen,
|
isChickinApproveModalOpen,
|
||||||
isChickinApproveLoading,
|
isChickinApproveLoading,
|
||||||
@@ -261,18 +257,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
updateFilter('kandang_id', values.kandang_id || '');
|
updateFilter('kandang_id', values.kandang_id || '');
|
||||||
updateFilter('category', values.category || '');
|
updateFilter('category', values.category || '');
|
||||||
updateFilter('period', values.period || '');
|
updateFilter('period', values.period || '');
|
||||||
updateFilter(
|
|
||||||
'area_name',
|
|
||||||
areaValue?.label ? String(areaValue.label) : ''
|
|
||||||
);
|
|
||||||
updateFilter(
|
|
||||||
'location_name',
|
|
||||||
locationValue?.label ? String(locationValue.label) : ''
|
|
||||||
);
|
|
||||||
updateFilter(
|
|
||||||
'kandang_name',
|
|
||||||
kandangValue?.label ? String(kandangValue.label) : ''
|
|
||||||
);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
@@ -282,9 +266,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
updateFilter('kandang_id', '');
|
updateFilter('kandang_id', '');
|
||||||
updateFilter('category', '');
|
updateFilter('category', '');
|
||||||
updateFilter('period', '');
|
updateFilter('period', '');
|
||||||
updateFilter('area_name', '');
|
|
||||||
updateFilter('location_name', '');
|
|
||||||
updateFilter('kandang_name', '');
|
|
||||||
setFilterAreaId(undefined);
|
setFilterAreaId(undefined);
|
||||||
setFilterLocationId(undefined);
|
setFilterLocationId(undefined);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
@@ -326,55 +307,40 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const periodOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: '1', label: 'Periode 1' },
|
||||||
|
{ value: '2', label: 'Periode 2' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
// ===== FILTER HELPERS =====
|
||||||
const areaValue = useMemo(() => {
|
const areaValue = useMemo(() => {
|
||||||
if (!formik.values.area_id) return null;
|
if (!formik.values.area_id) return null;
|
||||||
const found = areaOptions.find(
|
return (
|
||||||
(opt) => String(opt.value) === formik.values.area_id
|
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||||
|
null
|
||||||
);
|
);
|
||||||
if (found) return found;
|
}, [formik.values.area_id, areaOptions]);
|
||||||
if (tableFilterState.area_name) {
|
|
||||||
return {
|
|
||||||
value: formik.values.area_id,
|
|
||||||
label: tableFilterState.area_name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [formik.values.area_id, areaOptions, tableFilterState.area_name]);
|
|
||||||
|
|
||||||
const locationValue = useMemo(() => {
|
const locationValue = useMemo(() => {
|
||||||
if (!formik.values.location_id) return null;
|
if (!formik.values.location_id) return null;
|
||||||
const found = locationOptions.find(
|
return (
|
||||||
|
locationOptions.find(
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
(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 (
|
||||||
|
kandangOptions.find(
|
||||||
(opt) => String(opt.value) === formik.values.kandang_id
|
(opt) => String(opt.value) === formik.values.kandang_id
|
||||||
|
) || null
|
||||||
);
|
);
|
||||||
if (found) return found;
|
}, [formik.values.kandang_id, kandangOptions]);
|
||||||
if (tableFilterState.kandang_name) {
|
|
||||||
return {
|
|
||||||
value: formik.values.kandang_id,
|
|
||||||
label: tableFilterState.kandang_name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [formik.values.kandang_id, kandangOptions, tableFilterState.kandang_name]);
|
|
||||||
|
|
||||||
const categoryValue = useMemo(() => {
|
const categoryValue = useMemo(() => {
|
||||||
if (!formik.values.category) return null;
|
if (!formik.values.category) return null;
|
||||||
@@ -384,6 +350,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.category, categoryOptions]);
|
}, [formik.values.category, categoryOptions]);
|
||||||
|
|
||||||
|
const periodValue = useMemo(() => {
|
||||||
|
if (!formik.values.period) return null;
|
||||||
|
return (
|
||||||
|
periodOptions.find((opt) => opt.value === formik.values.period) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.period, periodOptions]);
|
||||||
|
|
||||||
// ===== FILTER DEPENDENCY HANDLERS =====
|
// ===== FILTER DEPENDENCY HANDLERS =====
|
||||||
const handleFilterAreaChange = (area: OptionType | null) => {
|
const handleFilterAreaChange = (area: OptionType | null) => {
|
||||||
const areaId = area?.value ? String(area.value) : undefined;
|
const areaId = area?.value ? String(area.value) : undefined;
|
||||||
@@ -452,11 +425,18 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
};
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('project-flock-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value, true);
|
setSearchValue(e.target.value);
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprovalHandler = async (
|
const confirmApprovalHandler = async (
|
||||||
notes: string,
|
notes: string,
|
||||||
approvalAction: 'APPROVED' | 'REJECTED'
|
approvalAction: 'APPROVED' | 'REJECTED'
|
||||||
@@ -574,7 +554,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
price: budget.price,
|
price: budget.price,
|
||||||
total_price: budget.qty * budget.price,
|
total_price: budget.qty * budget.price,
|
||||||
})) || [],
|
})) || [],
|
||||||
periode: createdProjectFlock.period ?? '-',
|
|
||||||
} as ProjectFlockFormValues;
|
} as ProjectFlockFormValues;
|
||||||
}, [createdProjectFlock]);
|
}, [createdProjectFlock]);
|
||||||
|
|
||||||
@@ -797,6 +776,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const exportToExcelHandler = async () => {
|
||||||
|
setIsLoadingExportingToExcel(true);
|
||||||
|
|
||||||
|
toast.error('Not implemented yet!');
|
||||||
|
|
||||||
|
setIsLoadingExportingToExcel(false);
|
||||||
|
};
|
||||||
|
|
||||||
const bulkApproveClickHandler = () => {
|
const bulkApproveClickHandler = () => {
|
||||||
setApprovalAction('APPROVED');
|
setApprovalAction('APPROVED');
|
||||||
confirmModal.openModal();
|
confirmModal.openModal();
|
||||||
@@ -985,17 +972,55 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
|
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={[
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
'page',
|
|
||||||
'pageSize',
|
|
||||||
'search',
|
|
||||||
'area_name',
|
|
||||||
'location_name',
|
|
||||||
'kandang_name',
|
|
||||||
]}
|
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
|
className={{
|
||||||
|
content:
|
||||||
|
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-1.5'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>Export</span>
|
||||||
|
|
||||||
|
<div className='w-px self-stretch bg-base-content/10' />
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chevron-down'
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportToExcelHandler}
|
||||||
|
isLoading={isLoadingExportingToExcel}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Export to Excel
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1324,14 +1349,18 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
isClearable={true}
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NumberInput
|
<SelectInputRadio
|
||||||
label='Periode'
|
label='Periode'
|
||||||
name='period'
|
placeholder='Pilih Periode'
|
||||||
placeholder='Masukkan Periode'
|
options={periodOptions}
|
||||||
value={formik.values.period ?? ''}
|
value={periodValue}
|
||||||
onChange={formik.handleChange}
|
onChange={(val) => {
|
||||||
onBlur={formik.handleBlur}
|
if (!Array.isArray(val)) {
|
||||||
|
formik.setFieldValue('period', val?.value || null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type ProjectFlockFormSchemaType = {
|
|||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
location_id: number;
|
location_id: number;
|
||||||
periode: number | string;
|
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
project_budgets: ProjectFlockBudgetsSchemaType[];
|
project_budgets: ProjectFlockBudgetsSchemaType[];
|
||||||
};
|
};
|
||||||
@@ -110,12 +109,6 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
|
|||||||
.min(1, 'Lokasi wajib diisi!')
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
.required('Lokasi wajib diisi!'),
|
.required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
// Period
|
|
||||||
periode: Yup.number()
|
|
||||||
.typeError('Periode harus berupa angka!')
|
|
||||||
.min(1, 'Periode minimal 1!')
|
|
||||||
.required('Periode wajib diisi!'),
|
|
||||||
|
|
||||||
kandang_ids: Yup.array()
|
kandang_ids: Yup.array()
|
||||||
.of(Yup.number().required('Kandang tidak valid!'))
|
.of(Yup.number().required('Kandang tidak valid!'))
|
||||||
.min(1, 'Minimal harus ada 1 kandang!')
|
.min(1, 'Minimal harus ada 1 kandang!')
|
||||||
|
|||||||
@@ -152,10 +152,6 @@ export const ProjectFlockFormConfirmationTable = ({
|
|||||||
label: 'Standar Produksi',
|
label: 'Standar Produksi',
|
||||||
value: projectFlockForm?.production_standard?.label ?? '-',
|
value: projectFlockForm?.production_standard?.label ?? '-',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Periode',
|
|
||||||
value: projectFlockForm?.periode ?? '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Informasi Kandang',
|
label: 'Informasi Kandang',
|
||||||
value: '',
|
value: '',
|
||||||
@@ -265,7 +261,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingFlocks,
|
isLoadingOptions: isLoadingFlocks,
|
||||||
options: optionsFlock,
|
options: optionsFlock,
|
||||||
loadMore: loadMoreFlock,
|
loadMore: loadMoreFlock,
|
||||||
} = useSelect(FlockApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
location_id: selectedLocation,
|
location_id: selectedLocation,
|
||||||
area_id: selectedArea,
|
area_id: selectedArea,
|
||||||
@@ -283,7 +279,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
setInputValue: setInputValueLocation,
|
setInputValue: setInputValueLocation,
|
||||||
loadMore: loadMoreLocation,
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
? selectedArea
|
? selectedArea
|
||||||
@@ -295,7 +291,7 @@ const ProjectFlockForm = ({
|
|||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
setInputValue: setInputValueProductionStandard,
|
setInputValue: setInputValueProductionStandard,
|
||||||
loadMore: loadMoreProductionStandard,
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +307,7 @@ const ProjectFlockForm = ({
|
|||||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||||
|
|
||||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||||
selectedFlock ? `${selectedFlock?.toString()}/periods` : undefined,
|
`${selectedFlock?.toString()}/periods`,
|
||||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -533,7 +529,6 @@ const ProjectFlockForm = ({
|
|||||||
kandang_ids: initialValues?.kandangs?.map(
|
kandang_ids: initialValues?.kandangs?.map(
|
||||||
(k: Kandang) => k.id
|
(k: Kandang) => k.id
|
||||||
) as number[],
|
) as number[],
|
||||||
periode: initialValues?.period ?? '',
|
|
||||||
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
||||||
return {
|
return {
|
||||||
nonstock: {
|
nonstock: {
|
||||||
@@ -573,7 +568,6 @@ const ProjectFlockForm = ({
|
|||||||
category: values.category as string,
|
category: values.category as string,
|
||||||
production_standard_id: values.production_standard_id as number,
|
production_standard_id: values.production_standard_id as number,
|
||||||
location_id: values.location_id as number,
|
location_id: values.location_id as number,
|
||||||
periode: parseInt(values.periode as unknown as string),
|
|
||||||
kandang_ids: values.kandang_ids as number[],
|
kandang_ids: values.kandang_ids as number[],
|
||||||
project_budgets: values.project_budgets.flatMap((budget) => {
|
project_budgets: values.project_budgets.flatMap((budget) => {
|
||||||
return {
|
return {
|
||||||
@@ -799,7 +793,6 @@ const ProjectFlockForm = ({
|
|||||||
formik.values.kandang_ids?.includes(kandang.id)
|
formik.values.kandang_ids?.includes(kandang.id)
|
||||||
)?.period
|
)?.period
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const inputPeriod =
|
const inputPeriod =
|
||||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||||
|
|
||||||
@@ -1029,18 +1022,12 @@ const ProjectFlockForm = ({
|
|||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='periode'
|
name='period'
|
||||||
label='Periode'
|
label='Periode'
|
||||||
|
disabled
|
||||||
|
readOnly
|
||||||
placeholder='Periode Flock'
|
placeholder='Periode Flock'
|
||||||
value={formik.values.periode}
|
value={selectedLocation ? inputPeriod : ''}
|
||||||
onChange={formik.handleChange}
|
|
||||||
onBlur={formik.handleBlur}
|
|
||||||
allowNegative={false}
|
|
||||||
decimalScale={0}
|
|
||||||
isError={
|
|
||||||
formik.touched.periode && Boolean(formik.errors.periode)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.periode as string}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
@@ -12,7 +18,6 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -34,11 +39,13 @@ import Table from '@/components/Table';
|
|||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
import { getRecordingRestriction } from './recording-utils';
|
import { getRecordingRestriction } from './recording-utils';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
@@ -68,26 +75,6 @@ const getStatusBadgeColor = (status: string): Color => {
|
|||||||
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecordingApproved = (recording: Recording): boolean => {
|
|
||||||
return (
|
|
||||||
recording.approval?.action === 'APPROVED' &&
|
|
||||||
recording.approval?.step_name === 'Disetujui'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const recordingApprovalStatusOptions: OptionType<string>[] = [
|
|
||||||
{ value: 'CREATED', label: 'Pengajuan' },
|
|
||||||
{ value: 'UPDATED', label: 'Diperbarui' },
|
|
||||||
{ value: 'APPROVED', label: 'Disetujui' },
|
|
||||||
{ value: 'REJECTED', label: 'Ditolak' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const projectFlockCategoryOptions: OptionType<string>[] = [
|
|
||||||
{ value: 'GROWING', label: 'Growing' },
|
|
||||||
{ value: 'LAYING', label: 'Laying' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
@@ -279,111 +266,80 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RecordingTable = () => {
|
const RecordingTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<{
|
} = useTableFilter({
|
||||||
search: string;
|
|
||||||
areaFilter: OptionType<number> | null;
|
|
||||||
locationFilter: OptionType<number> | null;
|
|
||||||
projectFlockFilter: OptionType<number> | null;
|
|
||||||
kandangFilter: OptionType<number> | null;
|
|
||||||
projectFlockKandangFilter: number | null;
|
|
||||||
approvalStatusFilter: OptionType<string> | null;
|
|
||||||
projectFlockCategoryFilter: OptionType<string> | null;
|
|
||||||
}>({
|
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
areaFilter: null,
|
areaFilter: '',
|
||||||
locationFilter: null,
|
locationFilter: '',
|
||||||
projectFlockFilter: null,
|
kandangFilter: '',
|
||||||
kandangFilter: null,
|
projectFlockKandangFilter: '',
|
||||||
projectFlockKandangFilter: null,
|
|
||||||
approvalStatusFilter: null,
|
|
||||||
projectFlockCategoryFilter: null,
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
search: 'search',
|
search: 'search',
|
||||||
areaFilter: 'area_id',
|
|
||||||
locationFilter: 'location_id',
|
|
||||||
projectFlockFilter: 'project_flock_id',
|
|
||||||
kandangFilter: 'kandang_id',
|
kandangFilter: 'kandang_id',
|
||||||
projectFlockKandangFilter: 'project_flock_kandang_id',
|
projectFlockKandangFilter: 'project_flock_kandang_id',
|
||||||
approvalStatusFilter: 'approval_status',
|
|
||||||
projectFlockCategoryFilter: 'project_flock_category',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
persist: true,
|
|
||||||
storeName: 'recording-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== FILTER STATE =====
|
||||||
|
const [filterArea, setFilterArea] = useState<OptionType | null>(null);
|
||||||
|
const [filterLocation, setFilterLocation] = useState<OptionType | null>(null);
|
||||||
|
const [filterProjectFlock, setFilterProjectFlock] =
|
||||||
|
useState<OptionType | null>(null);
|
||||||
|
const [filterKandang, setFilterKandang] = useState<OptionType | null>(null);
|
||||||
|
const [, setFilterProjectFlockKandangId] = useState<number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [filterLocationAreaId, setFilterLocationAreaId] = useState<string>('');
|
||||||
|
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
||||||
|
useState<string>('');
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<RecordingFilterType>({
|
const formik = useFormik<RecordingFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: tableFilterState.areaFilter,
|
area_id: null,
|
||||||
location_id: tableFilterState.locationFilter,
|
location_id: null,
|
||||||
project_flock_id: tableFilterState.projectFlockFilter,
|
kandang_id: null,
|
||||||
kandang_id: tableFilterState.kandangFilter,
|
project_flock_kandang_id: null,
|
||||||
project_flock_kandang_id: tableFilterState.projectFlockKandangFilter,
|
|
||||||
approval_status: tableFilterState.approvalStatusFilter,
|
|
||||||
project_flock_category: tableFilterState.projectFlockCategoryFilter,
|
|
||||||
},
|
},
|
||||||
validationSchema: RecordingFilterSchema,
|
validationSchema: RecordingFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id, true);
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
updateFilter('locationFilter', values.location_id, true);
|
updateFilter('locationFilter', values.location_id || '');
|
||||||
updateFilter('projectFlockFilter', values.project_flock_id, true);
|
updateFilter('kandangFilter', values.kandang_id || '');
|
||||||
updateFilter('kandangFilter', values.kandang_id, true);
|
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'projectFlockKandangFilter',
|
'projectFlockKandangFilter',
|
||||||
values.project_flock_kandang_id,
|
values.project_flock_kandang_id || ''
|
||||||
true
|
|
||||||
);
|
|
||||||
updateFilter('approvalStatusFilter', values.approval_status, true);
|
|
||||||
updateFilter(
|
|
||||||
'projectFlockCategoryFilter',
|
|
||||||
values.project_flock_category,
|
|
||||||
true
|
|
||||||
);
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
});
|
onReset: () => {
|
||||||
|
updateFilter('areaFilter', '');
|
||||||
const formikResetHandler = () => {
|
updateFilter('locationFilter', '');
|
||||||
updateFilter('areaFilter', null, true);
|
updateFilter('kandangFilter', '');
|
||||||
updateFilter('locationFilter', null, true);
|
updateFilter('projectFlockKandangFilter', '');
|
||||||
updateFilter('projectFlockFilter', null, true);
|
|
||||||
updateFilter('kandangFilter', null, true);
|
|
||||||
updateFilter('projectFlockKandangFilter', null, true);
|
|
||||||
updateFilter('approvalStatusFilter', null, true);
|
|
||||||
updateFilter('projectFlockCategoryFilter', null, true);
|
|
||||||
|
|
||||||
formik.resetForm({
|
|
||||||
values: {
|
|
||||||
area_id: null,
|
|
||||||
location_id: null,
|
|
||||||
project_flock_id: null,
|
|
||||||
kandang_id: null,
|
|
||||||
project_flock_kandang_id: null,
|
|
||||||
approval_status: null,
|
|
||||||
project_flock_category: null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
filterModal.closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const { project_flock_id, kandang_id } = formik.values;
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
@@ -399,14 +355,10 @@ const RecordingTable = () => {
|
|||||||
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
|
||||||
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
|
||||||
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
|
||||||
|
|
||||||
const singleDeleteModal = useModal();
|
const singleDeleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
const exportProgressInputModal = useModal();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: recordings,
|
data: recordings,
|
||||||
@@ -418,6 +370,13 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
||||||
|
const locationParams = useMemo(() => {
|
||||||
|
if (filterLocationAreaId) {
|
||||||
|
return { area_id: filterLocationAreaId };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [filterLocationAreaId]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
@@ -428,9 +387,7 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'search',
|
'search',
|
||||||
{
|
locationParams
|
||||||
area_id: String(formik.values.area_id?.value),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -445,6 +402,13 @@ const RecordingTable = () => {
|
|||||||
'search'
|
'search'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const projectFlockParams = useMemo(() => {
|
||||||
|
if (filterProjectFlockLocationId) {
|
||||||
|
return { location_id: filterProjectFlockLocationId };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [filterProjectFlockLocationId]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setProjectFlockInputValue,
|
setInputValue: setProjectFlockInputValue,
|
||||||
options: projectFlockOptions,
|
options: projectFlockOptions,
|
||||||
@@ -456,41 +420,34 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'flock_name',
|
'flock_name',
|
||||||
'search',
|
'search',
|
||||||
{
|
projectFlockParams
|
||||||
location_id: String(formik.values.location_id?.value),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const kandangOptions = useMemo(() => {
|
const kandangOptions = useMemo(() => {
|
||||||
if (!project_flock_id || !projectFlocksRawData) return [];
|
if (!filterProjectFlock || !projectFlocksRawData) return [];
|
||||||
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
||||||
|
|
||||||
const data = projectFlocksRawData.data as ProjectFlock[];
|
const data = projectFlocksRawData.data as ProjectFlock[];
|
||||||
const selectedProjectFlockData = data.find((pf) =>
|
const selectedProjectFlockData = data.find(
|
||||||
pf.id === formik.values.project_flock_id?.value
|
(pf) => pf.id === filterProjectFlock.value
|
||||||
? Number(formik.values.project_flock_id.value)
|
|
||||||
: 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedProjectFlockData?.kandangs) return [];
|
if (!selectedProjectFlockData?.kandangs) return [];
|
||||||
|
|
||||||
return selectedProjectFlockData.kandangs.map((k) => ({
|
return selectedProjectFlockData.kandangs.map((k) => ({
|
||||||
value: k.id,
|
value: k.id,
|
||||||
label: k.name || '',
|
label: k.name || '',
|
||||||
}));
|
}));
|
||||||
}, [project_flock_id, projectFlocksRawData]);
|
}, [filterProjectFlock, projectFlocksRawData]);
|
||||||
|
|
||||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
if (!project_flock_id?.value || !kandang_id?.value) return null;
|
if (!filterProjectFlock || !filterKandang) return null;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
project_flock_id: project_flock_id.value.toString(),
|
project_flock_id: filterProjectFlock.value.toString(),
|
||||||
kandang_id: kandang_id.value.toString(),
|
kandang_id: filterKandang.value.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||||
}, [project_flock_id, kandang_id]);
|
}, [filterProjectFlock, filterKandang]);
|
||||||
|
|
||||||
const { data: projectFlockKandangLookupData } = useSWR(
|
const { data: projectFlockKandangLookupData } = useSWR(
|
||||||
projectFlockKandangLookupUrl,
|
projectFlockKandangLookupUrl,
|
||||||
@@ -512,45 +469,118 @@ const RecordingTable = () => {
|
|||||||
? projectFlockKandangLookupData.data
|
? projectFlockKandangLookupData.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const formikRef = useRef(formik);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formikRef.current = formik;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectFlockKandangLookup?.id) {
|
if (projectFlockKandangLookup?.id) {
|
||||||
const pfkId = String(projectFlockKandangLookup.id);
|
const pfkId = String(projectFlockKandangLookup.id);
|
||||||
formik.setFieldValue('project_flock_kandang_id', pfkId);
|
setFilterProjectFlockKandangId(projectFlockKandangLookup.id);
|
||||||
|
formikRef.current.setFieldValue('project_flock_kandang_id', pfkId);
|
||||||
} else {
|
} else {
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
setFilterProjectFlockKandangId(undefined);
|
||||||
|
formikRef.current.setFieldValue('project_flock_kandang_id', null);
|
||||||
}
|
}
|
||||||
}, [projectFlockKandangLookup]);
|
}, [projectFlockKandangLookup]);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterAreaChange = useCallback(
|
||||||
formik.setFieldValue('area_id', val);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const area = val as OptionType | null;
|
||||||
|
const areaId = area?.value ? String(area.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('area_id', areaId);
|
||||||
formik.setFieldValue('location_id', null);
|
formik.setFieldValue('location_id', null);
|
||||||
formik.setFieldValue('project_flock_id', null);
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
formik.setFieldValue('kandang_id', null);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterLocationChange = (
|
setFilterArea(area);
|
||||||
val: OptionType | OptionType[] | null
|
setFilterLocation(null);
|
||||||
) => {
|
setFilterProjectFlock(null);
|
||||||
formik.setFieldValue('location_id', val);
|
setFilterKandang(null);
|
||||||
formik.setFieldValue('project_flock_id', null);
|
setFilterLocationAreaId(areaId || '');
|
||||||
|
setFilterProjectFlockLocationId('');
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterLocationChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const location = val as OptionType | null;
|
||||||
|
const locationId = location?.value ? String(location.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('location_id', locationId);
|
||||||
formik.setFieldValue('kandang_id', null);
|
formik.setFieldValue('kandang_id', null);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterProjectFlockChange = (
|
setFilterLocation(location);
|
||||||
val: OptionType | OptionType[] | null
|
setFilterProjectFlock(null);
|
||||||
) => {
|
setFilterKandang(null);
|
||||||
formik.setFieldValue('project_flock_id', val);
|
setFilterProjectFlockLocationId(locationId || '');
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterProjectFlockChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const projectFlock = val as OptionType | null;
|
||||||
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
formik.setFieldValue('kandang_id', null);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterKandangChange = (val: OptionType | OptionType[] | null) => {
|
setFilterProjectFlock(projectFlock);
|
||||||
formik.setFieldValue('kandang_id', val);
|
setFilterKandang(null);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterKandangChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const kandang = val as OptionType | null;
|
||||||
|
const kandangId = kandang?.value ? String(kandang.value) : null;
|
||||||
|
|
||||||
|
formik.setFieldValue('kandang_id', kandangId);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
};
|
|
||||||
|
setFilterKandang(kandang);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const areaIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.area_id) return null;
|
||||||
|
return (
|
||||||
|
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}, [formik.values.area_id, areaOptions]);
|
||||||
|
|
||||||
|
const locationIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.location_id) return null;
|
||||||
|
return (
|
||||||
|
locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.location_id, locationOptions]);
|
||||||
|
|
||||||
|
const projectFlockIdValue = useMemo(() => {
|
||||||
|
if (!filterProjectFlock) return null;
|
||||||
|
return filterProjectFlock;
|
||||||
|
}, [filterProjectFlock]);
|
||||||
|
|
||||||
|
const kandangIdValue = useMemo(() => {
|
||||||
|
if (!formik.values.kandang_id) return null;
|
||||||
|
return (
|
||||||
|
kandangOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.kandang_id
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.kandang_id, kandangOptions]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -558,9 +588,25 @@ const RecordingTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const isRecordingApproved = useCallback((recording: Recording): boolean => {
|
||||||
updateFilter('search', e.target.value, true);
|
return (
|
||||||
};
|
recording.approval?.action === 'APPROVED' &&
|
||||||
|
recording.approval?.step_name === 'Disetujui'
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('recording-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
|
const searchChangeHandler = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
setSearchValue(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
},
|
||||||
|
[updateFilter, setSearchValue, setPage]
|
||||||
|
);
|
||||||
|
|
||||||
const singleDeleteHandler = async () => {
|
const singleDeleteHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
@@ -652,60 +698,6 @@ const RecordingTable = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetExportProgressForm = useCallback(() => {
|
|
||||||
setExportProgressStartDate('');
|
|
||||||
setExportProgressEndDate('');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const exportProgressStartDateChangeHandler = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setExportProgressStartDate(e.target.value);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportProgressEndDateChangeHandler = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setExportProgressEndDate(e.target.value);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportProgressInputToExcelClickHandler = useCallback(() => {
|
|
||||||
resetExportProgressForm();
|
|
||||||
exportProgressInputModal.openModal();
|
|
||||||
}, [exportProgressInputModal, resetExportProgressForm]);
|
|
||||||
|
|
||||||
const submitExportProgressInputHandler = useCallback(async () => {
|
|
||||||
if (!exportProgressStartDate || !exportProgressEndDate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsExportProgressLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await RecordingApi.exportInputProgressToExcel(
|
|
||||||
exportProgressStartDate,
|
|
||||||
exportProgressEndDate
|
|
||||||
);
|
|
||||||
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
toast.success('Ekspor berhasil');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsExportProgressLoading(false);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
exportProgressEndDate,
|
|
||||||
exportProgressInputModal,
|
|
||||||
exportProgressStartDate,
|
|
||||||
resetExportProgressForm,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(recordings) && recordings.data) {
|
if (isResponseSuccess(recordings) && recordings.data) {
|
||||||
const newSelection: Record<string, boolean> = {};
|
const newSelection: Record<string, boolean> = {};
|
||||||
@@ -866,8 +858,7 @@ const RecordingTable = () => {
|
|||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
{props.row.original.day} (Minggu ke-
|
{props.row.original.day} (Minggu ke-
|
||||||
{props.row.original.week} hari ke-
|
{props.row.original.project_flock.production_standart.week})
|
||||||
{props.row.original.excess_days})
|
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1113,7 +1104,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)} butir`
|
? `${value.toFixed(2)}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1129,7 +1120,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center text-gray-600'>
|
<div className='text-center text-gray-600'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)} btr`
|
? `${value.toFixed(2)}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1377,16 +1368,6 @@ const RecordingTable = () => {
|
|||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Export to Excel
|
Export to Excel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={exportProgressInputToExcelClickHandler}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Ekspor Input Progress (Excel)
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1465,13 +1446,13 @@ const RecordingTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={formik.values.area_id}
|
value={areaIdValue}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -1484,13 +1465,13 @@ const RecordingTable = () => {
|
|||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={formik.values.location_id}
|
value={locationIdValue}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isDisabled={!formik.values.area_id?.value}
|
isDisabled={!filterArea}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1498,13 +1479,13 @@ const RecordingTable = () => {
|
|||||||
label='Project Flock'
|
label='Project Flock'
|
||||||
placeholder='Pilih Project Flock'
|
placeholder='Pilih Project Flock'
|
||||||
options={projectFlockOptions}
|
options={projectFlockOptions}
|
||||||
value={formik.values.project_flock_id}
|
value={projectFlockIdValue}
|
||||||
onChange={handleFilterProjectFlockChange}
|
onChange={handleFilterProjectFlockChange}
|
||||||
onInputChange={setProjectFlockInputValue}
|
onInputChange={setProjectFlockInputValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!formik.values.location_id?.value}
|
isDisabled={!filterLocation}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1512,35 +1493,11 @@ const RecordingTable = () => {
|
|||||||
label='Kandang'
|
label='Kandang'
|
||||||
placeholder='Pilih Kandang'
|
placeholder='Pilih Kandang'
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
value={formik.values.kandang_id}
|
value={kandangIdValue}
|
||||||
onChange={handleFilterKandangChange}
|
onChange={handleFilterKandangChange}
|
||||||
isLoading={!formik.values.project_flock_id?.value}
|
isLoading={!filterProjectFlock}
|
||||||
isClearable
|
|
||||||
isDisabled={!formik.values.project_flock_id?.value}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Kategori'
|
|
||||||
placeholder='Pilih Kategori'
|
|
||||||
options={projectFlockCategoryOptions}
|
|
||||||
value={formik.values.project_flock_category}
|
|
||||||
onChange={(val) => {
|
|
||||||
formik.setFieldValue('project_flock_category', val);
|
|
||||||
}}
|
|
||||||
isClearable
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Status Approval'
|
|
||||||
placeholder='Pilih Status Approval'
|
|
||||||
options={recordingApprovalStatusOptions}
|
|
||||||
value={formik.values.approval_status}
|
|
||||||
onChange={(val) => {
|
|
||||||
formik.setFieldValue('approval_status', val);
|
|
||||||
}}
|
|
||||||
isClearable
|
isClearable
|
||||||
|
isDisabled={!filterProjectFlock}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1548,16 +1505,30 @@ const RecordingTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='button'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
onClick={() => {
|
||||||
|
formik.resetForm();
|
||||||
|
setFilterArea(null);
|
||||||
|
setFilterLocation(null);
|
||||||
|
setFilterProjectFlock(null);
|
||||||
|
setFilterKandang(null);
|
||||||
|
setFilterLocationAreaId('');
|
||||||
|
setFilterProjectFlockLocationId('');
|
||||||
|
filterModal.closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={!formik.isValid || formik.isSubmitting}
|
disabled={
|
||||||
|
!formik.isValid ||
|
||||||
|
formik.isSubmitting ||
|
||||||
|
!formik.values.kandang_id
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1580,76 +1551,6 @@ const RecordingTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
|
||||||
ref={exportProgressInputModal.ref}
|
|
||||||
className={{
|
|
||||||
modalBox: 'max-w-lg rounded-lg p-0',
|
|
||||||
}}
|
|
||||||
closeOnBackdrop
|
|
||||||
>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
|
||||||
<h4 className='text-sm font-semibold text-base-content'>
|
|
||||||
Ekspor Input Progress
|
|
||||||
</h4>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
}}
|
|
||||||
className='p-1'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:close' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
|
||||||
<DateInput
|
|
||||||
name='export_progress_start_date'
|
|
||||||
label='Tanggal Mulai'
|
|
||||||
value={exportProgressStartDate}
|
|
||||||
onChange={exportProgressStartDateChangeHandler}
|
|
||||||
isNestedModal
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DateInput
|
|
||||||
name='export_progress_end_date'
|
|
||||||
label='Tanggal Selesai'
|
|
||||||
value={exportProgressEndDate}
|
|
||||||
onChange={exportProgressEndDateChangeHandler}
|
|
||||||
isNestedModal
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
}}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color='success'
|
|
||||||
onClick={submitExportProgressInputHandler}
|
|
||||||
isLoading={isExportProgressLoading}
|
|
||||||
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
|
|||||||
@@ -1,40 +1,15 @@
|
|||||||
import { OptionType } from '@/components/input/SelectInput';
|
import { string, object } from 'yup';
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
export const RecordingFilterSchema = Yup.object().shape({
|
export const RecordingFilterSchema = object().shape({
|
||||||
area_id: Yup.object({
|
area_id: string().nullable(),
|
||||||
value: Yup.number().nullable(),
|
location_id: string().nullable(),
|
||||||
label: Yup.string().nullable(),
|
kandang_id: string().nullable(),
|
||||||
}).nullable(),
|
project_flock_kandang_id: string().nullable(),
|
||||||
location_id: Yup.object({
|
|
||||||
value: Yup.number().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
project_flock_id: Yup.object({
|
|
||||||
value: Yup.number().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
kandang_id: Yup.object({
|
|
||||||
value: Yup.number().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
project_flock_kandang_id: Yup.number().nullable(),
|
|
||||||
approval_status: Yup.object({
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
project_flock_category: Yup.object({
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
label: Yup.string().nullable(),
|
|
||||||
}).nullable(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RecordingFilterType = {
|
export type RecordingFilterType = {
|
||||||
area_id: OptionType<number> | null;
|
area_id: string | null;
|
||||||
location_id: OptionType<number> | null;
|
location_id: string | null;
|
||||||
project_flock_id: OptionType<number> | null;
|
kandang_id: string | null;
|
||||||
kandang_id: OptionType<number> | null;
|
project_flock_kandang_id: string | null;
|
||||||
project_flock_kandang_id: number | null;
|
|
||||||
approval_status: OptionType<string> | null;
|
|
||||||
project_flock_category: OptionType<string> | null;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import {
|
|||||||
CreateGrowingRecordingPayload,
|
CreateGrowingRecordingPayload,
|
||||||
CreateLayingRecordingPayload,
|
CreateLayingRecordingPayload,
|
||||||
CreateEggPayload,
|
CreateEggPayload,
|
||||||
RecordingStock,
|
|
||||||
} from '@/types/api/production/recording';
|
} from '@/types/api/production/recording';
|
||||||
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
|
||||||
|
|
||||||
type RecordingGrowingFormSchemaType = {
|
type RecordingGrowingFormSchemaType = {
|
||||||
record_date: string;
|
record_date: string;
|
||||||
@@ -31,19 +29,11 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
} | null;
|
} | null;
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
stocks: {
|
stocks: {
|
||||||
product_warehouse_id:
|
product_warehouse_id: number;
|
||||||
| {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
depletions: {
|
depletions: {
|
||||||
product_warehouse_id?: {
|
product_warehouse_id?: number;
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
source_product_warehouse_id?: number;
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
@@ -51,48 +41,34 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
|
|
||||||
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||||
eggs: {
|
eggs: {
|
||||||
product_warehouse_id?: {
|
product_warehouse_id?: number;
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StockSchema = {
|
export type StockSchema = {
|
||||||
product_warehouse_id: {
|
product_warehouse_id: number;
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
qty: number | string;
|
qty: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DepletionSchema = {
|
export type DepletionSchema = {
|
||||||
product_warehouse_id?: {
|
product_warehouse_id?: number;
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
source_product_warehouse_id?: number;
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EggSchema = {
|
export type EggSchema = {
|
||||||
product_warehouse_id?: {
|
product_warehouse_id?: number;
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
} | null;
|
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.object({
|
product_warehouse_id: Yup.number()
|
||||||
value: Yup.number().min(1).required(),
|
|
||||||
label: Yup.string().required(),
|
|
||||||
})
|
|
||||||
.required('Produk wajib diisi!')
|
.required('Produk wajib diisi!')
|
||||||
.typeError('Produk wajib diisi!'),
|
.min(1, 'Produk wajib diisi!')
|
||||||
|
.typeError('Produk harus berupa angka!'),
|
||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.required('Jumlah penggunaan wajib diisi!')
|
.required('Jumlah penggunaan wajib diisi!')
|
||||||
.moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
|
.moreThan(0, 'Jumlah penggunaan harus lebih dari 0!')
|
||||||
@@ -100,12 +76,9 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.object({
|
product_warehouse_id: Yup.number()
|
||||||
value: Yup.number().min(1).required(),
|
|
||||||
label: Yup.string().required(),
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.typeError('Depletions harus berupa angka!'),
|
||||||
source_product_warehouse_id: Yup.number()
|
source_product_warehouse_id: Yup.number()
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Gudang sumber harus berupa angka!'),
|
.typeError('Gudang sumber harus berupa angka!'),
|
||||||
@@ -115,12 +88,9 @@ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.object({
|
product_warehouse_id: Yup.number()
|
||||||
value: Yup.number().min(1).required(),
|
|
||||||
label: Yup.string().required(),
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.typeError('Kondisi telur harus berupa angka!'),
|
||||||
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
||||||
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||||
});
|
});
|
||||||
@@ -278,18 +248,14 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
initialValues?.project_flock?.project_flock_kandang_id ??
|
initialValues?.project_flock?.project_flock_kandang_id ??
|
||||||
0,
|
0,
|
||||||
stocks: initialValues?.stocks?.map((stock) => ({
|
stocks: initialValues?.stocks?.map((stock) => ({
|
||||||
product_warehouse_id: {
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
value: stock.product_warehouse_id,
|
|
||||||
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
|
||||||
},
|
|
||||||
qty:
|
qty:
|
||||||
(stock as RecordingStock).qty ||
|
(stock as { qty?: number; usage_amount?: number }).qty ||
|
||||||
((stock as RecordingStock).usage_amount || 0) +
|
(stock as { qty?: number; usage_amount?: number }).usage_amount ||
|
||||||
((stock as RecordingStock).pending_qty || 0) ||
|
|
||||||
'',
|
'',
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: undefined,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -297,16 +263,13 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
(
|
(
|
||||||
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
|
||||||
) => ({
|
) => ({
|
||||||
product_warehouse_id: {
|
product_warehouse_id: depletion.product_warehouse_id,
|
||||||
value: Number(depletion.product_warehouse_id ?? 0),
|
|
||||||
label: getProductWarehouseOptionLabel(depletion.product_warehouse),
|
|
||||||
},
|
|
||||||
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
||||||
qty: depletion.qty,
|
qty: depletion.qty,
|
||||||
})
|
})
|
||||||
) ?? [
|
) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: undefined,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -318,15 +281,12 @@ export const getRecordingLayingFormInitialValues = (
|
|||||||
...getRecordingGrowingFormInitialValues(initialValues),
|
...getRecordingGrowingFormInitialValues(initialValues),
|
||||||
|
|
||||||
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
|
||||||
product_warehouse_id: {
|
product_warehouse_id: egg.product_warehouse_id,
|
||||||
value: Number(egg.product_warehouse_id ?? 0),
|
|
||||||
label: getProductWarehouseOptionLabel(egg.product_warehouse),
|
|
||||||
},
|
|
||||||
qty: egg.qty,
|
qty: egg.qty,
|
||||||
weight: egg.weight,
|
weight: egg.weight,
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -31,14 +31,12 @@ import {
|
|||||||
RecordingApi,
|
RecordingApi,
|
||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
} from '@/services/api/production';
|
} from '@/services/api/production';
|
||||||
import { ProductionStandardApi, ProductApi } from '@/services/api/master-data';
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
import {
|
import {
|
||||||
ProductionStandard,
|
ProductionStandard,
|
||||||
StandardDetails,
|
StandardDetails,
|
||||||
} from '@/types/api/master-data/production-standard';
|
} from '@/types/api/master-data/production-standard';
|
||||||
import { Product } from '@/types/api/master-data/product';
|
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { SystemSettingsApi } from '@/services/api/system-settings';
|
|
||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
|
|
||||||
@@ -501,20 +499,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
type,
|
type,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ===== MIGRATION MODE =====
|
|
||||||
const { data: systemSettingsResponse } = useSWR(
|
|
||||||
SystemSettingsApi.basePath,
|
|
||||||
SystemSettingsApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const isMigrationMode = useMemo(() => {
|
|
||||||
if (!isResponseSuccess(systemSettingsResponse)) return false;
|
|
||||||
const setting = systemSettingsResponse.data.find(
|
|
||||||
(s) => s.key === 'allow_negative_pakan_ovk'
|
|
||||||
);
|
|
||||||
return setting?.value === 'true';
|
|
||||||
}, [systemSettingsResponse]);
|
|
||||||
|
|
||||||
// ===== PAYLOAD CREATION HELPERS =====
|
// ===== PAYLOAD CREATION HELPERS =====
|
||||||
const createGrowingPayload = useCallback(
|
const createGrowingPayload = useCallback(
|
||||||
(values: RecordingGrowingFormValues) => {
|
(values: RecordingGrowingFormValues) => {
|
||||||
@@ -522,7 +506,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
? values.depletions
|
? values.depletions
|
||||||
?.filter((d) => d.product_warehouse_id && d.qty)
|
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||||
.map((depletion) => ({
|
.map((depletion) => ({
|
||||||
product_warehouse_id: depletion.product_warehouse_id?.value ?? 0,
|
product_warehouse_id: depletion.product_warehouse_id!,
|
||||||
...(depletion.source_product_warehouse_id && {
|
...(depletion.source_product_warehouse_id && {
|
||||||
source_product_warehouse_id:
|
source_product_warehouse_id:
|
||||||
depletion.source_product_warehouse_id,
|
depletion.source_product_warehouse_id,
|
||||||
@@ -533,13 +517,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const stocks = recordingRestriction.canEditStock
|
const stocks = recordingRestriction.canEditStock
|
||||||
? (values.stocks ?? [])
|
? (values.stocks ?? [])
|
||||||
.filter((s) => s.product_warehouse_id?.value && s.qty)
|
.filter((s) => s.product_warehouse_id && s.qty)
|
||||||
.map((stock) => ({
|
.map((stock) => ({
|
||||||
// In migration mode, product_warehouse_id field holds product.id;
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
// send it as product_id so the backend auto-creates the warehouse entry.
|
|
||||||
...(isMigrationMode
|
|
||||||
? { product_id: stock.product_warehouse_id?.value }
|
|
||||||
: { product_warehouse_id: stock.product_warehouse_id?.value }),
|
|
||||||
qty: Number(stock.qty) || 0,
|
qty: Number(stock.qty) || 0,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
@@ -551,19 +531,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
...(depletions.length > 0 && { depletions }),
|
...(depletions.length > 0 && { depletions }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[recordingRestriction.canEditStock, recordingRestriction.canEditDepletion]
|
||||||
isMigrationMode,
|
|
||||||
recordingRestriction.canEditStock,
|
|
||||||
recordingRestriction.canEditDepletion,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const createLayingPayload = useCallback(
|
const createLayingPayload = useCallback(
|
||||||
(values: RecordingLayingFormValues) => {
|
(values: RecordingLayingFormValues) => {
|
||||||
const depletions = values.depletions
|
const depletions = values.depletions
|
||||||
?.filter((d) => d.product_warehouse_id?.value && d.qty)
|
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||||
.map((depletion) => ({
|
.map((depletion) => ({
|
||||||
product_warehouse_id: depletion.product_warehouse_id?.value ?? 0,
|
product_warehouse_id: depletion.product_warehouse_id!,
|
||||||
...(depletion.source_product_warehouse_id && {
|
...(depletion.source_product_warehouse_id && {
|
||||||
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
source_product_warehouse_id: depletion.source_product_warehouse_id,
|
||||||
}),
|
}),
|
||||||
@@ -573,7 +549,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const eggs = values.eggs
|
const eggs = values.eggs
|
||||||
?.filter((e) => e.product_warehouse_id && e.qty && e.weight)
|
?.filter((e) => e.product_warehouse_id && e.qty && e.weight)
|
||||||
.map((egg) => ({
|
.map((egg) => ({
|
||||||
product_warehouse_id: egg.product_warehouse_id?.value ?? 0,
|
product_warehouse_id: egg.product_warehouse_id!,
|
||||||
qty: Number(egg.qty) || 0,
|
qty: Number(egg.qty) || 0,
|
||||||
weight:
|
weight:
|
||||||
typeof egg.weight === 'number'
|
typeof egg.weight === 'number'
|
||||||
@@ -583,11 +559,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const stocks = recordingRestriction.canEditStock
|
const stocks = recordingRestriction.canEditStock
|
||||||
? values.stocks
|
? values.stocks
|
||||||
.filter((s) => s.product_warehouse_id?.value && s.qty)
|
.filter((s) => s.product_warehouse_id && s.qty)
|
||||||
.map((stock) => ({
|
.map((stock) => ({
|
||||||
...(isMigrationMode
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
? { product_id: stock.product_warehouse_id?.value }
|
|
||||||
: { product_warehouse_id: stock.product_warehouse_id?.value }),
|
|
||||||
qty: Number(stock.qty) || 0,
|
qty: Number(stock.qty) || 0,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
@@ -600,7 +574,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
...(eggs && eggs.length > 0 && { eggs }),
|
...(eggs && eggs.length > 0 && { eggs }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[isMigrationMode, recordingRestriction.canEditStock]
|
[recordingRestriction.canEditStock]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRecordingEditable = useCallback((recording?: Recording) => {
|
const isRecordingEditable = useCallback((recording?: Recording) => {
|
||||||
@@ -629,13 +603,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return true;
|
return true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// When migration mode ON: fetch all master PAKAN/OVK products (no warehouse entry needed).
|
|
||||||
// When migration mode OFF: fetch from product-warehouses as usual.
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setStockProductInputValue,
|
setInputValue: setStockProductInputValue,
|
||||||
rawData: stockProductsPW,
|
rawData: stockProducts,
|
||||||
isLoadingOptions: isLoadingStockProductsPW,
|
isLoadingOptions: isLoadingStockProducts,
|
||||||
loadMore: loadMoreStockProductsPW,
|
loadMore: loadMoreStockProducts,
|
||||||
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
||||||
flags: 'PAKAN,OVK',
|
flags: 'PAKAN,OVK',
|
||||||
limit: '100',
|
limit: '100',
|
||||||
@@ -644,29 +616,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
|
...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setStockMasterInputValue,
|
|
||||||
rawData: stockProductsMaster,
|
|
||||||
isLoadingOptions: isLoadingStockProductsMaster,
|
|
||||||
loadMore: loadMoreStockProductsMaster,
|
|
||||||
} = useSelect(
|
|
||||||
isMigrationMode ? ProductApi.basePath : null,
|
|
||||||
'id',
|
|
||||||
'name',
|
|
||||||
'search',
|
|
||||||
{ flags: 'PAKAN,OVK', limit: '100' }
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLoadingStockProducts = isMigrationMode
|
|
||||||
? isLoadingStockProductsMaster
|
|
||||||
: isLoadingStockProductsPW;
|
|
||||||
const loadMoreStockProducts = isMigrationMode
|
|
||||||
? loadMoreStockProductsMaster
|
|
||||||
: loadMoreStockProductsPW;
|
|
||||||
const setStockInputValue = isMigrationMode
|
|
||||||
? setStockMasterInputValue
|
|
||||||
: setStockProductInputValue;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rawData: depletionProductsData,
|
rawData: depletionProductsData,
|
||||||
isLoadingOptions: isLoadingDepletionProducts,
|
isLoadingOptions: isLoadingDepletionProducts,
|
||||||
@@ -1050,9 +999,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const items: Array<ProductWarehouse | null | undefined> = [];
|
const items: Array<ProductWarehouse | null | undefined> = [];
|
||||||
|
|
||||||
if (!isMigrationMode && isResponseSuccess(stockProductsPW)) {
|
if (isResponseSuccess(stockProducts)) {
|
||||||
items.push(
|
items.push(
|
||||||
...((stockProductsPW.data as unknown as ProductWarehouse[]) ?? [])
|
...((stockProducts.data as unknown as ProductWarehouse[]) ?? [])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,8 +1035,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
mergeKnownProductWarehouses(items);
|
mergeKnownProductWarehouses(items);
|
||||||
}, [
|
}, [
|
||||||
isMigrationMode,
|
stockProducts,
|
||||||
stockProductsPW,
|
|
||||||
depletionProductsData,
|
depletionProductsData,
|
||||||
eggProductsData,
|
eggProductsData,
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -1118,20 +1066,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const unifiedStockProducts = useMemo(() => {
|
const unifiedStockProducts = useMemo(() => {
|
||||||
if (isMigrationMode) {
|
const options = isResponseSuccess(stockProducts)
|
||||||
// In migration mode, show all master PAKAN/OVK products (no warehouse context).
|
|
||||||
// value = product.id; submission will send product_id to the backend.
|
|
||||||
const options: OptionType[] = isResponseSuccess(stockProductsMaster)
|
|
||||||
? (stockProductsMaster.data as unknown as Product[])
|
|
||||||
.map((p) => ({ value: p.id, label: p.name }))
|
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
|
||||||
: [];
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = isResponseSuccess(stockProductsPW)
|
|
||||||
? buildProductWarehouseOptions(
|
? buildProductWarehouseOptions(
|
||||||
stockProductsPW.data as unknown as ProductWarehouse[]
|
stockProducts.data as unknown as ProductWarehouse[]
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -1148,9 +1085,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [
|
}, [
|
||||||
isMigrationMode,
|
stockProducts,
|
||||||
stockProductsMaster,
|
|
||||||
stockProductsPW,
|
|
||||||
buildProductWarehouseOptions,
|
buildProductWarehouseOptions,
|
||||||
initialValues,
|
initialValues,
|
||||||
type,
|
type,
|
||||||
@@ -1269,22 +1204,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// In migration mode (edit), the dropdown options use product.id as their value,
|
|
||||||
// but the API returns product_warehouse_id (PW entity ID). Remap so the dropdown
|
|
||||||
// can match the correct option. The product ID is available on the nested
|
|
||||||
// product_warehouse object returned by the API.
|
|
||||||
if (isMigrationMode && type === 'edit' && initialValues?.stocks?.length) {
|
|
||||||
baseValues.stocks = initialValues.stocks.map((stock) => ({
|
|
||||||
product_warehouse_id: {
|
|
||||||
value: Number(
|
|
||||||
stock.product_warehouse?.product_id ?? stock.product_warehouse_id
|
|
||||||
),
|
|
||||||
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
|
||||||
},
|
|
||||||
qty: stock.usage_amount ?? '',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recordingRestriction.canEditStock) {
|
if (!recordingRestriction.canEditStock) {
|
||||||
baseValues.stocks = [];
|
baseValues.stocks = [];
|
||||||
}
|
}
|
||||||
@@ -1305,7 +1224,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
selectedKandang,
|
selectedKandang,
|
||||||
recordingRestriction.canEditStock,
|
recordingRestriction.canEditStock,
|
||||||
recordingRestriction.canEditDepletion,
|
recordingRestriction.canEditDepletion,
|
||||||
isMigrationMode,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const formik = useFormik<
|
const formik = useFormik<
|
||||||
@@ -1417,35 +1335,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// SWR timing fix: formik initializes before system-settings load, so isMigrationMode
|
|
||||||
// starts false. When it flips true, formikInitialValues recomputes but enableReinitialize
|
|
||||||
// is false, so formik won't pick it up. Push the corrected stock values once, and only
|
|
||||||
// once — the ref prevents re-firing if something causes isMigrationMode to re-evaluate.
|
|
||||||
const migrationEditMappingApplied = useRef(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
type !== 'edit' ||
|
|
||||||
!isMigrationMode ||
|
|
||||||
!initialValues?.stocks?.length ||
|
|
||||||
migrationEditMappingApplied.current
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
migrationEditMappingApplied.current = true;
|
|
||||||
formik.setFieldValue(
|
|
||||||
'stocks',
|
|
||||||
initialValues.stocks.map((stock) => ({
|
|
||||||
product_warehouse_id: {
|
|
||||||
value: Number(
|
|
||||||
stock.product_warehouse?.product_id ?? stock.product_warehouse_id
|
|
||||||
),
|
|
||||||
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
|
||||||
},
|
|
||||||
qty: stock.usage_amount ?? '',
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isMigrationMode]);
|
|
||||||
|
|
||||||
// ===== HELPER FUNCTIONS =====
|
// ===== HELPER FUNCTIONS =====
|
||||||
const { setFieldValue } = formik;
|
const { setFieldValue } = formik;
|
||||||
|
|
||||||
@@ -1462,7 +1351,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
(stockIdx: number) => {
|
(stockIdx: number) => {
|
||||||
if ((type as 'add' | 'edit' | 'detail') === 'detail') return null;
|
if ((type as 'add' | 'edit' | 'detail') === 'detail') return null;
|
||||||
const stock = formik.values.stocks?.[stockIdx];
|
const stock = formik.values.stocks?.[stockIdx];
|
||||||
if (!stock || !stock.product_warehouse_id?.value) return null;
|
if (!stock || !stock.product_warehouse_id) return null;
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[formik.values.stocks, type]
|
[formik.values.stocks, type]
|
||||||
@@ -1472,7 +1361,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
(productWarehouseId: number) => {
|
(productWarehouseId: number) => {
|
||||||
if ((type === 'edit' || type === 'detail') && initialValues?.stocks) {
|
if ((type === 'edit' || type === 'detail') && initialValues?.stocks) {
|
||||||
const existingStock = initialValues.stocks.find(
|
const existingStock = initialValues.stocks.find(
|
||||||
(s) => Number(s.product_warehouse_id) === Number(productWarehouseId)
|
(s) => s.product_warehouse_id === productWarehouseId
|
||||||
) as RecordingStock | undefined;
|
) as RecordingStock | undefined;
|
||||||
if (existingStock) {
|
if (existingStock) {
|
||||||
return {
|
return {
|
||||||
@@ -1492,25 +1381,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const getStockUsageAdornment = useCallback(
|
const getStockUsageAdornment = useCallback(
|
||||||
(stockIdx: number) => {
|
(stockIdx: number) => {
|
||||||
const stock = formik.values.stocks?.[stockIdx];
|
const stock = formik.values.stocks?.[stockIdx];
|
||||||
if (!stock || !stock.product_warehouse_id?.value) return null;
|
if (!stock || !stock.product_warehouse_id) return null;
|
||||||
|
|
||||||
const isDetail = (type as 'add' | 'edit' | 'detail') === 'detail';
|
const isDetail = (type as 'add' | 'edit' | 'detail') === 'detail';
|
||||||
const availableStock = getAvailableStock(
|
const availableStock = getAvailableStock(stock.product_warehouse_id);
|
||||||
stock.product_warehouse_id.value
|
|
||||||
);
|
|
||||||
const requestedUsage = Number(stock.qty) || 0;
|
const requestedUsage = Number(stock.qty) || 0;
|
||||||
const remainingStock = availableStock - requestedUsage;
|
const remainingStock = availableStock - requestedUsage;
|
||||||
const { pendingQty } = getStockPendingInfo(
|
const { pendingQty } = getStockPendingInfo(stock.product_warehouse_id);
|
||||||
stock.product_warehouse_id.value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDetail) {
|
if (isDetail) {
|
||||||
if (pendingQty > 0) {
|
if (pendingQty > 0) {
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||||
(tersedia: {formatNumber(availableStock)} | pending:{' '}
|
(tersedia: {formatNumber(requestedUsage)} | pending:{' '}
|
||||||
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
||||||
pakai: {formatNumber(requestedUsage)})
|
pakai: {formatNumber(requestedUsage + pendingQty)})
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1609,10 +1494,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return (
|
return (
|
||||||
idx !== currentIdx &&
|
idx !== currentIdx &&
|
||||||
s.product_warehouse_id &&
|
s.product_warehouse_id &&
|
||||||
s.product_warehouse_id.value !== 0
|
s.product_warehouse_id !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((s) => s.product_warehouse_id?.value) || [];
|
.map((s) => s.product_warehouse_id) || [];
|
||||||
|
|
||||||
return unifiedStockProducts.filter(
|
return unifiedStockProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1629,10 +1514,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return (
|
return (
|
||||||
idx !== currentIdx &&
|
idx !== currentIdx &&
|
||||||
d.product_warehouse_id &&
|
d.product_warehouse_id &&
|
||||||
d.product_warehouse_id.value !== 0
|
d.product_warehouse_id !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((d) => d.product_warehouse_id?.value) || [];
|
.map((d) => d.product_warehouse_id) || [];
|
||||||
|
|
||||||
return depletionProducts.filter(
|
return depletionProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1649,10 +1534,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
return (
|
return (
|
||||||
idx !== currentIdx &&
|
idx !== currentIdx &&
|
||||||
e.product_warehouse_id &&
|
e.product_warehouse_id &&
|
||||||
e.product_warehouse_id.value !== 0
|
e.product_warehouse_id !== 0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((e) => e.product_warehouse_id?.value) || [];
|
.map((e) => e.product_warehouse_id) || [];
|
||||||
|
|
||||||
return eggProducts.filter(
|
return eggProducts.filter(
|
||||||
(opt) => !selectedProductIds.includes(Number(opt.value))
|
(opt) => !selectedProductIds.includes(Number(opt.value))
|
||||||
@@ -1698,9 +1583,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
isError: touchedField && Boolean(errorField?.[column]),
|
isError: touchedField && Boolean(errorField?.[column]),
|
||||||
errorMessage:
|
errorMessage:
|
||||||
touchedField && errorField?.[column]
|
touchedField && errorField?.[column]
|
||||||
? errorField[column] instanceof Object
|
? (errorField[column] as string)
|
||||||
? (errorField[column] as OptionType)?.label
|
|
||||||
: (errorField[column] as string)
|
|
||||||
: '',
|
: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1731,14 +1614,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1746,7 +1629,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1795,14 +1678,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1810,7 +1693,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1848,14 +1731,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1863,7 +1746,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -2076,7 +1959,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newStocks = [
|
const newStocks = [
|
||||||
...(formik.values.stocks || []),
|
...(formik.values.stocks || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2108,7 +1991,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newDepletions = [
|
const newDepletions = [
|
||||||
...(formik.values.depletions || []),
|
...(formik.values.depletions || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2142,7 +2025,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newEggs = [
|
const newEggs = [
|
||||||
...((formik.values as RecordingLayingFormValues).eggs || []),
|
...((formik.values as RecordingLayingFormValues).eggs || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: null,
|
product_warehouse_id: 0,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2185,7 +2068,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
|
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
|
||||||
const layingValues = formik.values as RecordingLayingFormValues;
|
const layingValues = formik.values as RecordingLayingFormValues;
|
||||||
if (!layingValues.eggs || layingValues.eggs.length === 0) {
|
if (!layingValues.eggs || layingValues.eggs.length === 0) {
|
||||||
setFieldValue('eggs', [{ product_warehouse_id: null, qty: '' }]);
|
setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
||||||
@@ -2907,15 +2790,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
key={`stock-product-${idx}-${stock.product_warehouse_id?.value}`}
|
key={`stock-product-${idx}-${stock.product_warehouse_id}`}
|
||||||
value={stock.product_warehouse_id}
|
value={
|
||||||
onInputChange={setStockInputValue}
|
unifiedStockProducts.find(
|
||||||
|
(product) =>
|
||||||
|
product.value === stock.product_warehouse_id
|
||||||
|
) || null
|
||||||
|
}
|
||||||
|
onInputChange={setStockProductInputValue}
|
||||||
onChange={(selectedOption) => {
|
onChange={(selectedOption) => {
|
||||||
const option =
|
const option =
|
||||||
selectedOption as OptionType | null;
|
selectedOption as OptionType | null;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`stocks.${idx}.product_warehouse_id`,
|
`stocks.${idx}.product_warehouse_id`,
|
||||||
option
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableStockProductOptions(idx)}
|
options={getAvailableStockProductOptions(idx)}
|
||||||
@@ -2951,9 +2839,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
}
|
}
|
||||||
isClearable={type !== 'detail'}
|
isClearable={type !== 'detail'}
|
||||||
inputPrefix={
|
inputPrefix={
|
||||||
stock.product_warehouse_id?.value
|
stock.product_warehouse_id
|
||||||
? getProductFlagBadgeAdornment(
|
? getProductFlagBadgeAdornment(
|
||||||
stock.product_warehouse_id.value
|
stock.product_warehouse_id
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -2989,7 +2877,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
inputSuffix={
|
inputSuffix={
|
||||||
stock.product_warehouse_id
|
stock.product_warehouse_id
|
||||||
? getProductUomSuffix(
|
? getProductUomSuffix(
|
||||||
stock.product_warehouse_id.value,
|
stock.product_warehouse_id,
|
||||||
'stock'
|
'stock'
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
@@ -3182,13 +3070,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
value={depletion.product_warehouse_id}
|
value={
|
||||||
|
depletionProducts.find(
|
||||||
|
(product) =>
|
||||||
|
product.value ===
|
||||||
|
depletion.product_warehouse_id
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(selectedOption) => {
|
onChange={(selectedOption) => {
|
||||||
const option =
|
const option =
|
||||||
selectedOption as OptionType | null;
|
selectedOption as OptionType | null;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`depletions.${idx}.product_warehouse_id`,
|
`depletions.${idx}.product_warehouse_id`,
|
||||||
option
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableDepletionProductOptions(idx)}
|
options={getAvailableDepletionProductOptions(idx)}
|
||||||
@@ -3251,7 +3145,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
inputSuffix={
|
inputSuffix={
|
||||||
depletion.product_warehouse_id
|
depletion.product_warehouse_id
|
||||||
? getProductUomSuffix(
|
? getProductUomSuffix(
|
||||||
depletion.product_warehouse_id.value,
|
depletion.product_warehouse_id,
|
||||||
'depletion'
|
'depletion'
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
@@ -3429,13 +3323,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
value={egg.product_warehouse_id}
|
value={
|
||||||
|
eggProducts.find(
|
||||||
|
(product) =>
|
||||||
|
product.value === egg.product_warehouse_id
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(selectedOption) => {
|
onChange={(selectedOption) => {
|
||||||
const option =
|
const option =
|
||||||
selectedOption as OptionType | null;
|
selectedOption as OptionType | null;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`eggs.${idx}.product_warehouse_id`,
|
`eggs.${idx}.product_warehouse_id`,
|
||||||
option
|
option?.value || 0
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={getAvailableEggProductOptions(idx)}
|
options={getAvailableEggProductOptions(idx)}
|
||||||
|
|||||||
+20
-32
@@ -40,9 +40,6 @@ const TransferToLayingDetailModal = () => {
|
|||||||
? transferToLayingResponse.data
|
? transferToLayingResponse.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const isTransferToLayingApproved =
|
|
||||||
transferToLaying?.approval.step_number === 2;
|
|
||||||
|
|
||||||
const { data: transferToLayingApprovalResponse } = useSWR(
|
const { data: transferToLayingApprovalResponse } = useSWR(
|
||||||
transferToLayingId
|
transferToLayingId
|
||||||
? ['approval-transfer-to-laying', transferToLayingId]
|
? ['approval-transfer-to-laying', transferToLayingId]
|
||||||
@@ -58,9 +55,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
const detailModal = useModal();
|
const detailModal = useModal();
|
||||||
|
|
||||||
const maxSourceQuantity =
|
const totalEnteredChickenForTransfer =
|
||||||
transferToLaying?.sources.reduce(
|
transferToLaying?.sources.reduce(
|
||||||
(acc, item) => acc + Number(item.product_warehouse.quantity),
|
(acc, item) => acc + Number(item.qty),
|
||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
@@ -70,9 +67,8 @@ const TransferToLayingDetailModal = () => {
|
|||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
|
|
||||||
const totalAvailableChickenForTransfer =
|
const totalAvailableChickenForTransfer =
|
||||||
maxSourceQuantity - totalTransferedChicken;
|
totalEnteredChickenForTransfer - totalTransferedChicken;
|
||||||
|
|
||||||
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
||||||
if (shouldPushToRoute) {
|
if (shouldPushToRoute) {
|
||||||
@@ -165,36 +161,13 @@ 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>
|
</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>
|
|
||||||
|
|
||||||
{transferToLaying?.sources.length === 0 && (
|
{transferToLaying?.sources.length === 0 && (
|
||||||
<span className='text-sm text-base-content/50 italic'>
|
<span className='text-sm text-base-content/50 italic'>
|
||||||
Belum ada kandang asal yang dipilih
|
Belum ada kandang asal yang dipilih
|
||||||
@@ -252,6 +225,21 @@ const TransferToLayingDetailModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0 ? 'error' : 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa transfer: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{transferToLaying?.targets.length === 0 && (
|
{transferToLaying?.targets.length === 0 && (
|
||||||
@@ -316,7 +304,7 @@ const TransferToLayingDetailModal = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
errorMessage={
|
errorMessage={
|
||||||
totalAvailableChickenForTransfer < 0
|
totalAvailableChickenForTransfer < 0
|
||||||
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
|
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+21
-10
@@ -13,6 +13,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Flock } from '@/types/api/master-data/flock';
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
|
import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying';
|
||||||
import {
|
import {
|
||||||
TransferToLayingFilterSchema,
|
TransferToLayingFilterSchema,
|
||||||
TransferToLayingFilterValues,
|
TransferToLayingFilterValues,
|
||||||
@@ -20,14 +21,12 @@ import {
|
|||||||
|
|
||||||
interface TransferToLayingFilterModal {
|
interface TransferToLayingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
initialValues?: Partial<TransferToLayingFilterValues>;
|
onSubmit?: (values: TransferToLayingFilter) => void;
|
||||||
onSubmit?: (values: TransferToLayingFilterValues) => void;
|
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransferToLayingFilterModal = ({
|
const TransferToLayingFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
initialValues: initialValuesProp,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: TransferToLayingFilterModal) => {
|
}: TransferToLayingFilterModal) => {
|
||||||
@@ -87,16 +86,28 @@ const TransferToLayingFilterModal = ({
|
|||||||
|
|
||||||
const formik = useFormik<TransferToLayingFilterValues>({
|
const formik = useFormik<TransferToLayingFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: initialValuesProp?.startDate ?? '',
|
startDate: '',
|
||||||
endDate: initialValuesProp?.endDate ?? '',
|
endDate: '',
|
||||||
flockSource: initialValuesProp?.flockSource ?? [],
|
flockSource: [],
|
||||||
flockDestination: initialValuesProp?.flockDestination ?? [],
|
flockDestination: [],
|
||||||
status: initialValuesProp?.status ?? [],
|
status: [],
|
||||||
},
|
},
|
||||||
enableReinitialize: true,
|
|
||||||
validationSchema: TransferToLayingFilterSchema,
|
validationSchema: TransferToLayingFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
onSubmit?.(values);
|
const formattedValues = {
|
||||||
|
...values,
|
||||||
|
flockSource: values.flockSource
|
||||||
|
? (values.flockSource as OptionType[]).map((item) => item.value)
|
||||||
|
: [],
|
||||||
|
flockDestination: values.flockDestination
|
||||||
|
? (values.flockDestination as OptionType[]).map((item) => item.value)
|
||||||
|
: [],
|
||||||
|
status: values.status
|
||||||
|
? (values.status as OptionType[]).map((item) => item.value)
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit?.(formattedValues as TransferToLayingFilter);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
|
|||||||
@@ -223,8 +223,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { flockSource: formikFlockSource } = formik.values;
|
|
||||||
|
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
|
|
||||||
const [selectedFlockSourceRawData, setSelectedFlockSourceRawData] = useState<
|
const [selectedFlockSourceRawData, setSelectedFlockSourceRawData] = useState<
|
||||||
@@ -457,13 +455,13 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(flockSourceRawData)) {
|
if (isResponseSuccess(flockSourceRawData)) {
|
||||||
const currentSelectedFlockSourceRawData = flockSourceRawData.data.find(
|
const selectedFlockSourceRawData = flockSourceRawData.data.find(
|
||||||
(item) => item.id === formik.values.flockSource?.value
|
(item) => item.id === formik.values.flockSource?.value
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedFlockSourceRawData(currentSelectedFlockSourceRawData);
|
setSelectedFlockSourceRawData(selectedFlockSourceRawData);
|
||||||
}
|
}
|
||||||
}, [flockSourceRawData, formikFlockSource]);
|
}, [flockSourceRawData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
formik.setFieldValue('totalQuantity', totalTransferedChicken);
|
||||||
@@ -627,7 +625,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-3'>
|
<div className='flex flex-row items-center gap-3'>
|
||||||
<input
|
<input
|
||||||
id={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
|
||||||
type='radio'
|
type='radio'
|
||||||
name='flockSourceKandang'
|
name='flockSourceKandang'
|
||||||
value={item.project_flock_kandang_id}
|
value={item.project_flock_kandang_id}
|
||||||
@@ -640,14 +637,13 @@ const TransferToLayingFormModal = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
htmlFor={`flock-source-kandang-${item.project_flock_kandang_id}`}
|
|
||||||
className={cn('text-sm text-base-content/50', {
|
className={cn('text-sm text-base-content/50', {
|
||||||
'cursor-pointer': isAvailable,
|
'cursor-pointer': isAvailable,
|
||||||
'cursor-not-allowed opacity-50': !isAvailable,
|
'cursor-not-allowed opacity-50': !isAvailable,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item.kandang_name}{' '}
|
{item.kandang_name}{' '}
|
||||||
<span className='text-base-content/20'>{`(Max: ${item.available_qty ?? '-'})`}</span>
|
<span className='text-base-content/20'>{`(Max: ${item.available_qty})`}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -822,35 +818,13 @@ 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
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
className='tooltip tooltip-error'
|
|
||||||
data-tip='required'
|
|
||||||
>
|
|
||||||
<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 ayam: ${formatNumber(
|
|
||||||
totalAvailableChickenForTransfer,
|
|
||||||
'en-US'
|
|
||||||
)} ekor`}
|
|
||||||
className={{
|
|
||||||
badge: 'text-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{formik.values.flockSourceKandangs.length === 0 && (
|
{formik.values.flockSourceKandangs.length === 0 && (
|
||||||
<span className='text-sm text-base-content/50 italic'>
|
<span className='text-sm text-base-content/50 italic'>
|
||||||
Belum ada kandang asal yang dipilih
|
Belum ada kandang asal yang dipilih
|
||||||
@@ -928,6 +902,23 @@ const TransferToLayingFormModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa transfer: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{formik.values.flockDestinationKandangs.length === 0 && (
|
{formik.values.flockDestinationKandangs.length === 0 && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -26,9 +26,10 @@ import TransferToLayingFilterModal from '@/components/pages/production/transfer-
|
|||||||
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
|
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
|
||||||
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
|
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
|
||||||
|
|
||||||
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
import {
|
||||||
import { TransferToLayingFilterValues } from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter';
|
TransferToLaying,
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
TransferToLayingFilter,
|
||||||
|
} from '@/types/api/production/transfer-to-laying';
|
||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -141,8 +142,6 @@ const TransferToLayingsTable = () => {
|
|||||||
status: '',
|
status: '',
|
||||||
filter_by: '',
|
filter_by: '',
|
||||||
sort_by: '',
|
sort_by: '',
|
||||||
flockSourceNames: '',
|
|
||||||
flockDestinationNames: '',
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -155,9 +154,6 @@ const TransferToLayingsTable = () => {
|
|||||||
filter_by: 'filter_by',
|
filter_by: 'filter_by',
|
||||||
sort_by: 'sort_by',
|
sort_by: 'sort_by',
|
||||||
},
|
},
|
||||||
excludeKeysFromUrl: ['flockSourceNames', 'flockDestinationNames'],
|
|
||||||
persist: true,
|
|
||||||
storeName: 'transfer-to-laying-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -435,84 +431,12 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_FILTER_OPTIONS = [
|
const filterSubmitHandler = (values: TransferToLayingFilter) => {
|
||||||
{ value: 'PENDING', label: 'Pengajuan' },
|
updateFilter('startDate', values.startDate);
|
||||||
{ value: 'APPROVED', label: 'Disetujui' },
|
updateFilter('endDate', values.endDate);
|
||||||
{ value: 'REJECTED', label: 'Ditolak' },
|
updateFilter('flockSource', values.flockSource.join(','));
|
||||||
];
|
updateFilter('flockDestination', values.flockDestination.join(','));
|
||||||
|
updateFilter('status', values.status.join(','));
|
||||||
const filterModalInitialValues = useMemo(() => {
|
|
||||||
const flockSourceIds = tableFilterState.flockSource
|
|
||||||
? tableFilterState.flockSource.split(',')
|
|
||||||
: [];
|
|
||||||
const flockSourceNameList = tableFilterState.flockSourceNames
|
|
||||||
? tableFilterState.flockSourceNames.split(',')
|
|
||||||
: [];
|
|
||||||
const flockSourceOptions = flockSourceIds.filter(Boolean).map((id, i) => ({
|
|
||||||
value: parseInt(id),
|
|
||||||
label: flockSourceNameList[i] || id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const flockDestIds = tableFilterState.flockDestination
|
|
||||||
? tableFilterState.flockDestination.split(',')
|
|
||||||
: [];
|
|
||||||
const flockDestNameList = tableFilterState.flockDestinationNames
|
|
||||||
? tableFilterState.flockDestinationNames.split(',')
|
|
||||||
: [];
|
|
||||||
const flockDestOptions = flockDestIds.filter(Boolean).map((id, i) => ({
|
|
||||||
value: parseInt(id),
|
|
||||||
label: flockDestNameList[i] || id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const statusIds = tableFilterState.status
|
|
||||||
? tableFilterState.status.split(',')
|
|
||||||
: [];
|
|
||||||
const statusOptions = statusIds.filter(Boolean).map((id) => {
|
|
||||||
const found = STATUS_FILTER_OPTIONS.find((opt) => opt.value === id);
|
|
||||||
return found || { value: id, label: id };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
startDate: tableFilterState.startDate || '',
|
|
||||||
endDate: tableFilterState.endDate || '',
|
|
||||||
flockSource: flockSourceOptions,
|
|
||||||
flockDestination: flockDestOptions,
|
|
||||||
status: statusOptions,
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
tableFilterState.startDate,
|
|
||||||
tableFilterState.endDate,
|
|
||||||
tableFilterState.flockSource,
|
|
||||||
tableFilterState.flockDestination,
|
|
||||||
tableFilterState.status,
|
|
||||||
tableFilterState.flockSourceNames,
|
|
||||||
tableFilterState.flockDestinationNames,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const filterSubmitHandler = (values: TransferToLayingFilterValues) => {
|
|
||||||
const flockSourceOpts = (values.flockSource as OptionType[]) || [];
|
|
||||||
const flockDestOpts = (values.flockDestination as OptionType[]) || [];
|
|
||||||
const statusOpts = (values.status as OptionType[]) || [];
|
|
||||||
|
|
||||||
updateFilter('startDate', values.startDate || '');
|
|
||||||
updateFilter('endDate', values.endDate || '');
|
|
||||||
updateFilter(
|
|
||||||
'flockSource',
|
|
||||||
flockSourceOpts.map((o) => String(o.value)).join(',')
|
|
||||||
);
|
|
||||||
updateFilter(
|
|
||||||
'flockDestination',
|
|
||||||
flockDestOpts.map((o) => String(o.value)).join(',')
|
|
||||||
);
|
|
||||||
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
|
|
||||||
updateFilter(
|
|
||||||
'flockSourceNames',
|
|
||||||
flockSourceOpts.map((o) => String(o.label)).join(',')
|
|
||||||
);
|
|
||||||
updateFilter(
|
|
||||||
'flockDestinationNames',
|
|
||||||
flockDestOpts.map((o) => String(o.label)).join(',')
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
@@ -521,8 +445,6 @@ const TransferToLayingsTable = () => {
|
|||||||
updateFilter('flockSource', '');
|
updateFilter('flockSource', '');
|
||||||
updateFilter('flockDestination', '');
|
updateFilter('flockDestination', '');
|
||||||
updateFilter('status', '');
|
updateFilter('status', '');
|
||||||
updateFilter('flockSourceNames', '');
|
|
||||||
updateFilter('flockDestinationNames', '');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportToExcelHandler = async () => {
|
const exportToExcelHandler = async () => {
|
||||||
@@ -636,8 +558,6 @@ const TransferToLayingsTable = () => {
|
|||||||
'search',
|
'search',
|
||||||
'filter_by',
|
'filter_by',
|
||||||
'sort_by',
|
'sort_by',
|
||||||
'flockSourceNames',
|
|
||||||
'flockDestinationNames',
|
|
||||||
]}
|
]}
|
||||||
fieldGroups={[['startDate', 'endDate']]}
|
fieldGroups={[['startDate', 'endDate']]}
|
||||||
onClick={filterModal.openModal}
|
onClick={filterModal.openModal}
|
||||||
@@ -750,7 +670,6 @@ const TransferToLayingsTable = () => {
|
|||||||
|
|
||||||
<TransferToLayingFilterModal
|
<TransferToLayingFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
initialValues={filterModalInitialValues}
|
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useState, useEffect, useMemo, useCallback } from 'react';
|
import { RefObject, useState, useEffect } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -9,49 +9,31 @@ import Modal from '@/components/Modal';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
import SelectInput from '@/components/input/SelectInput';
|
|
||||||
|
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { PurchaseFilter } from '@/types/api/purchase/purchase';
|
import { PurchaseFilter } from '@/types/api/purchase/purchase';
|
||||||
import { AreaApi, LocationApi, SupplierApi } from '@/services/api/master-data';
|
|
||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { Area } from '@/types/api/master-data/area';
|
|
||||||
import { Location } from '@/types/api/master-data/location';
|
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
|
||||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
|
|
||||||
interface PurchaseFilterModalProps {
|
interface PurchaseFilterModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
initialValues?: {
|
|
||||||
poDate: string;
|
|
||||||
category: OptionType<number>[];
|
|
||||||
status: OptionType<string>[];
|
|
||||||
supplier: OptionType<number> | null;
|
|
||||||
area: OptionType<number> | null;
|
|
||||||
location: OptionType<number> | null;
|
|
||||||
project_flock: OptionType<number> | null;
|
|
||||||
project_flock_kandang: OptionType<number> | null;
|
|
||||||
};
|
|
||||||
onSubmit?: (values: PurchaseFilter) => void;
|
onSubmit?: (values: PurchaseFilter) => void;
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PurchaseFilterModal = ({
|
const PurchaseFilterModal = ({
|
||||||
ref,
|
ref,
|
||||||
initialValues,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: PurchaseFilterModalProps) => {
|
}: PurchaseFilterModalProps) => {
|
||||||
const closeModalHandler = useCallback(() => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
}, [ref]);
|
};
|
||||||
|
|
||||||
// ===== DATE ERROR STATE =====
|
// ===== DATE ERROR STATE =====
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
// ===== CLEANUP TOAST ON UNMOUNT =====
|
// ===== CLEANUP TOAST ON UNMOUNT =====
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -91,134 +73,32 @@ const PurchaseFilterModal = ({
|
|||||||
'search'
|
'search'
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedAreaId, setSelectedAreaId] = useState(
|
|
||||||
initialValues?.area?.value ? String(initialValues.area.value) : ''
|
|
||||||
);
|
|
||||||
const [selectedLocationId, setSelectedLocationId] = useState(
|
|
||||||
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setSupplierInputValue,
|
|
||||||
options: supplierOptions,
|
|
||||||
isLoadingOptions: isLoadingSupplierOptions,
|
|
||||||
loadMore: loadMoreSuppliers,
|
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setAreaInputValue,
|
|
||||||
options: areaOptions,
|
|
||||||
isLoadingOptions: isLoadingAreaOptions,
|
|
||||||
loadMore: loadMoreAreas,
|
|
||||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setLocationInputValue,
|
|
||||||
options: locationOptions,
|
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
|
||||||
loadMore: loadMoreLocations,
|
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
|
|
||||||
area_id: selectedAreaId || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setProjectFlockInputValue,
|
|
||||||
options: projectFlockOptions,
|
|
||||||
rawData: projectFlocksRawData,
|
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
|
||||||
loadMore: loadMoreProjectFlocks,
|
|
||||||
} = useSelect<ProjectFlock>(
|
|
||||||
ProjectFlockApi.basePath,
|
|
||||||
'id',
|
|
||||||
'flock_name',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
location_id: selectedLocationId || '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const formik = useFormik<{
|
const formik = useFormik<{
|
||||||
poDate: string;
|
poDate: string;
|
||||||
category: { label: string; value: number }[];
|
category: { label: string; value: number }[];
|
||||||
status: { label: string; value: string }[];
|
status: { label: string; value: string }[];
|
||||||
supplier: OptionType<number> | null;
|
|
||||||
area: OptionType<number> | null;
|
|
||||||
location: OptionType<number> | null;
|
|
||||||
project_flock: OptionType<number> | null;
|
|
||||||
project_flock_kandang: OptionType<number> | null;
|
|
||||||
}>({
|
}>({
|
||||||
// enableReinitialize: true,
|
initialValues: {
|
||||||
initialValues: initialValues || {
|
|
||||||
poDate: '',
|
poDate: '',
|
||||||
category: [],
|
category: [],
|
||||||
status: [],
|
status: [],
|
||||||
supplier: null,
|
|
||||||
area: null,
|
|
||||||
location: null,
|
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
},
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues = {
|
const formattedValues = {
|
||||||
...values,
|
...values,
|
||||||
category: values.category.map((item) => String(item.value)),
|
category: values.category.map((item) => String(item.value)),
|
||||||
category_labels: values.category,
|
|
||||||
status: values.status.map((item) => String(item.value)),
|
status: values.status.map((item) => String(item.value)),
|
||||||
supplier_id: values.supplier?.value,
|
|
||||||
supplier_label: values.supplier?.label,
|
|
||||||
area_id: values.area?.value,
|
|
||||||
area_label: values.area?.label,
|
|
||||||
location_id: values.location?.value,
|
|
||||||
location_label: values.location?.label,
|
|
||||||
project_flock_id: values.project_flock?.value,
|
|
||||||
project_flock_label: values.project_flock?.label,
|
|
||||||
project_flock_kandang_id: values.project_flock_kandang?.value,
|
|
||||||
project_flock_kandang_label: values.project_flock_kandang?.label,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
setSelectedAreaId('');
|
|
||||||
setSelectedLocationId('');
|
|
||||||
onReset?.();
|
onReset?.();
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { resetForm, submitForm } = formik;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedAreaId(
|
|
||||||
initialValues?.area?.value ? String(initialValues.area.value) : ''
|
|
||||||
);
|
|
||||||
setSelectedLocationId(
|
|
||||||
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
|
||||||
);
|
|
||||||
}, [initialValues?.area, initialValues?.location]);
|
|
||||||
|
|
||||||
const projectFlockKandangOptions = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!formik.values.project_flock ||
|
|
||||||
!projectFlocksRawData ||
|
|
||||||
!isResponseSuccess(projectFlocksRawData)
|
|
||||||
) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedProjectFlock = projectFlocksRawData.data.find(
|
|
||||||
(item) => item.id === formik.values.project_flock?.value
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
selectedProjectFlock?.kandangs?.map((item) => ({
|
|
||||||
value: item.project_flock_kandang_id,
|
|
||||||
label: item.name,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [formik.values.project_flock, projectFlocksRawData]);
|
|
||||||
|
|
||||||
const productCategoryChangeHandler = (
|
const productCategoryChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
) => {
|
) => {
|
||||||
@@ -229,29 +109,6 @@ const PurchaseFilterModal = ({
|
|||||||
formik.setFieldValue('status', val);
|
formik.setFieldValue('status', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formikResetHandler = useCallback(() => {
|
|
||||||
resetForm({
|
|
||||||
values: {
|
|
||||||
poDate: '',
|
|
||||||
category: [],
|
|
||||||
status: [],
|
|
||||||
supplier: null,
|
|
||||||
area: null,
|
|
||||||
location: null,
|
|
||||||
project_flock: null,
|
|
||||||
project_flock_kandang: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setSelectedAreaId('');
|
|
||||||
setSelectedLocationId('');
|
|
||||||
onReset?.();
|
|
||||||
closeModalHandler();
|
|
||||||
}, [resetForm, onReset, closeModalHandler]);
|
|
||||||
|
|
||||||
const formikSubmitHandler = useCallback(async () => {
|
|
||||||
await submitForm();
|
|
||||||
}, [submitForm]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -261,7 +118,7 @@ const PurchaseFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formikResetHandler}
|
onReset={formik.handleReset}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -275,9 +132,7 @@ const PurchaseFilterModal = ({
|
|||||||
type='button'
|
type='button'
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
onClick={() => {
|
onClick={closeModalHandler}
|
||||||
closeModalHandler();
|
|
||||||
}}
|
|
||||||
className='p-0 text-base-content/50 hover:text-base-content'
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
@@ -317,108 +172,6 @@ const PurchaseFilterModal = ({
|
|||||||
value: item.step_name,
|
value: item.step_name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Vendor'
|
|
||||||
placeholder='Pilih Vendor'
|
|
||||||
value={formik.values.supplier}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'supplier',
|
|
||||||
!Array.isArray(val)
|
|
||||||
? (val as OptionType<number> | null)
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={supplierOptions}
|
|
||||||
isLoading={isLoadingSupplierOptions}
|
|
||||||
onInputChange={setSupplierInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreSuppliers}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Area'
|
|
||||||
placeholder='Pilih Area'
|
|
||||||
value={formik.values.area}
|
|
||||||
onChange={(val) => {
|
|
||||||
const nextValue = !Array.isArray(val)
|
|
||||||
? (val as OptionType<number> | null)
|
|
||||||
: null;
|
|
||||||
formik.setFieldValue('area', nextValue);
|
|
||||||
formik.setFieldValue('location', null);
|
|
||||||
formik.setFieldValue('project_flock', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang', null);
|
|
||||||
setSelectedAreaId(
|
|
||||||
nextValue?.value ? String(nextValue.value) : ''
|
|
||||||
);
|
|
||||||
setSelectedLocationId('');
|
|
||||||
}}
|
|
||||||
options={areaOptions}
|
|
||||||
isLoading={isLoadingAreaOptions}
|
|
||||||
onInputChange={setAreaInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreAreas}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Lokasi'
|
|
||||||
placeholder='Pilih Lokasi'
|
|
||||||
value={formik.values.location}
|
|
||||||
onChange={(val) => {
|
|
||||||
const nextValue = !Array.isArray(val)
|
|
||||||
? (val as OptionType<number> | null)
|
|
||||||
: null;
|
|
||||||
formik.setFieldValue('location', nextValue);
|
|
||||||
formik.setFieldValue('project_flock', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang', null);
|
|
||||||
setSelectedLocationId(
|
|
||||||
nextValue?.value ? String(nextValue.value) : ''
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={locationOptions}
|
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
onInputChange={setLocationInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
|
||||||
isClearable
|
|
||||||
isDisabled={!formik.values.area}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Project Flock'
|
|
||||||
placeholder='Pilih Project Flock'
|
|
||||||
value={formik.values.project_flock}
|
|
||||||
onChange={(val) => {
|
|
||||||
const nextValue = !Array.isArray(val)
|
|
||||||
? (val as OptionType<number> | null)
|
|
||||||
: null;
|
|
||||||
formik.setFieldValue('project_flock', nextValue);
|
|
||||||
formik.setFieldValue('project_flock_kandang', null);
|
|
||||||
}}
|
|
||||||
options={projectFlockOptions}
|
|
||||||
isLoading={isLoadingProjectFlockOptions}
|
|
||||||
onInputChange={setProjectFlockInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
|
||||||
isClearable
|
|
||||||
isDisabled={!formik.values.location}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Kandang'
|
|
||||||
placeholder='Pilih Kandang'
|
|
||||||
value={formik.values.project_flock_kandang}
|
|
||||||
onChange={(val) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
'project_flock_kandang',
|
|
||||||
!Array.isArray(val)
|
|
||||||
? (val as OptionType<number> | null)
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={projectFlockKandangOptions}
|
|
||||||
isClearable
|
|
||||||
isDisabled={!formik.values.project_flock}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -434,8 +187,7 @@ const PurchaseFilterModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='submit'
|
||||||
onClick={formikSubmitHandler}
|
|
||||||
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import {
|
import {
|
||||||
CellContext,
|
ChangeEventHandler,
|
||||||
ColumnDef,
|
useCallback,
|
||||||
SortingState,
|
useEffect,
|
||||||
Updater,
|
useMemo,
|
||||||
} from '@tanstack/react-table';
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
@@ -25,37 +28,17 @@ import StatusBadge from '@/components/helper/StatusBadge';
|
|||||||
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
|
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
|
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
|
||||||
|
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
|
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
|
||||||
import { PurchaseApi } from '@/services/api/purchase';
|
import { PurchaseApi } from '@/services/api/purchase';
|
||||||
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
|
import { Expense } from '@/types/api/expense';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
|
||||||
|
|
||||||
type PurchaseTableFilters = {
|
|
||||||
search: string;
|
|
||||||
sort_by: string;
|
|
||||||
order_by: string;
|
|
||||||
po_date: string;
|
|
||||||
approval_status: string;
|
|
||||||
product_category_id: string;
|
|
||||||
product_category_name: string;
|
|
||||||
supplier_id: string;
|
|
||||||
supplier_name: string;
|
|
||||||
area_id: string;
|
|
||||||
area_name: string;
|
|
||||||
location_id: string;
|
|
||||||
location_name: string;
|
|
||||||
project_flock_id: string;
|
|
||||||
project_flock_name: string;
|
|
||||||
project_flock_kandang_id: string;
|
|
||||||
project_flock_kandang_name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== STATUS BADGE UTILITIES =====
|
// ===== STATUS BADGE UTILITIES =====
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
@@ -164,94 +147,42 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PurchaseTable = () => {
|
const PurchaseTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// ===== STATE MANAGEMENT =====
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
// ===== TABLE FILTER STATE =====
|
// ===== TABLE FILTER STATE =====
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
setFilters,
|
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter<PurchaseTableFilters>({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
sort_by: '',
|
|
||||||
order_by: '',
|
|
||||||
po_date: '',
|
po_date: '',
|
||||||
approval_status: '',
|
approval_status: '',
|
||||||
product_category_id: '',
|
product_category_id: '',
|
||||||
product_category_name: '',
|
|
||||||
supplier_id: '',
|
|
||||||
supplier_name: '',
|
|
||||||
area_id: '',
|
|
||||||
area_name: '',
|
|
||||||
location_id: '',
|
|
||||||
location_name: '',
|
|
||||||
project_flock_id: '',
|
|
||||||
project_flock_name: '',
|
|
||||||
project_flock_kandang_id: '',
|
|
||||||
project_flock_kandang_name: '',
|
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
sort_by: 'sort_by',
|
|
||||||
order_by: 'sort_order',
|
|
||||||
po_date: 'po_date',
|
po_date: 'po_date',
|
||||||
approval_status: 'approval_status',
|
approval_status: 'approval_status',
|
||||||
product_category_id: 'product_category_id',
|
product_category_id: 'product_category_id',
|
||||||
supplier_id: 'supplier_id',
|
|
||||||
area_id: 'area_id',
|
|
||||||
location_id: 'location_id',
|
|
||||||
project_flock_id: 'project_flock_id',
|
|
||||||
project_flock_kandang_id: 'project_flock_kandang_id',
|
|
||||||
},
|
},
|
||||||
excludeKeysFromUrl: [
|
|
||||||
'product_category_name',
|
|
||||||
'supplier_name',
|
|
||||||
'area_name',
|
|
||||||
'location_name',
|
|
||||||
'project_flock_name',
|
|
||||||
'project_flock_kandang_name',
|
|
||||||
],
|
|
||||||
persist: true,
|
|
||||||
storeName: 'purchase-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== STATE MANAGEMENT =====
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
|
||||||
useState(false);
|
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
|
||||||
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
|
|
||||||
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
|
|
||||||
const sorting: SortingState = tableFilterState.sort_by
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: tableFilterState.sort_by,
|
|
||||||
desc: tableFilterState.order_by === 'desc',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
||||||
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
||||||
if (next.length > 0) {
|
|
||||||
updateFilter('sort_by', next[0].id, true);
|
|
||||||
updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
|
|
||||||
} else {
|
|
||||||
updateFilter('sort_by', '', true);
|
|
||||||
updateFilter('order_by', '', true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== MODAL HOOKS =====
|
// ===== MODAL HOOKS =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const exportProgressInputModal = useModal();
|
|
||||||
|
|
||||||
// ===== API DATA FETCHING =====
|
// ===== API DATA FETCHING =====
|
||||||
const {
|
const {
|
||||||
@@ -263,10 +194,36 @@ const PurchaseTable = () => {
|
|||||||
PurchaseApi.getAllFetcher
|
PurchaseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getKey = (
|
||||||
|
pageIndex: number,
|
||||||
|
previousPageData: BaseApiResponse<Expense>[] | null
|
||||||
|
) => {
|
||||||
|
if (pageIndex > 0 && !previousPageData) return null;
|
||||||
|
return `${ExpenseApi.basePath}?page=${pageIndex + 1}&limit=100`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: expensesPages } = useSWRInfinite(
|
||||||
|
getKey,
|
||||||
|
ExpenseApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const expenseMap = useMemo(() => {
|
||||||
|
const map = new Map<string, number>();
|
||||||
|
if (!expensesPages) return map;
|
||||||
|
|
||||||
|
expensesPages.forEach((page) => {
|
||||||
|
if (isResponseSuccess(page)) {
|
||||||
|
page.data.forEach((expense: Expense) => {
|
||||||
|
map.set(expense.reference_number, expense.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [expensesPages]);
|
||||||
|
|
||||||
// ===== TABLE COLUMNS DEFINITION =====
|
// ===== TABLE COLUMNS DEFINITION =====
|
||||||
const purchaseColumns: ColumnDef<Purchase>[] = [
|
const purchaseColumns: ColumnDef<Purchase>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'po_number',
|
|
||||||
header: 'No. PR/PO',
|
header: 'No. PR/PO',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const { pr_number, po_number } = props.row.original;
|
const { pr_number, po_number } = props.row.original;
|
||||||
@@ -282,16 +239,20 @@ const PurchaseTable = () => {
|
|||||||
return (
|
return (
|
||||||
<ul className='list-disc pl-4'>
|
<ul className='list-disc pl-4'>
|
||||||
{poExpedition.map((exp, index) => {
|
{poExpedition.map((exp, index) => {
|
||||||
|
const expenseId = expenseMap.get(exp.refrence);
|
||||||
|
if (expenseId) {
|
||||||
return (
|
return (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
<Link
|
<Link
|
||||||
href={`/expense/detail/?expenseId=${exp.id}`}
|
href={`/expense/detail/?expenseId=${expenseId}`}
|
||||||
className='p-0 h-auto text-primary underline'
|
className='p-0 h-auto text-primary underline'
|
||||||
>
|
>
|
||||||
{exp.refrence}
|
{exp.refrence}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return <li key={index}>{exp.refrence}</li>;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
@@ -308,7 +269,7 @@ const PurchaseTable = () => {
|
|||||||
cell: (props) => props.row.original.requester_name || '-',
|
cell: (props) => props.row.original.requester_name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'products',
|
accessorKey: 'products.name',
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const products = props.row.original.products;
|
const products = props.row.original.products;
|
||||||
@@ -323,7 +284,7 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'location',
|
accessorKey: 'location.name',
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
cell: (props) => props.row.original.location?.name || '-',
|
cell: (props) => props.row.original.location?.name || '-',
|
||||||
},
|
},
|
||||||
@@ -335,14 +296,6 @@ const PurchaseTable = () => {
|
|||||||
? formatDate(props.row.original.po_date, 'DD MMM YYYY')
|
? formatDate(props.row.original.po_date, 'DD MMM YYYY')
|
||||||
: '-',
|
: '-',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'received_date',
|
|
||||||
header: 'Tgl. Terima',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.received_date
|
|
||||||
? formatDate(props.row.original.received_date, 'DD MMM YYYY')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'due_date',
|
accessorKey: 'due_date',
|
||||||
header: 'Jatuh Tempo',
|
header: 'Jatuh Tempo',
|
||||||
@@ -353,7 +306,6 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aging',
|
header: 'Aging',
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const purchase = props.row.original;
|
const purchase = props.row.original;
|
||||||
if (!purchase.po_date) return '-';
|
if (!purchase.po_date) return '-';
|
||||||
@@ -365,7 +317,6 @@ const PurchaseTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
|
||||||
header: 'Status Approval',
|
header: 'Status Approval',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -410,14 +361,6 @@ const PurchaseTable = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
header: 'Tanggal Dibuat',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.created_at
|
|
||||||
? formatDate(props.row.original.created_at, 'DD MMM YYYY')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -449,17 +392,10 @@ const PurchaseTable = () => {
|
|||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deleteResponse = await PurchaseApi.delete(
|
await PurchaseApi.delete(selectedPurchase?.id as number);
|
||||||
selectedPurchase?.id as number
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseSuccess(deleteResponse)) {
|
|
||||||
refreshPurchaseRequests();
|
refreshPurchaseRequests();
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
toast.success('Berhasil menghapus data permintaan pembelian!');
|
toast.success('Berhasil menghapus data permintaan pembelian!');
|
||||||
} else {
|
|
||||||
toast.error(deleteResponse?.message ?? 'Gagal menghapus data!');
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal menghapus data permintaan pembelian!');
|
toast.error('Gagal menghapus data permintaan pembelian!');
|
||||||
}
|
}
|
||||||
@@ -467,191 +403,34 @@ const PurchaseTable = () => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
|
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('purchase-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
setSearchValue(e.target.value);
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
},
|
},
|
||||||
[updateFilter]
|
[updateFilter, setSearchValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterSubmitHandler = (values: PurchaseFilter) => {
|
const filterSubmitHandler = (values: PurchaseFilter) => {
|
||||||
setFilters({
|
updateFilter('po_date', values.poDate);
|
||||||
po_date: values.poDate,
|
updateFilter('product_category_id', values.category.join(','));
|
||||||
product_category_id: values.category.join(','),
|
updateFilter('approval_status', values.status.join(','));
|
||||||
product_category_name:
|
|
||||||
values.category_labels?.map((item) => item.label).join(',') || '',
|
|
||||||
approval_status: values.status.join(','),
|
|
||||||
supplier_id: values.supplier_id ? String(values.supplier_id) : '',
|
|
||||||
supplier_name: values.supplier_label || '',
|
|
||||||
area_id: values.area_id ? String(values.area_id) : '',
|
|
||||||
area_name: values.area_label || '',
|
|
||||||
location_id: values.location_id ? String(values.location_id) : '',
|
|
||||||
location_name: values.location_label || '',
|
|
||||||
project_flock_id: values.project_flock_id
|
|
||||||
? String(values.project_flock_id)
|
|
||||||
: '',
|
|
||||||
project_flock_name: values.project_flock_label || '',
|
|
||||||
project_flock_kandang_id: values.project_flock_kandang_id
|
|
||||||
? String(values.project_flock_kandang_id)
|
|
||||||
: '',
|
|
||||||
project_flock_kandang_name: values.project_flock_kandang_label || '',
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
setFilters({
|
updateFilter('po_date', '');
|
||||||
po_date: '',
|
updateFilter('product_category_id', '');
|
||||||
product_category_id: '',
|
updateFilter('approval_status', '');
|
||||||
product_category_name: '',
|
|
||||||
approval_status: '',
|
|
||||||
supplier_id: '',
|
|
||||||
supplier_name: '',
|
|
||||||
area_id: '',
|
|
||||||
area_name: '',
|
|
||||||
location_id: '',
|
|
||||||
location_name: '',
|
|
||||||
project_flock_id: '',
|
|
||||||
project_flock_name: '',
|
|
||||||
project_flock_kandang_id: '',
|
|
||||||
project_flock_kandang_name: '',
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const purchaseFilterInitialValues = useMemo(() => {
|
|
||||||
const categoryIds = tableFilterState.product_category_id
|
|
||||||
? tableFilterState.product_category_id
|
|
||||||
.split(',')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
const categoryLabels = tableFilterState.product_category_name
|
|
||||||
? tableFilterState.product_category_name
|
|
||||||
.split(',')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
const approvalStatuses = tableFilterState.approval_status
|
|
||||||
? tableFilterState.approval_status
|
|
||||||
.split(',')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
poDate: tableFilterState.po_date,
|
|
||||||
category: categoryIds.map((value, index) => ({
|
|
||||||
value: Number(value),
|
|
||||||
label: categoryLabels[index] || value,
|
|
||||||
})),
|
|
||||||
status: approvalStatuses.map((value) => ({
|
|
||||||
value,
|
|
||||||
label:
|
|
||||||
PURCHASE_ORDER_APPROVAL_LINE.find((item) => item.step_name === value)
|
|
||||||
?.step_name || value,
|
|
||||||
})),
|
|
||||||
supplier: tableFilterState.supplier_id
|
|
||||||
? ({
|
|
||||||
value: Number(tableFilterState.supplier_id),
|
|
||||||
label:
|
|
||||||
tableFilterState.supplier_name || tableFilterState.supplier_id,
|
|
||||||
} as OptionType<number>)
|
|
||||||
: null,
|
|
||||||
area: tableFilterState.area_id
|
|
||||||
? ({
|
|
||||||
value: Number(tableFilterState.area_id),
|
|
||||||
label: tableFilterState.area_name || tableFilterState.area_id,
|
|
||||||
} as OptionType<number>)
|
|
||||||
: null,
|
|
||||||
location: tableFilterState.location_id
|
|
||||||
? ({
|
|
||||||
value: Number(tableFilterState.location_id),
|
|
||||||
label:
|
|
||||||
tableFilterState.location_name || tableFilterState.location_id,
|
|
||||||
} as OptionType<number>)
|
|
||||||
: null,
|
|
||||||
project_flock: tableFilterState.project_flock_id
|
|
||||||
? ({
|
|
||||||
value: Number(tableFilterState.project_flock_id),
|
|
||||||
label:
|
|
||||||
tableFilterState.project_flock_name ||
|
|
||||||
tableFilterState.project_flock_id,
|
|
||||||
} as OptionType<number>)
|
|
||||||
: null,
|
|
||||||
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
|
||||||
? ({
|
|
||||||
value: Number(tableFilterState.project_flock_kandang_id),
|
|
||||||
label:
|
|
||||||
tableFilterState.project_flock_kandang_name ||
|
|
||||||
tableFilterState.project_flock_kandang_id,
|
|
||||||
} as OptionType<number>)
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const exportToExcel = useCallback(async () => {
|
|
||||||
setIsLoadingExportingToExcel(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await PurchaseApi.exportToExcel(getTableFilterQueryString());
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
await getErrorMessage(error, 'Gagal mengekspor data pembelian')
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingExportingToExcel(false);
|
|
||||||
}
|
|
||||||
}, [getTableFilterQueryString]);
|
|
||||||
|
|
||||||
const resetExportProgressForm = useCallback(() => {
|
|
||||||
setExportProgressStartDate('');
|
|
||||||
setExportProgressEndDate('');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const exportProgressStartDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
|
||||||
useCallback((e) => {
|
|
||||||
setExportProgressStartDate(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const exportProgressEndDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
|
||||||
useCallback((e) => {
|
|
||||||
setExportProgressEndDate(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const exportProgressInputToExcelClickHandler = useCallback(() => {
|
|
||||||
resetExportProgressForm();
|
|
||||||
exportProgressInputModal.openModal();
|
|
||||||
}, [exportProgressInputModal, resetExportProgressForm]);
|
|
||||||
|
|
||||||
const submitExportProgressInputHandler = useCallback(async () => {
|
|
||||||
if (!exportProgressStartDate || !exportProgressEndDate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsExportProgressLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await PurchaseApi.exportInputProgressToExcel(
|
|
||||||
exportProgressStartDate,
|
|
||||||
exportProgressEndDate
|
|
||||||
);
|
|
||||||
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
toast.success('Ekspor berhasil');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsExportProgressLoading(false);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
exportProgressEndDate,
|
|
||||||
exportProgressInputModal,
|
|
||||||
exportProgressStartDate,
|
|
||||||
resetExportProgressForm,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -698,73 +477,11 @@ const PurchaseTable = () => {
|
|||||||
'search',
|
'search',
|
||||||
'filter_by',
|
'filter_by',
|
||||||
'sort_by',
|
'sort_by',
|
||||||
'order_by',
|
|
||||||
'product_category_name',
|
|
||||||
'supplier_name',
|
|
||||||
'area_name',
|
|
||||||
'location_name',
|
|
||||||
'project_flock_name',
|
|
||||||
'project_flock_kandang_name',
|
|
||||||
]}
|
]}
|
||||||
fieldGroups={[['startDate', 'endDate']]}
|
fieldGroups={[['startDate', 'endDate']]}
|
||||||
onClick={filterModal.openModal}
|
onClick={filterModal.openModal}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
align='end'
|
|
||||||
direction='bottom'
|
|
||||||
className={{
|
|
||||||
content:
|
|
||||||
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
|
|
||||||
}}
|
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
|
||||||
>
|
|
||||||
<div className='flex flex-row items-center gap-1.5'>
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:cloud-arrow-down'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span>Export</span>
|
|
||||||
|
|
||||||
<div className='w-px self-stretch bg-base-content/10' />
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:chevron-down'
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={exportToExcel}
|
|
||||||
isLoading={isLoadingExportingToExcel}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Ekspor ke Excel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={exportProgressInputToExcelClickHandler}
|
|
||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
|
||||||
Ekspor Input Progress (Excel)
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -812,8 +529,7 @@ const PurchaseTable = () => {
|
|||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={handleSortingChange}
|
setSorting={setSorting}
|
||||||
manualSorting
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn('p-3 mb-0'),
|
containerClassName: cn('p-3 mb-0'),
|
||||||
headerColumnClassName: 'text-nowrap',
|
headerColumnClassName: 'text-nowrap',
|
||||||
@@ -827,7 +543,6 @@ const PurchaseTable = () => {
|
|||||||
|
|
||||||
<PurchaseFilterModal
|
<PurchaseFilterModal
|
||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
initialValues={purchaseFilterInitialValues}
|
|
||||||
onSubmit={filterSubmitHandler}
|
onSubmit={filterSubmitHandler}
|
||||||
onReset={filterResetHandler}
|
onReset={filterResetHandler}
|
||||||
/>
|
/>
|
||||||
@@ -847,76 +562,6 @@ const PurchaseTable = () => {
|
|||||||
onClick: confirmationModalDeleteClickHandler,
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
|
||||||
ref={exportProgressInputModal.ref}
|
|
||||||
className={{
|
|
||||||
modalBox: 'max-w-lg rounded-lg p-0',
|
|
||||||
}}
|
|
||||||
closeOnBackdrop
|
|
||||||
>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
|
||||||
<h4 className='text-sm font-semibold text-base-content'>
|
|
||||||
Ekspor Input Progress
|
|
||||||
</h4>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
}}
|
|
||||||
className='p-1'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:close' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
|
||||||
<DateInput
|
|
||||||
name='export_progress_start_date'
|
|
||||||
label='Tanggal Mulai'
|
|
||||||
value={exportProgressStartDate}
|
|
||||||
onChange={exportProgressStartDateChangeHandler}
|
|
||||||
isNestedModal
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DateInput
|
|
||||||
name='export_progress_end_date'
|
|
||||||
label='Tanggal Selesai'
|
|
||||||
value={exportProgressEndDate}
|
|
||||||
onChange={exportProgressEndDateChangeHandler}
|
|
||||||
isNestedModal
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
exportProgressInputModal.closeModal();
|
|
||||||
resetExportProgressForm();
|
|
||||||
}}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color='success'
|
|
||||||
onClick={submitExportProgressInputHandler}
|
|
||||||
isLoading={isExportProgressLoading}
|
|
||||||
disabled={!exportProgressStartDate || !exportProgressEndDate}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const PurchaseRequestForm = ({
|
|||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const [, setLocationSelectInputValue] = useState('');
|
||||||
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
|
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -162,7 +163,6 @@ const PurchaseRequestForm = ({
|
|||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
loadMore: loadMoreLocations,
|
loadMore: loadMoreLocations,
|
||||||
setInputValue: setLocationSelectInputValue,
|
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ import PurchaseOrderAcceptApprovalForm from '@/components/pages/purchase/form/or
|
|||||||
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
|
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import TextArea from '@/components/input/TextArea';
|
|
||||||
import {
|
import {
|
||||||
CreateAcceptApprovalRequestPayload,
|
CreateAcceptApprovalRequestPayload,
|
||||||
CreateManagerApprovalRequestPayload,
|
CreateManagerApprovalRequestPayload,
|
||||||
@@ -98,7 +96,6 @@ const PurchaseOrderDetail = ({
|
|||||||
const acceptRejectionModal = useModal();
|
const acceptRejectionModal = useModal();
|
||||||
const managerRejectionModal = useModal();
|
const managerRejectionModal = useModal();
|
||||||
const editModal = useModal();
|
const editModal = useModal();
|
||||||
const editPoDateModal = useModal();
|
|
||||||
const penerimaanBarangModal = useModal();
|
const penerimaanBarangModal = useModal();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
|
|
||||||
@@ -108,9 +105,6 @@ const PurchaseOrderDetail = ({
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
const [managerApprovalNotes, setManagerApprovalNotes] = useState('');
|
|
||||||
const [managerApprovalPoDate, setManagerApprovalPoDate] = useState('');
|
|
||||||
const [editPoDate, setEditPoDate] = useState('');
|
|
||||||
|
|
||||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
parseInt(item)
|
parseInt(item)
|
||||||
@@ -218,8 +212,6 @@ const PurchaseOrderDetail = ({
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
setApprovalNotes('');
|
setApprovalNotes('');
|
||||||
setManagerApprovalNotes('');
|
|
||||||
setManagerApprovalPoDate('');
|
|
||||||
confirmationModalWithNotes.openModal();
|
confirmationModalWithNotes.openModal();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
@@ -422,50 +414,17 @@ const PurchaseOrderDetail = ({
|
|||||||
deleteModal,
|
deleteModal,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updatePoDateHandler = useCallback(async () => {
|
|
||||||
const purchaseRequestId = searchParams.get('purchaseId')
|
|
||||||
? parseInt(searchParams.get('purchaseId')!)
|
|
||||||
: initialValues?.id || 1;
|
|
||||||
|
|
||||||
if (!purchaseRequestId) {
|
|
||||||
toast.error('Purchase Request ID is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await PurchaseApi.updatePoDate(purchaseRequestId, {
|
|
||||||
po_date: editPoDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isResponseError(res)) {
|
|
||||||
toast.error(res.message || 'Gagal mengubah tanggal PO');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('Tanggal PO berhasil diubah');
|
|
||||||
setEditPoDate('');
|
|
||||||
editPoDateModal.closeModal();
|
|
||||||
refetchData?.();
|
|
||||||
}, [
|
|
||||||
initialValues?.id,
|
|
||||||
searchParams,
|
|
||||||
editPoDate,
|
|
||||||
editPoDateModal,
|
|
||||||
refetchData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// ===== APPROVAL/REJECTION HANDLERS =====
|
// ===== APPROVAL/REJECTION HANDLERS =====
|
||||||
const managerApprovalHandler = async () => {
|
const managerApprovalHandler = async (notes: string) => {
|
||||||
const payload: CreateManagerApprovalRequestPayload = {
|
const payload: CreateManagerApprovalRequestPayload = {
|
||||||
action: 'APPROVED',
|
action: 'APPROVED',
|
||||||
notes: managerApprovalNotes || null,
|
notes: notes || null,
|
||||||
po_date: managerApprovalPoDate || null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await createManagerApprovalHandler(payload);
|
await createManagerApprovalHandler(payload);
|
||||||
await refreshApprovals();
|
await refreshApprovals();
|
||||||
await refetchData?.();
|
await refetchData?.();
|
||||||
setManagerApprovalNotes('');
|
setApprovalNotes('');
|
||||||
setManagerApprovalPoDate('');
|
|
||||||
confirmationModalWithNotes.closeModal();
|
confirmationModalWithNotes.closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -870,41 +829,6 @@ const PurchaseOrderDetail = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{purchaseData.po_date &&
|
|
||||||
!purchaseData.po_date.startsWith('0001') && (
|
|
||||||
<div className='group'>
|
|
||||||
<div className='flex items-start'>
|
|
||||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
|
||||||
Tanggal PO
|
|
||||||
</span>
|
|
||||||
<div className='ml-3 flex items-center gap-1'>
|
|
||||||
<span className='text-gray-900'>
|
|
||||||
: {formatDate(purchaseData.po_date, 'DD MMM YYYY')}
|
|
||||||
</span>
|
|
||||||
<RequirePermission permissions='lti.purchase.update'>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
color='warning'
|
|
||||||
className='p-1 min-h-0 h-auto'
|
|
||||||
onClick={() => {
|
|
||||||
setEditPoDate(
|
|
||||||
formatDate(purchaseData.po_date, 'YYYY-MM-DD')
|
|
||||||
);
|
|
||||||
editPoDateModal.openModal();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:edit-outline'
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1092,79 +1016,27 @@ const PurchaseOrderDetail = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Manager Approval Modal */}
|
{/* Confirmation Modal with Notes */}
|
||||||
<Modal
|
<ConfirmationModalWithNotes
|
||||||
ref={confirmationModalWithNotes.ref}
|
ref={confirmationModalWithNotes.ref}
|
||||||
closeOnBackdrop
|
type='success'
|
||||||
className={{
|
text='Apakah Anda yakin ingin melanjutkan approval ini?'
|
||||||
modalBox: 'max-w-lg rounded-lg p-0',
|
placeholder='(Opsional) Tambahkan catatan untuk approval ini...'
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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'>
|
|
||||||
Konfirmasi Approval Manager
|
|
||||||
</h4>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
setManagerApprovalNotes('');
|
|
||||||
setManagerApprovalPoDate('');
|
|
||||||
confirmationModalWithNotes.closeModal();
|
|
||||||
}}
|
|
||||||
className='p-1'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:close' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
|
||||||
<p className='text-sm text-base-content/70'>
|
|
||||||
Apakah Anda yakin ingin melanjutkan approval ini?
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<DateInput
|
|
||||||
name='manager_approval_po_date'
|
|
||||||
label='Tanggal PO'
|
|
||||||
value={managerApprovalPoDate}
|
|
||||||
onChange={(e) => setManagerApprovalPoDate(e.target.value)}
|
|
||||||
isNestedModal
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextArea
|
|
||||||
name='manager_approval_notes'
|
|
||||||
label='Catatan (Opsional)'
|
|
||||||
placeholder='Tambahkan catatan untuk approval ini...'
|
|
||||||
value={managerApprovalNotes}
|
|
||||||
onChange={(e) => setManagerApprovalNotes(e.target.value)}
|
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
closeOnBackdrop
|
||||||
</div>
|
primaryButton={{
|
||||||
|
text: 'Ya, Lanjutkan',
|
||||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
color: 'success',
|
||||||
<Button
|
onClick: managerApprovalHandler,
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
setManagerApprovalNotes('');
|
|
||||||
setManagerApprovalPoDate('');
|
|
||||||
confirmationModalWithNotes.closeModal();
|
|
||||||
}}
|
}}
|
||||||
className='px-3 py-2.5'
|
secondaryButton={{
|
||||||
>
|
text: 'Batal',
|
||||||
Batal
|
onClick: () => {
|
||||||
</Button>
|
setApprovalNotes('');
|
||||||
<Button
|
confirmationModalWithNotes.closeModal();
|
||||||
color='success'
|
},
|
||||||
onClick={managerApprovalHandler}
|
}}
|
||||||
className='px-3 py-2.5'
|
/>
|
||||||
>
|
|
||||||
Ya, Lanjutkan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Staff Approval Modal */}
|
{/* Staff Approval Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -1240,66 +1112,6 @@ const PurchaseOrderDetail = ({
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Edit PO Date Modal */}
|
|
||||||
<Modal
|
|
||||||
ref={editPoDateModal.ref}
|
|
||||||
closeOnBackdrop
|
|
||||||
className={{
|
|
||||||
modalBox: 'max-w-sm rounded-lg p-0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
|
|
||||||
<h4 className='text-sm font-semibold text-base-content'>
|
|
||||||
Edit Tanggal PO
|
|
||||||
</h4>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
setEditPoDate('');
|
|
||||||
editPoDateModal.closeModal();
|
|
||||||
}}
|
|
||||||
className='p-1'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:close' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
|
||||||
<DateInput
|
|
||||||
name='edit_po_date'
|
|
||||||
label='Tanggal PO'
|
|
||||||
value={editPoDate}
|
|
||||||
onChange={(e) => setEditPoDate(e.target.value)}
|
|
||||||
isNestedModal
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='none'
|
|
||||||
onClick={() => {
|
|
||||||
setEditPoDate('');
|
|
||||||
editPoDateModal.closeModal();
|
|
||||||
}}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color='primary'
|
|
||||||
onClick={updatePoDateHandler}
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
disabled={!editPoDate}
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Staff Rejection Modal */}
|
{/* Staff Rejection Modal */}
|
||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={staffRejectionModal.ref}
|
ref={staffRejectionModal.ref}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { useState } from 'react';
|
|||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import ReportExpenseTab from '@/components/pages/report/expense/tab/ReportExpenseTab';
|
import ReportExpenseTab from './tab/ReportExpenseTab';
|
||||||
import ReportDepreciationTab from '@/components/pages/report/expense/tab/ReportDepreciationTab';
|
|
||||||
|
|
||||||
const ReportExpenseTabs = () => {
|
const ReportExpenseTabs = () => {
|
||||||
const [activeTabId, setActiveTabId] = useState<string>('1');
|
const [activeTabId, setActiveTabId] = useState<string>('1');
|
||||||
@@ -17,11 +16,6 @@ const ReportExpenseTabs = () => {
|
|||||||
label: 'Laporan Biaya Operasional',
|
label: 'Laporan Biaya Operasional',
|
||||||
content: <ReportExpenseTab tabId={'1'} />,
|
content: <ReportExpenseTab tabId={'1'} />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
label: 'Laporan Depresiasi',
|
|
||||||
content: <ReportDepreciationTab tabId={'2'} />,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
type ReportSkeletonColumn<TData extends object> =
|
type ReportExpenseColumn =
|
||||||
| ColumnDef<TData>
|
| ColumnDef<ReportExpense>
|
||||||
| {
|
| {
|
||||||
header: string;
|
header: string;
|
||||||
columns: Array<{
|
columns: Array<{
|
||||||
header: string;
|
header: string;
|
||||||
accessorKey?: string;
|
accessorKey?: string;
|
||||||
cell?: (props: { row: { original: TData } }) => React.ReactNode;
|
cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ReportExpenseSkeleton = <TData extends object>({
|
const ReportExpenseSkeleton = ({
|
||||||
columns,
|
columns,
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
}: {
|
}: {
|
||||||
columns: ReportSkeletonColumn<TData>[];
|
columns: ReportExpenseColumn[];
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { RefObject, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useFormik } from 'formik';
|
|
||||||
import * as yup from 'yup';
|
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import Modal from '@/components/Modal';
|
|
||||||
import Button from '@/components/Button';
|
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import SelectInput, {
|
|
||||||
OptionType,
|
|
||||||
useSelect,
|
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
|
|
||||||
import { AreaApi, LocationApi } from '@/services/api/master-data';
|
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
|
||||||
import { Area } from '@/types/api/master-data/area';
|
|
||||||
import { Location } from '@/types/api/master-data/location';
|
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
|
||||||
|
|
||||||
export type ReportDepreciationFilterValues = {
|
|
||||||
area_id: string | null;
|
|
||||||
location_id: string | null;
|
|
||||||
project_flock_id: string | null;
|
|
||||||
period: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ReportDepreciationFilterSchema = yup.object({
|
|
||||||
area_id: yup.string().nullable(),
|
|
||||||
location_id: yup.string().nullable(),
|
|
||||||
project_flock_id: yup.string().nullable(),
|
|
||||||
period: yup.string().nullable().required('Periode wajib dipilih'),
|
|
||||||
}) as yup.ObjectSchema<ReportDepreciationFilterValues>;
|
|
||||||
|
|
||||||
interface ReportDepreciationFilterModalProps {
|
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
|
||||||
initialValues?: ReportDepreciationFilterValues;
|
|
||||||
onSubmit?: (values: Partial<ReportDepreciationFilterValues>) => void;
|
|
||||||
onReset?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultInitialValues: ReportDepreciationFilterValues = {
|
|
||||||
area_id: null,
|
|
||||||
location_id: null,
|
|
||||||
project_flock_id: null,
|
|
||||||
period: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReportDepreciationFilterModal = ({
|
|
||||||
ref,
|
|
||||||
initialValues,
|
|
||||||
onSubmit,
|
|
||||||
onReset,
|
|
||||||
}: ReportDepreciationFilterModalProps) => {
|
|
||||||
const [selectedAreaId, setSelectedAreaId] = useState<string | undefined>(
|
|
||||||
initialValues?.area_id || undefined
|
|
||||||
);
|
|
||||||
const [selectedLocationId, setSelectedLocationId] = useState<
|
|
||||||
string | undefined
|
|
||||||
>(initialValues?.location_id || undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedAreaId(initialValues?.area_id || undefined);
|
|
||||||
setSelectedLocationId(initialValues?.location_id || undefined);
|
|
||||||
}, [initialValues?.area_id, initialValues?.location_id]);
|
|
||||||
|
|
||||||
const closeModalHandler = () => {
|
|
||||||
ref.current?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setAreaInputValue,
|
|
||||||
options: areaOptions,
|
|
||||||
isLoadingOptions: isLoadingAreaOptions,
|
|
||||||
loadMore: loadMoreAreas,
|
|
||||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setLocationInputValue,
|
|
||||||
options: locationOptions,
|
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
|
||||||
loadMore: loadMoreLocations,
|
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
|
|
||||||
area_id: selectedAreaId || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setProjectFlockInputValue,
|
|
||||||
options: projectFlockOptions,
|
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
|
||||||
loadMore: loadMoreProjectFlocks,
|
|
||||||
} = useSelect<ProjectFlock>(
|
|
||||||
ProjectFlockApi.basePath,
|
|
||||||
'id',
|
|
||||||
'flock_name',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
location_id: selectedLocationId || '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const formik = useFormik<ReportDepreciationFilterValues>({
|
|
||||||
initialValues: initialValues || defaultInitialValues,
|
|
||||||
enableReinitialize: true,
|
|
||||||
validationSchema: ReportDepreciationFilterSchema,
|
|
||||||
onSubmit: async (values) => {
|
|
||||||
onSubmit?.(values);
|
|
||||||
closeModalHandler();
|
|
||||||
},
|
|
||||||
onReset: (_) => {
|
|
||||||
onReset?.();
|
|
||||||
closeModalHandler();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const areaValue = useMemo(() => {
|
|
||||||
if (!formik.values.area_id) return null;
|
|
||||||
return (
|
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.area_id, areaOptions]);
|
|
||||||
|
|
||||||
const locationValue = useMemo(() => {
|
|
||||||
if (!formik.values.location_id) return null;
|
|
||||||
return (
|
|
||||||
locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.location_id, locationOptions]);
|
|
||||||
|
|
||||||
const projectFlockValue = useMemo(() => {
|
|
||||||
if (!formik.values.project_flock_id) return null;
|
|
||||||
return (
|
|
||||||
projectFlockOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.project_flock_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.project_flock_id, projectFlockOptions]);
|
|
||||||
|
|
||||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const areaId = val && !Array.isArray(val) ? String(val.value) : null;
|
|
||||||
|
|
||||||
setSelectedAreaId(areaId || undefined);
|
|
||||||
formik.setFieldValue('area_id', areaId);
|
|
||||||
formik.setFieldValue('location_id', null);
|
|
||||||
formik.setFieldValue('project_flock_id', null);
|
|
||||||
setSelectedLocationId(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const locationId = val && !Array.isArray(val) ? String(val.value) : null;
|
|
||||||
|
|
||||||
setSelectedLocationId(locationId || undefined);
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
|
||||||
formik.setFieldValue('project_flock_id', null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const projectFlockId =
|
|
||||||
val && !Array.isArray(val) ? String(val.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('project_flock_id', projectFlockId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={ref}
|
|
||||||
className={{
|
|
||||||
modalBox: 'p-0 rounded-xl xl:max-w-4/12 max-w-sm',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
onSubmit={formik.handleSubmit}
|
|
||||||
onReset={formik.handleReset}
|
|
||||||
className='w-full flex flex-col'
|
|
||||||
>
|
|
||||||
<div className='p-4 flex items-center justify-between gap-2 border-b border-base-content/10'>
|
|
||||||
<div className='flex items-center gap-2 text-primary'>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
<h3 className='text-sm font-medium'>Filter Data</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={closeModalHandler}
|
|
||||||
className='p-0 text-base-content/50 hover:text-base-content'
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
|
||||||
<SelectInput
|
|
||||||
label='Area'
|
|
||||||
placeholder='Pilih Area'
|
|
||||||
options={areaOptions}
|
|
||||||
value={areaValue}
|
|
||||||
onChange={areaChangeHandler}
|
|
||||||
onInputChange={setAreaInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreAreas}
|
|
||||||
isLoading={isLoadingAreaOptions}
|
|
||||||
isClearable
|
|
||||||
isSearchable={true}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Lokasi'
|
|
||||||
placeholder='Pilih Lokasi'
|
|
||||||
options={locationOptions}
|
|
||||||
value={locationValue}
|
|
||||||
onChange={locationChangeHandler}
|
|
||||||
onInputChange={setLocationInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
isClearable
|
|
||||||
isSearchable={true}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Project Flock'
|
|
||||||
placeholder='Pilih Project Flock'
|
|
||||||
options={projectFlockOptions}
|
|
||||||
value={projectFlockValue}
|
|
||||||
onChange={projectFlockChangeHandler}
|
|
||||||
onInputChange={setProjectFlockInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
|
||||||
isLoading={isLoadingProjectFlockOptions}
|
|
||||||
isClearable
|
|
||||||
isSearchable={true}
|
|
||||||
className={{ wrapper: 'w-full' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DateInput
|
|
||||||
label='Periode'
|
|
||||||
name='period'
|
|
||||||
placeholder='Pilih Periode'
|
|
||||||
value={formik.values.period || ''}
|
|
||||||
onChange={formik.handleChange}
|
|
||||||
onBlur={formik.handleBlur}
|
|
||||||
isError={formik.touched.period && !!formik.errors.period}
|
|
||||||
errorMessage={formik.errors.period}
|
|
||||||
required
|
|
||||||
isNestedModal
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='p-4 flex justify-between gap-4 border-t border-base-content/10 bg-gray-50'>
|
|
||||||
<Button
|
|
||||||
type='reset'
|
|
||||||
variant='soft'
|
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
|
||||||
>
|
|
||||||
Reset Filter
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
|
||||||
disabled={!formik.isValid || formik.isSubmitting}
|
|
||||||
>
|
|
||||||
Apply Filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReportDepreciationFilterModal;
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useEffect, useMemo } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import Card from '@/components/Card';
|
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
|
||||||
import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton';
|
|
||||||
import { useModal } from '@/components/Modal';
|
|
||||||
import ReportDepreciationFilterModal from '@/components/pages/report/expense/tab/ReportDepreciationFilterModal';
|
|
||||||
|
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
|
||||||
import { ReportDepreciation } from '@/types/api/report/report-expense';
|
|
||||||
import { DepreciationReportApi } from '@/services/api/report/expense-report';
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|
||||||
|
|
||||||
interface ReportDepreciationTabProps {
|
|
||||||
tabId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
updateFilter,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
reset: resetFilter,
|
|
||||||
} = useTableFilter({
|
|
||||||
initial: {
|
|
||||||
area_id: '',
|
|
||||||
location_id: '',
|
|
||||||
project_flock_id: '',
|
|
||||||
period: formatDate(Date.now(), 'YYYY-MM-DD'),
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
pageSize: 'limit',
|
|
||||||
area_id: 'area_id',
|
|
||||||
location_id: 'location_id',
|
|
||||||
project_flock_id: 'project_flock_id',
|
|
||||||
period: 'period',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: depreciationsResponse, isLoading: isLoadingDepreciations } =
|
|
||||||
useSWR(
|
|
||||||
`${DepreciationReportApi.basePath}${getTableFilterQueryString()}`,
|
|
||||||
DepreciationReportApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const depreciations = isResponseSuccess(depreciationsResponse)
|
|
||||||
? depreciationsResponse.data
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const filterModal = useModal();
|
|
||||||
const { ref: filterModalRef } = filterModal;
|
|
||||||
|
|
||||||
const setTabActions = useTabActionsStore((state) => state.setTabActions);
|
|
||||||
const clearTabActions = useTabActionsStore((state) => state.clearTabActions);
|
|
||||||
|
|
||||||
const depreciationKandangColumns: ColumnDef<
|
|
||||||
ReportDepreciation['components']['kandang'][0]
|
|
||||||
>[] = [
|
|
||||||
{
|
|
||||||
accessorKey: 'kandang_name',
|
|
||||||
header: 'Kandang',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'house_type',
|
|
||||||
header: 'Tipe Kandang',
|
|
||||||
cell: ({ row }) => row.original.house_type.toUpperCase(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'depreciation_percent',
|
|
||||||
header: 'Persentase Depresiasi',
|
|
||||||
cell: ({ row }) => row.original.depreciation_percent + '%',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'depreciation_value',
|
|
||||||
header: 'Nilai Depresiasi',
|
|
||||||
cell: ({ row }) => formatCurrency(row.original.depreciation_value),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'depreciation_source',
|
|
||||||
header: 'Asal Depresiasi',
|
|
||||||
cell: ({ row }) => row.original.depreciation_source.toUpperCase(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'cutover_date',
|
|
||||||
header: 'Tanggal Cutover',
|
|
||||||
cell: ({ row }) => formatDate(row.original.cutover_date, 'DD MMM YYYY'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'origin_date',
|
|
||||||
header: 'Tanggal Origin',
|
|
||||||
cell: ({ row }) => formatDate(row.original.origin_date, 'DD MMM YYYY'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const tabActionsElement = useMemo(
|
|
||||||
() => (
|
|
||||||
<div className='flex flex-row gap-3'>
|
|
||||||
<ButtonFilter
|
|
||||||
values={tableFilterState}
|
|
||||||
excludeFields={['page', 'pageSize']}
|
|
||||||
onClick={() => filterModal.openModal()}
|
|
||||||
variant='outline'
|
|
||||||
className='px-3 py-2.5'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
[tableFilterState]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTabActions(tabId, tabActionsElement);
|
|
||||||
}, [setTabActions, tabActionsElement, tabId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
clearTabActions(tabId);
|
|
||||||
};
|
|
||||||
}, [clearTabActions, tabId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='w-full p-0 sm:p-3 flex flex-col gap-3'>
|
|
||||||
{isLoadingDepreciations && (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoadingDepreciations && depreciations.length === 0 && (
|
|
||||||
<ReportExpenseSkeleton
|
|
||||||
columns={depreciationKandangColumns}
|
|
||||||
icon={
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:chart-bar'
|
|
||||||
className='text-white'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title='Data Not Yet Available'
|
|
||||||
subtitle='Please change your filters to get the data.'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoadingDepreciations && depreciations.length > 0 && (
|
|
||||||
<>
|
|
||||||
{depreciations.map((depreciationItem, idx) => (
|
|
||||||
<Card
|
|
||||||
key={idx}
|
|
||||||
title={depreciationItem.farm_name}
|
|
||||||
subtitle={`Period: ${formatDate(depreciationItem.period, 'DD MMM YYYY')} | Depresiasi Efektif: ${formatNumber(depreciationItem.depreciation_percent_effective, 'en-US', 0, 10)}% | Nilai Depresiasi: ${formatCurrency(depreciationItem.depreciation_value)} | Total Pullet Cost: ${formatCurrency(depreciationItem.pullet_cost_day_n_total, 'IDR', 'id-ID', 0, 10)}`}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full rounded-lg border-none',
|
|
||||||
body: 'p-0',
|
|
||||||
title:
|
|
||||||
'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
|
||||||
subtitle:
|
|
||||||
'px-2 pb-1.5 bg-primary text-white text-xs font-normal',
|
|
||||||
collapsible: 'rounded-lg',
|
|
||||||
}}
|
|
||||||
variant='bordered'
|
|
||||||
collapsible={true}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
data={depreciationItem.components.kandang}
|
|
||||||
columns={depreciationKandangColumns}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
page={
|
|
||||||
isResponseSuccess(depreciationsResponse)
|
|
||||||
? depreciationsResponse?.meta?.page
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(depreciationsResponse)
|
|
||||||
? depreciationsResponse?.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
onPageChange={setPage}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
isLoading={isLoadingDepreciations}
|
|
||||||
className={{
|
|
||||||
containerClassName: 'w-full mb-0!',
|
|
||||||
tableWrapperClassName:
|
|
||||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
|
||||||
bodyRowClassName:
|
|
||||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
tableFooterClassName:
|
|
||||||
'bg-gray-100 font-semibold border border-gray-200',
|
|
||||||
footerRowClassName: 'border-t-2 border-gray-300',
|
|
||||||
footerColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
paginationClassName: 'hidden',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(depreciationsResponse)
|
|
||||||
? (depreciationsResponse?.meta?.total_results ?? 0)
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
itemsPerPage={tableFilterState.pageSize}
|
|
||||||
currentPage={
|
|
||||||
isResponseSuccess(depreciationsResponse)
|
|
||||||
? (depreciationsResponse?.meta?.page ?? 0)
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
onPrevPage={() => setPage(tableFilterState.page - 1)}
|
|
||||||
onNextPage={() => setPage(tableFilterState.page + 1)}
|
|
||||||
onPageChange={setPage}
|
|
||||||
rowOptions={[10, 20, 50, 100]}
|
|
||||||
onRowChange={setPageSize}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ReportDepreciationFilterModal
|
|
||||||
ref={filterModalRef}
|
|
||||||
initialValues={tableFilterState}
|
|
||||||
onReset={resetFilter}
|
|
||||||
onSubmit={(values) => {
|
|
||||||
updateFilter('area_id', values.area_id ?? '');
|
|
||||||
updateFilter('location_id', values.location_id ?? '');
|
|
||||||
updateFilter('project_flock_id', values.project_flock_id ?? '');
|
|
||||||
updateFilter(
|
|
||||||
'period',
|
|
||||||
values.period ? formatDate(values.period, 'YYYY-MM-DD') : ''
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReportDepreciationTab;
|
|
||||||
@@ -23,8 +23,8 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||||
import { ReportExpenseApi } from '@/services/api/report/expense-report';
|
import { ReportExpenseApi } from '@/services/api/report';
|
||||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { ColumnDef, SortingState, Updater } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { httpClient } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
@@ -73,25 +73,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
// ===== SORTING STATE =====
|
|
||||||
const [sortBy, setSortBy] = useState('');
|
|
||||||
const [orderBy, setOrderBy] = useState('');
|
|
||||||
|
|
||||||
const sorting: SortingState = sortBy
|
|
||||||
? [{ id: sortBy, desc: orderBy === 'desc' }]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleSortingChange = (updater: Updater<SortingState>) => {
|
|
||||||
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
|
||||||
if (next.length > 0) {
|
|
||||||
setSortBy(next[0].id);
|
|
||||||
setOrderBy(next[0].desc ? 'desc' : 'asc');
|
|
||||||
} else {
|
|
||||||
setSortBy('');
|
|
||||||
setOrderBy('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterModalOpenRef = useRef(() => {});
|
const handleFilterModalOpenRef = useRef(() => {});
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
@@ -145,49 +126,8 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
const restoredLocation = filterParams.location_id
|
|
||||||
? locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.location_id
|
|
||||||
) || {
|
|
||||||
value: filterParams.location_id,
|
|
||||||
label: filterParams.location_id,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
const restoredSupplier = filterParams.supplier_id
|
|
||||||
? supplierOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.supplier_id
|
|
||||||
) || {
|
|
||||||
value: filterParams.supplier_id,
|
|
||||||
label: filterParams.supplier_id,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
const restoredKandang = filterParams.kandang_id
|
|
||||||
? projectFlockKandangOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.kandang_id
|
|
||||||
) || { value: filterParams.kandang_id, label: filterParams.kandang_id }
|
|
||||||
: null;
|
|
||||||
const restoredNonstock = filterParams.nonstock_id
|
|
||||||
? nonstockOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.nonstock_id
|
|
||||||
) || {
|
|
||||||
value: filterParams.nonstock_id,
|
|
||||||
label: filterParams.nonstock_id,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
const restoredCategory = filterParams.category
|
|
||||||
? categoryOptions.find((opt) => opt.value === filterParams.category) ||
|
|
||||||
null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
formik.setValues({
|
|
||||||
location_id: restoredLocation,
|
|
||||||
supplier_id: restoredSupplier,
|
|
||||||
kandang_id: restoredKandang,
|
|
||||||
nonstock_id: restoredNonstock,
|
|
||||||
realization_date: filterParams.realization_date || null,
|
|
||||||
category: restoredCategory,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== OPTIONS =====
|
// ===== OPTIONS =====
|
||||||
@@ -249,49 +189,26 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
[formik.values.category]
|
[formik.values.category]
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildReportExpenseQueryString = useCallback(
|
|
||||||
(extraParams?: Record<string, string>) => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (filterParams.location_id) {
|
|
||||||
params.append('location_id', filterParams.location_id);
|
|
||||||
}
|
|
||||||
if (filterParams.supplier_id) {
|
|
||||||
params.append('supplier_id', filterParams.supplier_id);
|
|
||||||
}
|
|
||||||
if (filterParams.kandang_id) {
|
|
||||||
params.append('project_flock_kandang_id', filterParams.kandang_id);
|
|
||||||
}
|
|
||||||
if (filterParams.nonstock_id) {
|
|
||||||
params.append('nonstock_id', filterParams.nonstock_id);
|
|
||||||
}
|
|
||||||
if (filterParams.realization_date) {
|
|
||||||
params.append('realization_date', filterParams.realization_date);
|
|
||||||
}
|
|
||||||
if (filterParams.category) {
|
|
||||||
params.append('category', filterParams.category);
|
|
||||||
}
|
|
||||||
if (sortBy) params.append('sort_by', sortBy);
|
|
||||||
if (orderBy) params.append('sort_order', orderBy);
|
|
||||||
|
|
||||||
Object.entries(extraParams ?? {}).forEach(([key, value]) => {
|
|
||||||
params.set(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return params.toString();
|
|
||||||
},
|
|
||||||
[filterParams, sortBy, orderBy]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: reportExpenseResponse, isLoading } = useSWR(
|
const { data: reportExpenseResponse, isLoading } = useSWR(
|
||||||
() => {
|
() => {
|
||||||
const queryString = buildReportExpenseQueryString({
|
const params = new URLSearchParams();
|
||||||
page: String(page),
|
if (filterParams.location_id)
|
||||||
limit: String(pageSize),
|
params.append('location_id', filterParams.location_id);
|
||||||
});
|
if (filterParams.supplier_id)
|
||||||
|
params.append('supplier_id', filterParams.supplier_id);
|
||||||
|
if (filterParams.kandang_id)
|
||||||
|
params.append('project_flock_kandang_id', filterParams.kandang_id);
|
||||||
|
if (filterParams.nonstock_id)
|
||||||
|
params.append('nonstock_id', filterParams.nonstock_id);
|
||||||
|
if (filterParams.realization_date)
|
||||||
|
params.append('realization_date', filterParams.realization_date);
|
||||||
|
if (filterParams.category)
|
||||||
|
params.append('category', filterParams.category);
|
||||||
|
params.append('page', String(page));
|
||||||
|
params.append('limit', String(pageSize));
|
||||||
|
|
||||||
return [`${ReportExpenseApi.basePath}?${queryString}`];
|
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
|
||||||
},
|
},
|
||||||
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
||||||
);
|
);
|
||||||
@@ -316,31 +233,47 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const reportExpenseExport = useCallback(async (): Promise<
|
const reportExpenseExport = useCallback(async (): Promise<
|
||||||
ReportExpense[] | null
|
ReportExpense[] | null
|
||||||
> => {
|
> => {
|
||||||
const queryString = buildReportExpenseQueryString({
|
const params = new URLSearchParams();
|
||||||
page: '1',
|
if (filterParams.location_id)
|
||||||
limit: '100',
|
params.append('location_id', filterParams.location_id);
|
||||||
});
|
if (filterParams.supplier_id)
|
||||||
|
params.append('supplier_id', filterParams.supplier_id);
|
||||||
|
if (filterParams.kandang_id)
|
||||||
|
params.append('kandang_id', filterParams.kandang_id);
|
||||||
|
if (filterParams.nonstock_id)
|
||||||
|
params.append('nonstock_id', filterParams.nonstock_id);
|
||||||
|
if (filterParams.realization_date)
|
||||||
|
params.append('realization_date', filterParams.realization_date);
|
||||||
|
if (filterParams.category) params.append('category', filterParams.category);
|
||||||
|
params.append('limit', '100');
|
||||||
|
params.append('page', '1');
|
||||||
|
|
||||||
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
||||||
`${ReportExpenseApi.basePath}?${queryString}`
|
`${ReportExpenseApi.basePath}?${params.toString()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
}, [buildReportExpenseQueryString]);
|
}, [filterParams]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
await ReportExpenseApi.exportToExcel(buildReportExpenseQueryString());
|
const allDataForExport = await reportExpenseExport();
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
if (!allDataForExport || allDataForExport.length === 0) {
|
||||||
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await generateReportExpenseExcel(allDataForExport);
|
||||||
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [buildReportExpenseQueryString]);
|
}, [reportExpenseExport]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -464,23 +397,19 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: 'No',
|
||||||
enableSorting: false,
|
|
||||||
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
cell: (props) => (page - 1) * pageSize + props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. PO',
|
header: 'No. PO',
|
||||||
accessorKey: 'po_number',
|
accessorKey: 'po_number',
|
||||||
enableSorting: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'No. Referensi',
|
header: 'No. Referensi',
|
||||||
accessorKey: 'reference_number',
|
accessorKey: 'reference_number',
|
||||||
enableSorting: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Tanggal Realisasi',
|
header: 'Tanggal Realisasi',
|
||||||
accessorKey: 'realization_date',
|
accessorKey: 'realization_date',
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.realization_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -488,7 +417,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Tanggal Transaksi',
|
header: 'Tanggal Transaksi',
|
||||||
accessorKey: 'transaction_date',
|
accessorKey: 'transaction_date',
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
return formatDate(row.original?.transaction_date, 'DD MMM, YYYY');
|
||||||
},
|
},
|
||||||
@@ -496,30 +424,21 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
enableSorting: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
accessorKey: 'product',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
accessorFn: (row) => row.pengajuan?.nonstock?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Supplier',
|
header: 'Supplier',
|
||||||
accessorKey: 'supplier',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.supplier?.name,
|
accessorFn: (row) => row.supplier?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Lokasi',
|
header: 'Lokasi',
|
||||||
accessorKey: 'location',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.kandang?.location?.name,
|
accessorFn: (row) => row.kandang?.location?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Kandang',
|
header: 'Kandang',
|
||||||
accessorKey: 'kandang',
|
|
||||||
enableSorting: true,
|
|
||||||
accessorFn: (row) => row.kandang?.name,
|
accessorFn: (row) => row.kandang?.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -527,19 +446,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
accessorKey: 'qty_pengajuan',
|
id: 'qty_pengajuan',
|
||||||
|
accessorFn: (row) => row.pengajuan?.qty,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
accessorKey: 'price_pengajuan',
|
id: 'harga_pengajuan',
|
||||||
|
accessorFn: (row) => row.pengajuan?.price,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.pengajuan?.price || 0),
|
formatCurrency(row.original.pengajuan?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
accessorKey: 'total_pengajuan',
|
id: 'total_pengajuan',
|
||||||
|
accessorFn: (row) =>
|
||||||
|
(row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.pengajuan?.qty || 0) *
|
(row.original.pengajuan?.qty || 0) *
|
||||||
@@ -554,19 +477,23 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Qty',
|
header: 'Qty',
|
||||||
accessorKey: 'qty_realisasi',
|
id: 'qty_realisasi',
|
||||||
|
accessorFn: (row) => row.realisasi?.qty,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
row.original.realisasi?.qty?.toLocaleString('id-ID') || '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Harga',
|
header: 'Harga',
|
||||||
accessorKey: 'price_realisasi',
|
id: 'harga_realisasi',
|
||||||
|
accessorFn: (row) => row.realisasi?.price,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
formatCurrency(row.original.realisasi?.price || 0),
|
formatCurrency(row.original.realisasi?.price || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
accessorKey: 'total_realisasi',
|
id: 'total_realisasi',
|
||||||
|
accessorFn: (row) =>
|
||||||
|
(row.realisasi?.qty || 0) * (row.realisasi?.price || 0),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const total =
|
const total =
|
||||||
(row.original.realisasi?.qty || 0) *
|
(row.original.realisasi?.qty || 0) *
|
||||||
@@ -577,7 +504,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'realization_status',
|
|
||||||
header: 'Status Pencairan',
|
header: 'Status Pencairan',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RealizationStatusBadge
|
<RealizationStatusBadge
|
||||||
@@ -586,7 +512,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bop_status',
|
|
||||||
header: 'Status BOP',
|
header: 'Status BOP',
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
<ExpenseStatusBadge approval={props.row.original?.latest_approval} />
|
||||||
@@ -631,9 +556,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
totalItems={meta?.total_results || 0}
|
totalItems={meta?.total_results || 0}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
onPageSizeChange={setPageSize}
|
onPageSizeChange={setPageSize}
|
||||||
sorting={sorting}
|
|
||||||
setSorting={handleSortingChange}
|
|
||||||
manualSorting
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-0!',
|
containerClassName: 'w-full mb-0!',
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ import {
|
|||||||
CustomerPaymentFilterSchema,
|
CustomerPaymentFilterSchema,
|
||||||
CustomerPaymentFilterType,
|
CustomerPaymentFilterType,
|
||||||
} from '@/components/pages/report/finance/filter/CustomerPaymentFilter';
|
} from '@/components/pages/report/finance/filter/CustomerPaymentFilter';
|
||||||
|
import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX';
|
||||||
import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF';
|
import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
||||||
import { OptionType } from '@/components/table/TableRowSizeSelector';
|
import { OptionType } from '@/components/table/TableRowSizeSelector';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
|
|
||||||
interface CustomerPaymentTabProps {
|
interface CustomerPaymentTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
@@ -54,14 +54,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
useState(false);
|
|
||||||
const isAnyExportLoading =
|
|
||||||
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
|
||||||
|
|
||||||
// ===== PAGINATION STATE =====
|
// ===== PAGINATION STATE =====
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize] = useState(10);
|
||||||
|
|
||||||
// ===== SUBMISSION STATE =====
|
// ===== SUBMISSION STATE =====
|
||||||
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
||||||
@@ -120,13 +117,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
formik.setValues({
|
|
||||||
start_date: filterParams.start_date || null,
|
|
||||||
end_date: filterParams.end_date || null,
|
|
||||||
customer_ids: filterParams.customer_ids || null,
|
|
||||||
filter_by: filterParams.filter_by || null,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPaymentStatusBadgeColor = (notes: string): Color => {
|
const getPaymentStatusBadgeColor = (notes: string): Color => {
|
||||||
@@ -257,14 +249,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
[customerPayment]
|
[customerPayment]
|
||||||
);
|
);
|
||||||
|
|
||||||
const meta = useMemo(
|
|
||||||
() =>
|
|
||||||
isResponseSuccess(customerPayment) && customerPayment.meta
|
|
||||||
? customerPayment.meta
|
|
||||||
: null,
|
|
||||||
[customerPayment]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const customerPaymentExport = useCallback(async (): Promise<
|
const customerPaymentExport = useCallback(async (): Promise<
|
||||||
CustomerPaymentReport[] | null
|
CustomerPaymentReport[] | null
|
||||||
@@ -296,39 +280,28 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
}, [filterParams]);
|
}, [filterParams]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcelGeneral = useCallback(async () => {
|
|
||||||
setIsExcelGeneralExportLoading(true);
|
|
||||||
try {
|
|
||||||
await FinanceApi.exportCustomerPaymentToExcelGeneral(
|
|
||||||
filterParams.customer_ids,
|
|
||||||
filterParams.filter_by,
|
|
||||||
filterParams.start_date,
|
|
||||||
filterParams.end_date
|
|
||||||
);
|
|
||||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExcelGeneralExportLoading(false);
|
|
||||||
}
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
await FinanceApi.exportCustomerPaymentToExcelCustomerPerSheet(
|
const allDataForExport = await customerPaymentExport();
|
||||||
filterParams.customer_ids,
|
|
||||||
filterParams.filter_by,
|
if (
|
||||||
filterParams.start_date,
|
!allDataForExport ||
|
||||||
filterParams.end_date
|
!Array.isArray(allDataForExport) ||
|
||||||
);
|
allDataForExport.length === 0
|
||||||
|
) {
|
||||||
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await generateCustomerPaymentExcel({ data: allDataForExport });
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [filterParams]);
|
}, [customerPaymentExport]);
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -435,19 +408,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Export to Excel - Customer Per Sheet
|
Export to Excel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={handleExportExcelGeneral}
|
|
||||||
isLoading={isExcelGeneralExportLoading}
|
|
||||||
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 - General
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
@@ -474,10 +436,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcelGeneral,
|
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
isExcelGeneralExportLoading,
|
|
||||||
isExcelExportLoading,
|
isExcelExportLoading,
|
||||||
isPdfExportLoading,
|
isPdfExportLoading,
|
||||||
filterParams,
|
filterParams,
|
||||||
@@ -757,27 +717,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && data.length > 0 && meta && (
|
|
||||||
<div className='w-full ml-auto'>
|
|
||||||
<Pagination
|
|
||||||
totalItems={meta.total_results || 0}
|
|
||||||
itemsPerPage={meta.limit || 0}
|
|
||||||
currentPage={meta.page || 0}
|
|
||||||
onPrevPage={() =>
|
|
||||||
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
|
||||||
}
|
|
||||||
onNextPage={() =>
|
|
||||||
setCurrentPage((curr) =>
|
|
||||||
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
|
||||||
rowOptions={[10, 20, 50, 100]}
|
|
||||||
onRowChange={setPageSize}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map((customerReport) => {
|
data.map((customerReport) => {
|
||||||
@@ -872,27 +811,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{!isLoading && data.length > 0 && meta && (
|
|
||||||
<div className='mt-5 px-3'>
|
|
||||||
<Pagination
|
|
||||||
totalItems={meta.total_results || 0}
|
|
||||||
itemsPerPage={meta.limit || 0}
|
|
||||||
currentPage={meta.page || 0}
|
|
||||||
onPrevPage={() =>
|
|
||||||
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
|
||||||
}
|
|
||||||
onNextPage={() =>
|
|
||||||
setCurrentPage((curr) =>
|
|
||||||
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
|
||||||
rowOptions={[10, 20, 50, 100]}
|
|
||||||
onRowChange={setPageSize}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Modal */}
|
{/* Filter Modal */}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
DebtSupplier,
|
DebtSupplier,
|
||||||
DebtSupplierFilter,
|
DebtSupplierFilter,
|
||||||
} from '@/types/api/report/debt-supplier';
|
} from '@/types/api/report/debt-supplier';
|
||||||
|
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
|
||||||
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
@@ -76,14 +76,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
const [isExcelGeneralExportLoading, setIsExcelGeneralExportLoading] =
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
useState(false);
|
|
||||||
const isAnyExportLoading =
|
|
||||||
isPdfExportLoading || isExcelExportLoading || isExcelGeneralExportLoading;
|
|
||||||
|
|
||||||
// ===== PAGINATION STATE =====
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(10);
|
|
||||||
|
|
||||||
// ===== SUBMISSION STATE =====
|
// ===== SUBMISSION STATE =====
|
||||||
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
|
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
|
||||||
@@ -135,7 +128,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
filter_by: values.filterBy?.value?.toString() || undefined,
|
filter_by: values.filterBy?.value?.toString() || undefined,
|
||||||
});
|
});
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setCurrentPage(1);
|
// setIsSubmitted(true);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
setFilterParams({
|
setFilterParams({
|
||||||
@@ -144,30 +137,14 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
supplier_ids: undefined,
|
supplier_ids: undefined,
|
||||||
filter_by: undefined,
|
filter_by: undefined,
|
||||||
});
|
});
|
||||||
setCurrentPage(1);
|
// setIsSubmitted(false);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
const restoredFilterBy =
|
|
||||||
dataTypeOptions.find((opt) => opt.value === filterParams.filter_by) ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
const supplierIdList = filterParams.supplier_ids
|
|
||||||
? filterParams.supplier_ids.split(',')
|
|
||||||
: [];
|
|
||||||
const restoredSupplierIds = supplierOptions.filter((opt) =>
|
|
||||||
supplierIdList.includes(String(opt.value))
|
|
||||||
);
|
|
||||||
|
|
||||||
formik.setValues({
|
|
||||||
startDate: filterParams.start_date || null,
|
|
||||||
endDate: filterParams.end_date || null,
|
|
||||||
supplierIds: restoredSupplierIds.length > 0 ? restoredSupplierIds : null,
|
|
||||||
filterBy: restoredFilterBy,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
@@ -178,8 +155,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
filter_by: filterParams.filter_by,
|
filter_by: filterParams.filter_by,
|
||||||
start_date: filterParams.start_date,
|
start_date: filterParams.start_date,
|
||||||
end_date: filterParams.end_date,
|
end_date: filterParams.end_date,
|
||||||
page: currentPage,
|
|
||||||
limit: pageSize,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return ['debt-supplier-report', params];
|
return ['debt-supplier-report', params];
|
||||||
@@ -189,9 +164,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
params.supplier_ids,
|
params.supplier_ids,
|
||||||
params.filter_by,
|
params.filter_by,
|
||||||
params.start_date,
|
params.start_date,
|
||||||
params.end_date,
|
params.end_date
|
||||||
params.page,
|
|
||||||
params.limit
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -203,14 +176,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
[debtSupplier]
|
[debtSupplier]
|
||||||
);
|
);
|
||||||
|
|
||||||
const meta = useMemo(
|
|
||||||
() =>
|
|
||||||
isResponseSuccess(debtSupplier) && debtSupplier.meta
|
|
||||||
? debtSupplier.meta
|
|
||||||
: null,
|
|
||||||
[debtSupplier]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== EXPORT DATA FETCHER =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const debtSupplierExport = useCallback(async (): Promise<
|
const debtSupplierExport = useCallback(async (): Promise<
|
||||||
DebtSupplier[] | null
|
DebtSupplier[] | null
|
||||||
@@ -251,19 +216,25 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
await DebtSupplierApi.exportToExcelSupplierPerSheet(
|
const allDataForExport = await debtSupplierExport();
|
||||||
filterParams.supplier_ids,
|
|
||||||
filterParams.filter_by,
|
if (
|
||||||
filterParams.start_date,
|
!allDataForExport ||
|
||||||
filterParams.end_date
|
!Array.isArray(allDataForExport) ||
|
||||||
);
|
allDataForExport.length === 0
|
||||||
|
) {
|
||||||
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDebtSupplierExcel({ data: allDataForExport });
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [filterParams]);
|
}, [debtSupplierExport]);
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -304,23 +275,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
formik.values.endDate,
|
formik.values.endDate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleExportExcelGeneral = useCallback(async () => {
|
|
||||||
setIsExcelGeneralExportLoading(true);
|
|
||||||
try {
|
|
||||||
await DebtSupplierApi.exportToExcelGeneral(
|
|
||||||
filterParams.supplier_ids,
|
|
||||||
filterParams.filter_by,
|
|
||||||
filterParams.start_date,
|
|
||||||
filterParams.end_date
|
|
||||||
);
|
|
||||||
toast.success('Excel General berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel General. Silakan coba lagi.');
|
|
||||||
} finally {
|
|
||||||
setIsExcelGeneralExportLoading(false);
|
|
||||||
}
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
// ===== TAB ACTIONS COMPONENT =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
const TabActions = useMemo(() => {
|
const TabActions = useMemo(() => {
|
||||||
return function TabActionsComponent() {
|
return function TabActionsComponent() {
|
||||||
@@ -383,17 +337,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Export to Excel - Supplier Per Sheet
|
Export to Excel
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='none'
|
|
||||||
onClick={handleExportExcelGeneral}
|
|
||||||
isLoading={isExcelGeneralExportLoading}
|
|
||||||
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 - General
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -423,10 +367,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
filterParams,
|
filterParams,
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportExcelGeneral,
|
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
isExcelExportLoading,
|
isExcelExportLoading,
|
||||||
isExcelGeneralExportLoading,
|
|
||||||
isPdfExportLoading,
|
isPdfExportLoading,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -688,27 +630,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && data.length > 0 && meta && (
|
|
||||||
<div className='w-full ml-auto'>
|
|
||||||
<Pagination
|
|
||||||
totalItems={meta.total_results || 0}
|
|
||||||
itemsPerPage={meta.limit || 0}
|
|
||||||
currentPage={meta.page || 0}
|
|
||||||
onPrevPage={() =>
|
|
||||||
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
|
||||||
}
|
|
||||||
onNextPage={() =>
|
|
||||||
setCurrentPage((curr) =>
|
|
||||||
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
|
||||||
rowOptions={[10, 20, 50, 100]}
|
|
||||||
onRowChange={setPageSize}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map((supplierReport) => {
|
data.map((supplierReport) => {
|
||||||
@@ -796,27 +717,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{!isLoading && data.length > 0 && meta && (
|
|
||||||
<div className='mt-5 px-3'>
|
|
||||||
<Pagination
|
|
||||||
totalItems={meta.total_results || 0}
|
|
||||||
itemsPerPage={meta.limit || 0}
|
|
||||||
currentPage={meta.page || 0}
|
|
||||||
onPrevPage={() =>
|
|
||||||
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
|
||||||
}
|
|
||||||
onNextPage={() =>
|
|
||||||
setCurrentPage((curr) =>
|
|
||||||
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
|
||||||
rowOptions={[10, 20, 50, 100]}
|
|
||||||
onRowChange={setPageSize}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Modal */}
|
{/* Filter Modal */}
|
||||||
|
|||||||
@@ -156,17 +156,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
formik.setValues({
|
|
||||||
start_date: filterParams.start_date || null,
|
|
||||||
end_date: filterParams.end_date || null,
|
|
||||||
area_ids: filterParams.area_id || null,
|
|
||||||
supplier_ids: filterParams.supplier_id || null,
|
|
||||||
product_ids: filterParams.product_id || null,
|
|
||||||
product_category_ids: filterParams.product_category_id || null,
|
|
||||||
filter_by: filterParams.filter_by || null,
|
|
||||||
sort_by: filterParams.sort_by || null,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { setFieldValue } = formik;
|
const { setFieldValue } = formik;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
export type DailyMarketingReportFilterType = {
|
export type DailyMarketingReportFilterType = {
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
search: string | null;
|
search: string | null;
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
location_id: string | null;
|
location_id: string | null;
|
||||||
@@ -16,8 +14,6 @@ export type DailyMarketingReportFilterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DailyMarketingReportFilterSchema = yup.object({
|
export const DailyMarketingReportFilterSchema = yup.object({
|
||||||
page: yup.number().nullable(),
|
|
||||||
pageSize: yup.number().nullable(),
|
|
||||||
search: yup.string().nullable(),
|
search: yup.string().nullable(),
|
||||||
area_id: yup.string().nullable(),
|
area_id: yup.string().nullable(),
|
||||||
location_id: yup.string().nullable(),
|
location_id: yup.string().nullable(),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
export type HppPerKandangFilterType = {
|
export type HppPerKandangFilterType = {
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
location_id: string | null;
|
location_id: string | null;
|
||||||
kandang_id: string | null;
|
kandang_id: string | null;
|
||||||
@@ -14,8 +12,6 @@ export type HppPerKandangFilterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const HppPerKandangFilterSchema = yup.object({
|
export const HppPerKandangFilterSchema = yup.object({
|
||||||
page: yup.number().nullable(),
|
|
||||||
pageSize: yup.number().nullable(),
|
|
||||||
area_id: yup.string().nullable(),
|
area_id: yup.string().nullable(),
|
||||||
location_id: yup.string().nullable(),
|
location_id: yup.string().nullable(),
|
||||||
kandang_id: yup.string().nullable(),
|
kandang_id: yup.string().nullable(),
|
||||||
|
|||||||
@@ -17,10 +17,16 @@ import {
|
|||||||
formatVechicleNumber,
|
formatVechicleNumber,
|
||||||
formatTitleCase,
|
formatTitleCase,
|
||||||
} from '@/lib/helper';
|
} from '@/lib/helper';
|
||||||
import { DailyMarketingRow } from '@/types/api/report/marketing';
|
import {
|
||||||
|
DailyMarketingRow,
|
||||||
|
DailyMarketingReportResponse,
|
||||||
|
} from '@/types/api/report/marketing';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF';
|
||||||
|
import { generateDailyMarketingExcel } from '@/components/pages/report/marketing/export/DailyMarketingExportXLSX';
|
||||||
|
import { pdf } from '@react-pdf/renderer';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -33,6 +39,8 @@ import Modal, { useModal } from '@/components/Modal';
|
|||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton';
|
import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton';
|
||||||
import { useEffect as useEffectHook } from 'react';
|
import { useEffect as useEffectHook } from 'react';
|
||||||
|
import { httpClient } from '@/services/http/client';
|
||||||
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
import {
|
import {
|
||||||
MARKETING_DATE_FILTER_TYPE_OPTIONS,
|
MARKETING_DATE_FILTER_TYPE_OPTIONS,
|
||||||
MARKETING_TYPE_OPTIONS,
|
MARKETING_TYPE_OPTIONS,
|
||||||
@@ -45,8 +53,6 @@ interface DailyMarketingTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FilterParams {
|
interface FilterParams {
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
area_id?: string;
|
area_id?: string;
|
||||||
location_id?: string;
|
location_id?: string;
|
||||||
warehouse_id?: string;
|
warehouse_id?: string;
|
||||||
@@ -110,8 +116,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<DailyMarketingReportFilterType>({
|
const formik = useFormik<DailyMarketingReportFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
search: null,
|
search: null,
|
||||||
area_id: null,
|
area_id: null,
|
||||||
location_id: null,
|
location_id: null,
|
||||||
@@ -126,8 +130,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
validationSchema: DailyMarketingReportFilterSchema,
|
validationSchema: DailyMarketingReportFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
setFilterParams({
|
setFilterParams({
|
||||||
page: values.page || undefined,
|
|
||||||
pageSize: values.pageSize || undefined,
|
|
||||||
area_id: values.area_id || undefined,
|
area_id: values.area_id || undefined,
|
||||||
location_id: values.location_id || undefined,
|
location_id: values.location_id || undefined,
|
||||||
warehouse_id: values.warehouse_id || undefined,
|
warehouse_id: values.warehouse_id || undefined,
|
||||||
@@ -148,21 +150,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
formik.setValues({
|
|
||||||
page: formik.values.page,
|
|
||||||
pageSize: formik.values.pageSize,
|
|
||||||
search: formik.values.search,
|
|
||||||
area_id: filterParams.area_id || null,
|
|
||||||
location_id: filterParams.location_id || null,
|
|
||||||
warehouse_id: filterParams.warehouse_id || null,
|
|
||||||
customer_id: filterParams.customer_id || null,
|
|
||||||
start_date: filterParams.start_date || null,
|
|
||||||
end_date: filterParams.end_date || null,
|
|
||||||
filter_by: filterParams.filter_by || null,
|
|
||||||
marketing_type: filterParams.marketing_type || null,
|
|
||||||
sort_by: filterParams.sort_by || null,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== SEARCH CHANGE HANDLER =====
|
// ===== SEARCH CHANGE HANDLER =====
|
||||||
@@ -233,9 +222,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
if (searchValue) params.set('search', searchValue);
|
||||||
if (filterParams.page) params.set('page', String(filterParams.page));
|
|
||||||
if (filterParams.pageSize)
|
|
||||||
params.set('limit', String(filterParams.pageSize));
|
|
||||||
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
||||||
if (filterParams.location_id)
|
if (filterParams.location_id)
|
||||||
params.set('location_id', filterParams.location_id);
|
params.set('location_id', filterParams.location_id);
|
||||||
@@ -276,10 +262,10 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
[dailyMarketings]
|
[dailyMarketings]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT DATA FETCHER =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const dailyMarketingsExport = useCallback(async (): Promise<
|
||||||
setIsExcelExportLoading(true);
|
DailyMarketingRow[] | null
|
||||||
try {
|
> => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
if (searchValue) params.set('search', searchValue);
|
||||||
@@ -293,13 +279,50 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
if (filterParams.start_date)
|
if (filterParams.start_date)
|
||||||
params.set('start_date', filterParams.start_date);
|
params.set('start_date', filterParams.start_date);
|
||||||
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
|
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
|
||||||
if (filterParams.filter_by)
|
if (filterParams.filter_by) params.set('filter_by', filterParams.filter_by);
|
||||||
params.set('filter_by', filterParams.filter_by);
|
|
||||||
if (filterParams.marketing_type)
|
if (filterParams.marketing_type)
|
||||||
params.set('marketing_type', filterParams.marketing_type);
|
params.set('marketing_type', filterParams.marketing_type);
|
||||||
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
||||||
|
params.set('limit', '9999999');
|
||||||
|
|
||||||
await MarketingReportApi.exportDailyMarketingToExcel(params.toString());
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await httpClient<DailyMarketingReportResponse>(
|
||||||
|
`${MarketingReportApi.basePath}${queryString}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(response)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [filterParams, searchValue]);
|
||||||
|
|
||||||
|
// ===== EXPORT HANDLERS =====
|
||||||
|
const handleExportExcel = useCallback(async () => {
|
||||||
|
setIsExcelExportLoading(true);
|
||||||
|
try {
|
||||||
|
const allDataForExport = await dailyMarketingsExport();
|
||||||
|
|
||||||
|
if (!allDataForExport || allDataForExport.length === 0) {
|
||||||
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const period =
|
||||||
|
filterParams.start_date && filterParams.end_date
|
||||||
|
? `${formatDate(filterParams.start_date, 'DD-MMM-YYYY')}_to_${formatDate(filterParams.end_date, 'DD-MMM-YYYY')}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await generateDailyMarketingExcel({
|
||||||
|
data: allDataForExport,
|
||||||
|
summaryTotal: summaryTotal,
|
||||||
|
period: period,
|
||||||
|
});
|
||||||
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
@@ -307,39 +330,34 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [filterParams, searchValue]);
|
}, [filterParams, dailyMarketingsExport, summaryTotal]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams();
|
const allDataForExport = await dailyMarketingsExport();
|
||||||
|
|
||||||
if (searchValue) params.set('search', searchValue);
|
if (!allDataForExport || allDataForExport.length === 0) {
|
||||||
if (filterParams.area_id) params.set('area_id', filterParams.area_id);
|
toast.error('Tidak ada data untuk diekspor.');
|
||||||
if (filterParams.location_id)
|
return;
|
||||||
params.set('location_id', filterParams.location_id);
|
}
|
||||||
if (filterParams.warehouse_id)
|
|
||||||
params.set('warehouse_id', filterParams.warehouse_id);
|
|
||||||
if (filterParams.customer_id)
|
|
||||||
params.set('customer_id', filterParams.customer_id);
|
|
||||||
if (filterParams.start_date)
|
|
||||||
params.set('start_date', filterParams.start_date);
|
|
||||||
if (filterParams.end_date) params.set('end_date', filterParams.end_date);
|
|
||||||
if (filterParams.filter_by)
|
|
||||||
params.set('filter_by', filterParams.filter_by);
|
|
||||||
if (filterParams.marketing_type)
|
|
||||||
params.set('marketing_type', filterParams.marketing_type);
|
|
||||||
if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by);
|
|
||||||
|
|
||||||
await MarketingReportApi.exportDailyMarketingToPDF(params.toString());
|
const dailyMarketingReportPdfBlob = await pdf(
|
||||||
|
<DailyMarketingReportPDF data={allDataForExport} total={summaryTotal} />
|
||||||
|
).toBlob();
|
||||||
|
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
||||||
|
dailyMarketingReportPdfBlob
|
||||||
|
);
|
||||||
|
window.open(dailyMarketingReportPdfUrl, '_blank');
|
||||||
|
|
||||||
|
toast.success('PDF berhasil dibuat.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsPdfExportLoading(false);
|
setIsPdfExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [filterParams, searchValue]);
|
}, [dailyMarketingsExport, summaryTotal]);
|
||||||
|
|
||||||
// ===== TAB ACTIONS COMPONENT =====
|
// ===== TAB ACTIONS COMPONENT =====
|
||||||
const TabActions = useMemo(() => {
|
const TabActions = useMemo(() => {
|
||||||
@@ -554,7 +572,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'qty',
|
accessorKey: 'qty',
|
||||||
cell: (props) => formatNumber(props.row.original.qty),
|
cell: (props) => formatNumber(props.row.original.qty),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_qty
|
{summaryTotal?.total_qty
|
||||||
? formatNumber(summaryTotal.total_qty)
|
? formatNumber(summaryTotal.total_qty)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -567,7 +585,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'average_weight_kg',
|
accessorKey: 'average_weight_kg',
|
||||||
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_weight_kg
|
{summaryTotal?.average_weight_kg
|
||||||
? formatNumber(summaryTotal.average_weight_kg)
|
? formatNumber(summaryTotal.average_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -580,7 +598,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'total_weight_kg',
|
accessorKey: 'total_weight_kg',
|
||||||
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_weight_kg
|
{summaryTotal?.total_weight_kg
|
||||||
? formatNumber(summaryTotal.total_weight_kg)
|
? formatNumber(summaryTotal.total_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -593,9 +611,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'sales_price_per_kg',
|
accessorKey: 'sales_price_per_kg',
|
||||||
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_sales_price
|
{summaryTotal?.average_sales_price
|
||||||
? formatCurrency(summaryTotal.average_sales_price)
|
? formatNumber(summaryTotal.average_sales_price)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -606,7 +624,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'hpp_price_per_kg',
|
accessorKey: 'hpp_price_per_kg',
|
||||||
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_hpp_price_per_kg
|
{summaryTotal?.total_hpp_price_per_kg
|
||||||
? formatCurrency(summaryTotal.total_hpp_price_per_kg)
|
? formatCurrency(summaryTotal.total_hpp_price_per_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -619,7 +637,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'sales_amount',
|
accessorKey: 'sales_amount',
|
||||||
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_sales_amount
|
{summaryTotal?.total_sales_amount
|
||||||
? formatCurrency(summaryTotal.total_sales_amount)
|
? formatCurrency(summaryTotal.total_sales_amount)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -670,27 +688,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
<Table
|
<Table
|
||||||
data={data}
|
data={data}
|
||||||
columns={getTableColumns()}
|
columns={getTableColumns()}
|
||||||
pageSize={filterParams.pageSize}
|
|
||||||
page={
|
|
||||||
isResponseSuccess(dailyMarketings)
|
|
||||||
? dailyMarketings?.meta?.page
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(dailyMarketings)
|
|
||||||
? dailyMarketings?.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
onPageChange={(newPage) =>
|
|
||||||
setFilterParams((prevVal) => ({ ...prevVal, page: newPage }))
|
|
||||||
}
|
|
||||||
onPageSizeChange={(newPageSize) =>
|
|
||||||
setFilterParams((prevVal) => ({
|
|
||||||
...prevVal,
|
|
||||||
pageSize: newPageSize,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
isLoading={isLoading}
|
|
||||||
renderFooter={data.length > 0}
|
renderFooter={data.length > 0}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-0!',
|
containerClassName: 'w-full mb-0!',
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ interface HppPerKandangTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FilterParams {
|
interface FilterParams {
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
area_id?: string;
|
area_id?: string;
|
||||||
location_id?: string;
|
location_id?: string;
|
||||||
kandang_id?: string;
|
kandang_id?: string;
|
||||||
@@ -110,8 +108,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<HppPerKandangFilterType>({
|
const formik = useFormik<HppPerKandangFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
area_id: null,
|
area_id: null,
|
||||||
location_id: null,
|
location_id: null,
|
||||||
kandang_id: null,
|
kandang_id: null,
|
||||||
@@ -124,8 +120,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
validationSchema: HppPerKandangFilterSchema,
|
validationSchema: HppPerKandangFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
setFilterParams({
|
setFilterParams({
|
||||||
page: values.page || undefined,
|
|
||||||
pageSize: values.pageSize || undefined,
|
|
||||||
area_id: values.area_id || undefined,
|
area_id: values.area_id || undefined,
|
||||||
location_id: values.location_id || undefined,
|
location_id: values.location_id || undefined,
|
||||||
kandang_id: values.kandang_id || undefined,
|
kandang_id: values.kandang_id || undefined,
|
||||||
@@ -152,19 +146,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
formik.setValues({
|
|
||||||
page: formik.values.page,
|
|
||||||
pageSize: formik.values.pageSize,
|
|
||||||
area_id: filterParams.area_id || null,
|
|
||||||
location_id: filterParams.location_id || null,
|
|
||||||
kandang_id: filterParams.kandang_id || null,
|
|
||||||
weight_min: filterParams.weight_min || null,
|
|
||||||
weight_max: filterParams.weight_max || null,
|
|
||||||
period: filterParams.period || null,
|
|
||||||
sort_by: filterParams.sort_by || null,
|
|
||||||
show_unrecorded: filterParams.show_unrecorded ?? false,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== WEIGHT CHANGE HANDLERS =====
|
// ===== WEIGHT CHANGE HANDLERS =====
|
||||||
@@ -274,8 +257,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
period: filterParams.period,
|
period: filterParams.period,
|
||||||
sort_by: filterParams.sort_by,
|
sort_by: filterParams.sort_by,
|
||||||
show_unrecorded: filterParams.show_unrecorded,
|
show_unrecorded: filterParams.show_unrecorded,
|
||||||
page: filterParams.page,
|
|
||||||
pageSize: filterParams.pageSize,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return ['hpp-per-kandang-report', params];
|
return ['hpp-per-kandang-report', params];
|
||||||
@@ -290,9 +271,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
params.weight_max,
|
params.weight_max,
|
||||||
params.period,
|
params.period,
|
||||||
params.sort_by,
|
params.sort_by,
|
||||||
params.show_unrecorded,
|
params.show_unrecorded
|
||||||
params.page,
|
|
||||||
params.pageSize
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -342,9 +321,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
params.weight_max,
|
params.weight_max,
|
||||||
params.period,
|
params.period,
|
||||||
params.sort_by,
|
params.sort_by,
|
||||||
params.show_unrecorded,
|
params.show_unrecorded
|
||||||
params.page,
|
|
||||||
params.limit
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
@@ -489,7 +466,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={filterParams}
|
values={filterParams}
|
||||||
excludeFields={['page', 'pageSize']}
|
|
||||||
onClick={() => handleFilterModalOpenRef.current()}
|
onClick={() => handleFilterModalOpenRef.current()}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -869,25 +845,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
<Table
|
<Table
|
||||||
data={data}
|
data={data}
|
||||||
columns={getTableColumns()}
|
columns={getTableColumns()}
|
||||||
pageSize={filterParams.pageSize}
|
|
||||||
page={
|
|
||||||
isResponseSuccess(hppPerKandang) ? hppPerKandang?.meta?.page : 0
|
|
||||||
}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(hppPerKandang)
|
|
||||||
? hppPerKandang?.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
onPageChange={(newPage) =>
|
|
||||||
setFilterParams((prevVal) => ({ ...prevVal, page: newPage }))
|
|
||||||
}
|
|
||||||
onPageSizeChange={(newPageSize) =>
|
|
||||||
setFilterParams((prevVal) => ({
|
|
||||||
...prevVal,
|
|
||||||
pageSize: newPageSize,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
isLoading={isLoading}
|
|
||||||
renderFooter={data.length > 0}
|
renderFooter={data.length > 0}
|
||||||
renderCustomRow={renderCustomRow}
|
renderCustomRow={renderCustomRow}
|
||||||
className={{
|
className={{
|
||||||
|
|||||||
+1
-36
@@ -263,43 +263,8 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
const restoredAreaId = filterParams.area_id
|
|
||||||
? areaOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.area_id
|
|
||||||
) || { value: filterParams.area_id, label: filterParams.area_id }
|
|
||||||
: null;
|
|
||||||
const restoredLocationId = filterParams.location_id
|
|
||||||
? locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.location_id
|
|
||||||
) || {
|
|
||||||
value: filterParams.location_id,
|
|
||||||
label: filterParams.location_id,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
const restoredProjectFlockId = filterParams.project_flock_id
|
|
||||||
? projectFlockOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.project_flock_id
|
|
||||||
) || {
|
|
||||||
value: filterParams.project_flock_id,
|
|
||||||
label: filterParams.project_flock_id,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
const restoredKandangId = filterParams.project_flock_kandang_id
|
|
||||||
? projectFlockKandangOptions.find(
|
|
||||||
(opt) => String(opt.value) === filterParams.project_flock_kandang_id
|
|
||||||
) || {
|
|
||||||
value: filterParams.project_flock_kandang_id,
|
|
||||||
label: filterParams.project_flock_kandang_id,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
formik.setValues({
|
|
||||||
area_id: restoredAreaId,
|
|
||||||
location_id: restoredLocationId,
|
|
||||||
project_flock_id: restoredProjectFlockId,
|
|
||||||
kandang_id: restoredKandangId,
|
|
||||||
});
|
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
|
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
|
||||||
|
|||||||
+1
-11
@@ -197,7 +197,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
icon: 'heroicons-outline:folder',
|
icon: 'heroicons-outline:folder',
|
||||||
permission: [
|
permission: [
|
||||||
'lti.inventory.product_stock.list',
|
'lti.inventory.product_stock.list',
|
||||||
'lti.inventory.stock_log.list',
|
|
||||||
'lti.inventory.product_warehouses.list',
|
'lti.inventory.product_warehouses.list',
|
||||||
'lti.inventory.transfer.list',
|
'lti.inventory.transfer.list',
|
||||||
],
|
],
|
||||||
@@ -205,10 +204,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
{
|
{
|
||||||
text: 'Stok Produk',
|
text: 'Stok Produk',
|
||||||
link: '/inventory/product',
|
link: '/inventory/product',
|
||||||
permission: [
|
permission: ['lti.inventory.product_stock.list'],
|
||||||
'lti.inventory.product_stock.list',
|
|
||||||
'lti.inventory.stock_log.list',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Penyesuaian Stok',
|
text: 'Penyesuaian Stok',
|
||||||
@@ -240,7 +236,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
'lti.master.uoms.list',
|
'lti.master.uoms.list',
|
||||||
'lti.master.warehouses.list',
|
'lti.master.warehouses.list',
|
||||||
'lti.master.production_standards.list',
|
'lti.master.production_standards.list',
|
||||||
'lti.system_settings.update',
|
|
||||||
],
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
@@ -308,11 +303,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/master-data/production-standard',
|
link: '/master-data/production-standard',
|
||||||
permission: ['lti.master.production_standards.list'],
|
permission: ['lti.master.production_standards.list'],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: 'Konfigurasi Sistem',
|
|
||||||
link: '/master-data/system-config',
|
|
||||||
permission: ['lti.system_settings.update'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -218,6 +218,4 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/master-data/production-standard/detail/edit/': [
|
'/master-data/production-standard/detail/edit/': [
|
||||||
'lti.master.production_standards.update',
|
'lti.master.production_standards.update',
|
||||||
],
|
],
|
||||||
|
|
||||||
'/master-data/system-config/': ['lti.system_settings.update'],
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ interface DatePickerProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
formatDisplay?: (date: string) => string;
|
formatDisplay?: (date: string) => string;
|
||||||
hasError?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DatePicker({
|
export function DatePicker({
|
||||||
@@ -29,7 +28,6 @@ export function DatePicker({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
placeholder = 'Select date',
|
placeholder = 'Select date',
|
||||||
formatDisplay,
|
formatDisplay,
|
||||||
hasError = false,
|
|
||||||
}: DatePickerProps) {
|
}: DatePickerProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [currentMonth, setCurrentMonth] = useState(() => {
|
const [currentMonth, setCurrentMonth] = useState(() => {
|
||||||
@@ -156,7 +154,7 @@ export function DatePicker({
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`w-full justify-start text-left font-normal hover:bg-gray-50 ${hasError ? 'border-red-500 focus:ring-red-500' : 'border-gray-200'}`}
|
className='w-full justify-start text-left font-normal border-gray-200 hover:bg-gray-50'
|
||||||
>
|
>
|
||||||
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
<CalendarIcon className='mr-2 h-4 w-4 text-gray-500' />
|
||||||
{date ? (
|
{date ? (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -89,10 +89,7 @@ export function Dashboard() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
order_by: 'asc',
|
|
||||||
sort_by: 'name',
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const target = e.target as HTMLDivElement;
|
const target = e.target as HTMLDivElement;
|
||||||
|
|||||||
+20
-303
@@ -40,12 +40,11 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { ColumnDef, Row } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'ALL', label: 'Semua Status' },
|
{ value: 'ALL', label: 'Semua Status' },
|
||||||
@@ -60,7 +59,6 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
pullet_close: 'Pullet Close',
|
pullet_close: 'Pullet Close',
|
||||||
produksi_open: 'Produksi Open',
|
produksi_open: 'Produksi Open',
|
||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
empty_kandang: 'Kandang Kosong',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ListDailyChecklistContent() {
|
export function ListDailyChecklistContent() {
|
||||||
@@ -89,9 +87,6 @@ export function ListDailyChecklistContent() {
|
|||||||
date_from: 'date_from',
|
date_from: 'date_from',
|
||||||
date_to: 'date_to',
|
date_to: 'date_to',
|
||||||
},
|
},
|
||||||
|
|
||||||
persist: true,
|
|
||||||
storeName: 'list-daily-checklist-content-table',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -110,10 +105,7 @@ export function ListDailyChecklistContent() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
order_by: 'asc',
|
|
||||||
sort_by: 'name',
|
|
||||||
});
|
|
||||||
|
|
||||||
const checklistList = isResponseSuccess(checklistListRes)
|
const checklistList = isResponseSuccess(checklistListRes)
|
||||||
? checklistListRes.data || []
|
? checklistListRes.data || []
|
||||||
@@ -130,29 +122,12 @@ export function ListDailyChecklistContent() {
|
|||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
const [showBulkApproveModal, setShowBulkApproveModal] = useState(false);
|
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
const [showBulkRejectModal, setShowBulkRejectModal] = useState(false);
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
|
||||||
const selectedRowIds = Object.keys(rowSelection);
|
|
||||||
|
|
||||||
const selectedRowItems = selectedRowIds.map((itemId) =>
|
|
||||||
checklistList.find((item) => item.id === parseInt(itemId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const tableEnableRowSelectionHandler: (
|
|
||||||
row: Row<DailyChecklist>
|
|
||||||
) => boolean = (row) => {
|
|
||||||
return (
|
|
||||||
row.original.status !== 'APPROVED' && row.original.status !== 'REJECTED'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDetail = (item: DailyChecklist) => {
|
const handleDetail = (item: DailyChecklist) => {
|
||||||
router.push(
|
router.push(
|
||||||
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
||||||
@@ -160,7 +135,13 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item: DailyChecklist) => {
|
const handleEdit = (item: DailyChecklist) => {
|
||||||
router.push(`/daily-checklist/daily-checklist?checklistId=${item.id}`);
|
const formattedDate = new Date(item.date).toISOString().split('T')[0];
|
||||||
|
const kandangId = item.kandang?.id ?? '';
|
||||||
|
const category = item.category;
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
`/daily-checklist/daily-checklist?date=${formattedDate}&kandang_id=${kandangId}&category=${category}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = (item: DailyChecklist) => {
|
const handleApprove = (item: DailyChecklist) => {
|
||||||
@@ -168,22 +149,21 @@ export function ListDailyChecklistContent() {
|
|||||||
setShowApproveModal(true);
|
setShowApproveModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkApprove = () => {
|
|
||||||
setShowBulkApproveModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReject = (item: DailyChecklist) => {
|
const handleReject = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
setShowRejectModal(true);
|
setShowRejectModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkReject = () => {
|
|
||||||
setRejectReason('');
|
|
||||||
setShowBulkRejectModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (item: DailyChecklist) => {
|
const handleDelete = (item: DailyChecklist) => {
|
||||||
|
// ✅ VALIDATION: Only DRAFT can be deleted
|
||||||
|
if (item.status !== 'DRAFT') {
|
||||||
|
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
||||||
|
description: `Status saat ini: ${item.status}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
};
|
};
|
||||||
@@ -215,31 +195,6 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmBulkApprove = async () => {
|
|
||||||
if (!selectedRowIds.length) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setActionLoading(true);
|
|
||||||
|
|
||||||
const approveRes = await DailyChecklistApi.bulkApprove(selectedRowIds);
|
|
||||||
|
|
||||||
if (isResponseError(approveRes)) {
|
|
||||||
toast.error('Gagal approve checklist: ' + approveRes.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshChecklistList();
|
|
||||||
toast.success('Checklist berhasil di-approve');
|
|
||||||
setShowBulkApproveModal(false);
|
|
||||||
setRowSelection({});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error approving checklist:', error);
|
|
||||||
toast.error('Terjadi kesalahan');
|
|
||||||
} finally {
|
|
||||||
setActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmReject = async () => {
|
const confirmReject = async () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
@@ -274,40 +229,6 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmBulkReject = async () => {
|
|
||||||
if (!selectedRowIds.length) return;
|
|
||||||
|
|
||||||
if (!rejectReason.trim()) {
|
|
||||||
toast.error('Alasan reject harus diisi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setActionLoading(true);
|
|
||||||
|
|
||||||
const rejectRes = await DailyChecklistApi.bulkReject(
|
|
||||||
selectedRowIds,
|
|
||||||
rejectReason
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isResponseError(rejectRes)) {
|
|
||||||
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshChecklistList();
|
|
||||||
toast.success('Checklist berhasil di-reject');
|
|
||||||
setShowBulkRejectModal(false);
|
|
||||||
setRowSelection({});
|
|
||||||
setRejectReason('');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error rejecting checklist:', error);
|
|
||||||
toast.error('Terjadi kesalahan');
|
|
||||||
} finally {
|
|
||||||
setActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!selectedItem) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
@@ -404,37 +325,6 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<div className='w-full flex flex-row justify-center'>
|
|
||||||
<CheckboxInput
|
|
||||||
name='allRow'
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
indeterminate={table.getIsSomeRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const isCheckboxDisabled =
|
|
||||||
!row.getCanSelect() ||
|
|
||||||
row.original.status === 'APPROVED' ||
|
|
||||||
row.original.status === 'REJECTED';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<CheckboxInput
|
|
||||||
name='row'
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
disabled={isCheckboxDisabled}
|
|
||||||
indeterminate={row.getIsSomeSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'date',
|
accessorKey: 'date',
|
||||||
header: 'Tanggal',
|
header: 'Tanggal',
|
||||||
@@ -547,6 +437,7 @@ export function ListDailyChecklistContent() {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{row.original.status === 'DRAFT' && (
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -558,6 +449,7 @@ export function ListDailyChecklistContent() {
|
|||||||
Hapus
|
Hapus
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -567,8 +459,7 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div className='mb-6 flex flex-row justify-between items-center gap-3'>
|
<div className='mb-6'>
|
||||||
<div>
|
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
List Daily Checklist
|
List Daily Checklist
|
||||||
</h1>
|
</h1>
|
||||||
@@ -577,31 +468,6 @@ export function ListDailyChecklistContent() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
|
||||||
{selectedRowIds.length > 0 && (
|
|
||||||
<div className='flex flex-row items-center gap-3'>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
onClick={handleBulkApprove}
|
|
||||||
className='bg-green-600 hover:bg-green-700 text-white'
|
|
||||||
>
|
|
||||||
<CheckCircle className='w-4 h-4 mr-1' />
|
|
||||||
Bulk Approve {`(${selectedRowIds.length}) item`}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='destructive'
|
|
||||||
onClick={handleBulkReject}
|
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
|
||||||
>
|
|
||||||
<XCircle className='w-4 h-4 mr-1' />
|
|
||||||
Bulk Reject {`(${selectedRowIds.length}) item`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</RequirePermission>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Card */}
|
{/* Main Card */}
|
||||||
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
<CardContent className='p-6'>
|
<CardContent className='p-6'>
|
||||||
@@ -722,10 +588,6 @@ export function ListDailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
isLoading={isLoadingChecklistList}
|
isLoading={isLoadingChecklistList}
|
||||||
rowSelection={rowSelection}
|
|
||||||
setRowSelection={setRowSelection}
|
|
||||||
enableRowSelection={tableEnableRowSelectionHandler}
|
|
||||||
withCheckbox
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn({
|
||||||
'w-full mb-20':
|
'w-full mb-20':
|
||||||
@@ -804,76 +666,6 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Bulk Approve Modal */}
|
|
||||||
<Dialog
|
|
||||||
open={showBulkApproveModal}
|
|
||||||
onOpenChange={setShowBulkApproveModal}
|
|
||||||
>
|
|
||||||
<DialogContent className='sm:max-w-md max-h-[80vh] overflow-y-auto bg-white rounded-xl shadow-lg'>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Approve Checklist</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Apakah Anda yakin ingin approve {selectedRowIds.length} checklist
|
|
||||||
ini?
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
|
|
||||||
{selectedRowItems.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item?.id ?? 0}
|
|
||||||
className='bg-gray-50 rounded-lg p-4 space-y-2'
|
|
||||||
>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600'>Tanggal:</span>
|
|
||||||
<span className='font-medium text-gray-900'>
|
|
||||||
{formatDate(item?.date ?? '')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600'>Kandang:</span>
|
|
||||||
<span className='font-medium text-gray-900'>
|
|
||||||
{item?.kandang?.name ?? '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600'>Kategori:</span>
|
|
||||||
<span className='font-medium text-gray-900'>
|
|
||||||
{item?.category
|
|
||||||
? (CATEGORY_LABELS[item.category] ?? item?.category)
|
|
||||||
: item?.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600'>Progress:</span>
|
|
||||||
<span className='font-medium text-gray-900'>
|
|
||||||
{item?.progress}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className='flex gap-2'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setShowBulkApproveModal(false)}
|
|
||||||
disabled={actionLoading}
|
|
||||||
className='border-gray-200'
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={confirmBulkApprove}
|
|
||||||
disabled={actionLoading}
|
|
||||||
className='bg-green-600 hover:bg-green-700 text-white'
|
|
||||||
>
|
|
||||||
{actionLoading ? 'Memproses...' : 'Ya, Approve'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Reject Modal */}
|
{/* Reject Modal */}
|
||||||
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
@@ -943,81 +735,6 @@ export function ListDailyChecklistContent() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Bulk Reject Modal */}
|
|
||||||
<Dialog open={showBulkRejectModal} onOpenChange={setShowBulkRejectModal}>
|
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Reject Checklist</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Berikan alasan reject untuk checklist ini
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className='max-h-[60vh] overflow-y-auto flex flex-col gap-3'>
|
|
||||||
{selectedRowItems.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item?.id ?? 0}
|
|
||||||
className='bg-gray-50 rounded-lg p-4 space-y-2 mb-4'
|
|
||||||
>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600'>Tanggal:</span>
|
|
||||||
<span className='font-medium text-gray-900'>
|
|
||||||
{formatDate(item?.date ?? '')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600'>Kandang:</span>
|
|
||||||
<span className='font-medium text-gray-900'>
|
|
||||||
{item?.kandang?.name ?? '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-between text-sm'>
|
|
||||||
<span className='text-gray-600'>Kategori:</span>
|
|
||||||
<span className='font-medium text-gray-900'>
|
|
||||||
{item?.category
|
|
||||||
? CATEGORY_LABELS[item.category] || item?.category
|
|
||||||
: item?.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor='reject-reason'>
|
|
||||||
Alasan Reject <span className='text-red-500'>*</span>
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id='reject-reason'
|
|
||||||
value={rejectReason}
|
|
||||||
onChange={(e) => setRejectReason(e.target.value)}
|
|
||||||
placeholder='Tuliskan alasan reject...'
|
|
||||||
className='mt-1.5 border-gray-200 min-h-[100px]'
|
|
||||||
disabled={actionLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className='flex gap-2'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => setShowBulkRejectModal(false)}
|
|
||||||
disabled={actionLoading}
|
|
||||||
className='border-gray-200'
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={confirmBulkReject}
|
|
||||||
disabled={actionLoading}
|
|
||||||
variant='destructive'
|
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
|
||||||
>
|
|
||||||
{actionLoading ? 'Memproses...' : 'Ya, Reject'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Delete Modal */}
|
{/* Delete Modal */}
|
||||||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||||
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
||||||
|
|||||||
+12
-169
@@ -2,14 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
||||||
ArrowLeft,
|
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
AlertCircle,
|
|
||||||
Share2,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import * as htmlToImage from 'html-to-image';
|
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Badge } from '@/figma-make/components/base/badge';
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
@@ -60,7 +53,6 @@ interface ChecklistHeader {
|
|||||||
progress_percent: number;
|
progress_percent: number;
|
||||||
total_phases: number;
|
total_phases: number;
|
||||||
total_activities: number;
|
total_activities: number;
|
||||||
empty_kandang_end_date?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PhaseGroup {
|
interface PhaseGroup {
|
||||||
@@ -114,7 +106,6 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
pullet_close: 'Pullet Close',
|
pullet_close: 'Pullet Close',
|
||||||
produksi_open: 'Produksi Open',
|
produksi_open: 'Produksi Open',
|
||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
empty_kandang: 'Kandang Kosong',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
||||||
@@ -146,8 +137,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
const [isGeneratingImage, setIsGeneratingImage] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (checklistId) {
|
if (checklistId) {
|
||||||
fetchChecklistDetail();
|
fetchChecklistDetail();
|
||||||
@@ -180,9 +169,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
|
|
||||||
setDocuments(rawDetailChecklist?.document_urls || []);
|
setDocuments(rawDetailChecklist?.document_urls || []);
|
||||||
|
|
||||||
const emptyKandangEndDate =
|
|
||||||
rawDetailChecklist?.empty_kandang?.end_date ?? null;
|
|
||||||
|
|
||||||
const checklistData = {
|
const checklistData = {
|
||||||
id: rawDetailChecklist?.id,
|
id: rawDetailChecklist?.id,
|
||||||
date: rawDetailChecklist?.date,
|
date: rawDetailChecklist?.date,
|
||||||
@@ -209,7 +195,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: 0,
|
total_phases: 0,
|
||||||
total_activities: 0,
|
total_activities: 0,
|
||||||
empty_kandang_end_date: emptyKandangEndDate,
|
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -277,7 +262,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
||||||
total_activities: tasks.length,
|
total_activities: tasks.length,
|
||||||
empty_kandang_end_date: emptyKandangEndDate,
|
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -328,7 +312,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
progress_percent: progressPercent,
|
progress_percent: progressPercent,
|
||||||
total_phases: uniquePhases.size,
|
total_phases: uniquePhases.size,
|
||||||
total_activities: uniqueActivities.size,
|
total_activities: uniqueActivities.size,
|
||||||
empty_kandang_end_date: emptyKandangEndDate,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching checklist detail:', error);
|
console.error('Error fetching checklist detail:', error);
|
||||||
@@ -564,103 +547,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMobileDevice = () => {
|
|
||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
||||||
navigator.userAgent
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusMessage = () => {
|
|
||||||
switch (header?.status) {
|
|
||||||
case 'DRAFT':
|
|
||||||
return 'Checklist harian perlu disubmit';
|
|
||||||
case 'SUBMITTED':
|
|
||||||
return 'Checklist harian menunggu persetujuan';
|
|
||||||
case 'APPROVED':
|
|
||||||
return 'Checklist harian telah disetujui';
|
|
||||||
case 'REJECTED':
|
|
||||||
return 'Checklist harian telah ditolak';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const shareHandler = async () => {
|
|
||||||
const isMobile = isMobileDevice();
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
setIsGeneratingImage(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseTitle = `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`;
|
|
||||||
const statusMsg = getStatusMessage();
|
|
||||||
const statusInfo = `\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}`;
|
|
||||||
const urlMessage = `\n\nView full checklist: ${window.location.href}`;
|
|
||||||
const fullMessage = baseTitle + statusInfo + urlMessage;
|
|
||||||
|
|
||||||
let shareData: ShareData;
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
const htmlBlob = await htmlToImage.toBlob(document.body, {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
});
|
|
||||||
const imgFile = new File(
|
|
||||||
[htmlBlob!],
|
|
||||||
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
|
|
||||||
{
|
|
||||||
type: 'image/png',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
shareData = {
|
|
||||||
files: [imgFile],
|
|
||||||
title: baseTitle,
|
|
||||||
text: fullMessage,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
shareData = {
|
|
||||||
title: baseTitle,
|
|
||||||
text: fullMessage,
|
|
||||||
url: window.location.href,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsGeneratingImage(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!navigator.canShare(shareData)) {
|
|
||||||
toast.error(
|
|
||||||
'Gagal membagikan checklist, coba dengan perangkat yang berbeda'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await navigator.share(shareData);
|
|
||||||
toast.success('Checklist berhasil dibagikan');
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Gagal membagikan checklist');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const shareToWhatsAppHandler = async () => {
|
|
||||||
const isMobile = isMobileDevice();
|
|
||||||
setIsGeneratingImage(true);
|
|
||||||
|
|
||||||
const statusMsg = getStatusMessage();
|
|
||||||
const category = header?.category || '';
|
|
||||||
const message = encodeURIComponent(
|
|
||||||
`Daily Checklist\n\nTanggal: ${formatDate(header?.date || '')}\nKandang: ${header?.kandang_name}\nKategori: ${CATEGORY_LABELS[category] || category}\nProgress: ${header?.progress_percent}%\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}`
|
|
||||||
);
|
|
||||||
|
|
||||||
setIsGeneratingImage(false);
|
|
||||||
|
|
||||||
const whatsappUrl = isMobile
|
|
||||||
? `https://wa.me/?text=${message}`
|
|
||||||
: `https://web.whatsapp.com/send?text=${message}`;
|
|
||||||
|
|
||||||
window.open(whatsappUrl, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
@@ -687,8 +573,8 @@ export function DetailDailyChecklistContent() {
|
|||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Action Buttons */}
|
{/* Page Title with Back Button */}
|
||||||
<div className='mb-6 flex items-start sm:items-center justify-between gap-4 flex-wrap'>
|
<div className='mb-6 flex items-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -698,11 +584,17 @@ export function DetailDailyChecklistContent() {
|
|||||||
<ArrowLeft className='w-4 h-4 mr-1' />
|
<ArrowLeft className='w-4 h-4 mr-1' />
|
||||||
Kembali
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
|
<div className='flex-1'>
|
||||||
<div className='flex items-center gap-2 flex-wrap'>
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Detail Daily Checklist
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Lihat detail checklist harian
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{header.status === 'SUBMITTED' && (
|
{header.status === 'SUBMITTED' && (
|
||||||
<RequirePermission permissions='lti.daily_checklist.create'>
|
<RequirePermission permissions='lti.daily_checklist.create'>
|
||||||
<div className='flex gap-2 flex-wrap'>
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleApprove}
|
onClick={handleApprove}
|
||||||
disabled={actionLoading}
|
disabled={actionLoading}
|
||||||
@@ -723,43 +615,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
</div>
|
</div>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
onClick={shareHandler}
|
|
||||||
disabled={isGeneratingImage}
|
|
||||||
className='border-gray-200'
|
|
||||||
>
|
|
||||||
<Share2 className='w-4 h-4 mr-1' />
|
|
||||||
{!isGeneratingImage && 'Bagikan'}
|
|
||||||
|
|
||||||
{isGeneratingImage && 'Memuat...'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
onClick={shareToWhatsAppHandler}
|
|
||||||
disabled={isGeneratingImage}
|
|
||||||
className='border-gray-200'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:whatsapp' className='w-4 h-4 mr-1' />
|
|
||||||
{!isGeneratingImage && 'Bagikan via WhatsApp'}
|
|
||||||
|
|
||||||
{isGeneratingImage && 'Memuat...'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Page Title */}
|
|
||||||
<div className='mb-6'>
|
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
|
||||||
Detail Daily Checklist
|
|
||||||
</h1>
|
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
|
||||||
Lihat detail checklist harian
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header Info Card */}
|
{/* Header Info Card */}
|
||||||
@@ -784,18 +639,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
{CATEGORY_LABELS[header.category] || header.category}
|
{CATEGORY_LABELS[header.category] || header.category}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{header.category === 'empty_kandang' && (
|
|
||||||
<div>
|
|
||||||
<Label className='text-xs text-gray-500'>
|
|
||||||
Tanggal Selesai Kandang Kosong
|
|
||||||
</Label>
|
|
||||||
<p className='text-sm font-medium text-gray-900 mt-1'>
|
|
||||||
{header.empty_kandang_end_date
|
|
||||||
? formatDate(header.empty_kandang_end_date)
|
|
||||||
: '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
<Label className='text-xs text-gray-500'>Status</Label>
|
<Label className='text-xs text-gray-500'>Status</Label>
|
||||||
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
<div className='mt-1'>{getStatusBadge(header.status)}</div>
|
||||||
|
|||||||
@@ -96,10 +96,7 @@ export function MasterEmployeeContent() {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
loadMore: loadMoreKandang,
|
loadMore: loadMoreKandang,
|
||||||
isLoadingMore: isLoadingMoreKandang,
|
isLoadingMore: isLoadingMoreKandang,
|
||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name');
|
||||||
order_by: 'asc',
|
|
||||||
sort_by: 'name',
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const target = e.target as HTMLDivElement;
|
const target = e.target as HTMLDivElement;
|
||||||
@@ -220,9 +217,7 @@ export function MasterEmployeeContent() {
|
|||||||
'Error creating employee:',
|
'Error creating employee:',
|
||||||
createEmployeeResponse.message
|
createEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error(
|
toast.error('Gagal menambahkan ABK');
|
||||||
'Gagal menambahkan ABK: ' + createEmployeeResponse.message
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,9 +238,7 @@ export function MasterEmployeeContent() {
|
|||||||
'Error updating employee:',
|
'Error updating employee:',
|
||||||
updateEmployeeResponse.message
|
updateEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error(
|
toast.error('Gagal menambahkan ABK');
|
||||||
'Gagal memperbarui ABK: ' + updateEmployeeResponse.message
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ import { cn } from '@/lib/helper';
|
|||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { useSelect } from '@/components/input/SelectInput';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
|
||||||
import { UserApi } from '@/services/api/user';
|
import { UserApi } from '@/services/api/user';
|
||||||
|
|
||||||
export function MasterKandangContent() {
|
export function MasterKandangContent() {
|
||||||
@@ -107,6 +108,12 @@ export function MasterKandangContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingMore: isLoadingKandangOptionsMore,
|
||||||
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
|
||||||
@@ -368,9 +375,7 @@ export function MasterKandangContent() {
|
|||||||
name='search'
|
name='search'
|
||||||
placeholder='Cari kandang...'
|
placeholder='Cari kandang...'
|
||||||
value={tableFilterState.search}
|
value={tableFilterState.search}
|
||||||
onChange={(e) =>
|
onChange={(e) => updateFilter('search', e.target.value)}
|
||||||
updateFilter('search', e.target.value, true)
|
|
||||||
}
|
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full sm:w-[280px] border-gray-200',
|
wrapper: 'w-full sm:w-[280px] border-gray-200',
|
||||||
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
||||||
@@ -385,11 +390,7 @@ export function MasterKandangContent() {
|
|||||||
<Select
|
<Select
|
||||||
value={tableFilterState.location_id}
|
value={tableFilterState.location_id}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateFilter(
|
updateFilter('location_id', value === 'all' ? '' : value)
|
||||||
'location_id',
|
|
||||||
value === 'all' ? '' : value,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className='w-[180px] border-gray-200'>
|
<SelectTrigger className='w-[180px] border-gray-200'>
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { SystemSettingsApi } from '@/services/api/system-settings';
|
|
||||||
import { SystemSetting } from '@/types/api/system-settings/system-setting';
|
|
||||||
|
|
||||||
const ALLOW_NEGATIVE_PAKAN_OVK_KEY = 'allow_negative_pakan_ovk';
|
|
||||||
|
|
||||||
function SettingToggle({
|
|
||||||
setting,
|
|
||||||
onToggle,
|
|
||||||
loading,
|
|
||||||
}: {
|
|
||||||
setting: SystemSetting;
|
|
||||||
onToggle: (key: string, currentValue: boolean) => void;
|
|
||||||
loading: boolean;
|
|
||||||
}) {
|
|
||||||
const isEnabled = setting.value === 'true';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex items-start justify-between gap-4 py-5'>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<p className='text-sm font-medium text-gray-900'>
|
|
||||||
{setting.key === ALLOW_NEGATIVE_PAKAN_OVK_KEY
|
|
||||||
? 'Mode Migrasi PAKAN & OVK'
|
|
||||||
: setting.key}
|
|
||||||
</p>
|
|
||||||
{setting.description && (
|
|
||||||
<p className='text-sm text-gray-500 mt-0.5'>{setting.description}</p>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center mt-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
||||||
isEnabled
|
|
||||||
? 'bg-amber-100 text-amber-700'
|
|
||||||
: 'bg-gray-100 text-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isEnabled ? 'Aktif' : 'Nonaktif'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
role='switch'
|
|
||||||
aria-checked={isEnabled}
|
|
||||||
disabled={loading}
|
|
||||||
onClick={() => onToggle(setting.key, isEnabled)}
|
|
||||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[#0069e0] focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
||||||
isEnabled ? 'bg-[#0069e0]' : 'bg-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden='true'
|
|
||||||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
||||||
isEnabled ? 'translate-x-5' : 'translate-x-0'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SystemConfigContent() {
|
|
||||||
const [toggling, setToggling] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: settingsResponse,
|
|
||||||
isLoading,
|
|
||||||
mutate: refreshSettings,
|
|
||||||
} = useSWR(SystemSettingsApi.basePath, SystemSettingsApi.getAllFetcher, {
|
|
||||||
keepPreviousData: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleToggle = async (key: string, currentValue: boolean) => {
|
|
||||||
if (key !== ALLOW_NEGATIVE_PAKAN_OVK_KEY) return;
|
|
||||||
|
|
||||||
setToggling(key);
|
|
||||||
try {
|
|
||||||
const res = await SystemSettingsApi.setAllowNegativePakanOvk({
|
|
||||||
value: !currentValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isResponseError(res)) {
|
|
||||||
toast.error(res.message || 'Gagal mengubah pengaturan');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshSettings();
|
|
||||||
toast.success(
|
|
||||||
!currentValue
|
|
||||||
? 'Mode migrasi PAKAN & OVK diaktifkan'
|
|
||||||
: 'Mode migrasi PAKAN & OVK dinonaktifkan'
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
toast.error('Terjadi kesalahan saat mengubah pengaturan');
|
|
||||||
} finally {
|
|
||||||
setToggling(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const settings = isResponseSuccess(settingsResponse)
|
|
||||||
? settingsResponse.data
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (isLoading && !settingsResponse) {
|
|
||||||
return (
|
|
||||||
<div className='min-h-screen'>
|
|
||||||
<div className='p-6'>
|
|
||||||
<div className='mb-6'>
|
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
|
||||||
Konfigurasi Sistem
|
|
||||||
</h1>
|
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
|
||||||
Master Data •{' '}
|
|
||||||
<span className='text-[#0069e0]'>Konfigurasi Sistem</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
|
||||||
<CardContent className='p-12 text-center text-gray-500'>
|
|
||||||
Memuat data...
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='min-h-screen'>
|
|
||||||
<div className='p-6'>
|
|
||||||
<div className='mb-6'>
|
|
||||||
<h1 className='text-2xl font-semibold text-gray-900'>
|
|
||||||
Konfigurasi Sistem
|
|
||||||
</h1>
|
|
||||||
<p className='text-sm text-gray-600 mt-1'>
|
|
||||||
Master Data •{' '}
|
|
||||||
<span className='text-[#0069e0]'>Konfigurasi Sistem</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
|
||||||
<CardContent className='p-0'>
|
|
||||||
<div className='px-6 py-4 border-b border-gray-200/60'>
|
|
||||||
<h2 className='text-base font-semibold text-gray-800'>
|
|
||||||
Pengaturan Global
|
|
||||||
</h2>
|
|
||||||
<p className='text-sm text-gray-500 mt-0.5'>
|
|
||||||
Pengaturan ini berlaku untuk seluruh sistem.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='px-6 divide-y divide-gray-200/60'>
|
|
||||||
{settings.length === 0 ? (
|
|
||||||
<p className='py-10 text-center text-sm text-gray-500'>
|
|
||||||
Tidak ada pengaturan tersedia.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
settings.map((setting) => (
|
|
||||||
<SettingToggle
|
|
||||||
key={setting.key}
|
|
||||||
setting={setting}
|
|
||||||
onToggle={handleToggle}
|
|
||||||
loading={toggling === setting.key}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -137,8 +137,6 @@ export function DailyChecklistReportsContent() {
|
|||||||
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', {
|
||||||
area_id: tableFilterState.area_id,
|
area_id: tableFilterState.area_id,
|
||||||
location_id: tableFilterState.location_id,
|
location_id: tableFilterState.location_id,
|
||||||
order_by: 'asc',
|
|
||||||
sort_by: 'name',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
@@ -161,24 +159,17 @@ export function DailyChecklistReportsContent() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { options: employeeOptions } = useSelect(
|
||||||
options: employeeOptions,
|
EmployeeApi.basePath,
|
||||||
loadMore: loadMoreEmployee,
|
'id',
|
||||||
isLoadingMore: isLoadingMoreEmployee,
|
'name',
|
||||||
} = useSelect(EmployeeApi.basePath, 'id', 'name', 'search', {
|
'search',
|
||||||
order_by: 'asc',
|
{
|
||||||
sort_by: 'name',
|
page: '1',
|
||||||
|
limit: '500',
|
||||||
kandang_id: tableFilterState.kandang_id,
|
kandang_id: tableFilterState.kandang_id,
|
||||||
});
|
|
||||||
|
|
||||||
const handleEmployeeScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
||||||
const target = e.target as HTMLDivElement;
|
|
||||||
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
|
|
||||||
if (!isLoadingMoreEmployee) {
|
|
||||||
loadMoreEmployee();
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const currentMonthMaxDay = new Date(
|
const currentMonthMaxDay = new Date(
|
||||||
Number(tableFilterState.tahun),
|
Number(tableFilterState.tahun),
|
||||||
@@ -502,7 +493,7 @@ export function DailyChecklistReportsContent() {
|
|||||||
>
|
>
|
||||||
<SelectValue placeholder='Semua ABK' />
|
<SelectValue placeholder='Semua ABK' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent onScroll={handleEmployeeScroll}>
|
<SelectContent>
|
||||||
<SelectItem value='ALL'>Semua ABK</SelectItem>
|
<SelectItem value='ALL'>Semua ABK</SelectItem>
|
||||||
{employeeOptions.map((employee) => (
|
{employeeOptions.map((employee) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -512,11 +503,6 @@ export function DailyChecklistReportsContent() {
|
|||||||
{employee.label}
|
{employee.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
{isLoadingMoreEmployee && (
|
|
||||||
<div className='flex justify-center p-2'>
|
|
||||||
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import {
|
import {
|
||||||
BaseApiResponse,
|
BaseApiResponse,
|
||||||
ErrorApiResponse,
|
ErrorApiResponse,
|
||||||
@@ -16,40 +15,3 @@ export const isResponseError = <T>(
|
|||||||
): res is ErrorApiResponse => {
|
): res is ErrorApiResponse => {
|
||||||
return res?.status === 'error';
|
return res?.status === 'error';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getErrorMessage = async (
|
|
||||||
error: unknown,
|
|
||||||
fallbackMessage: string
|
|
||||||
) => {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const responseData = error.response?.data;
|
|
||||||
|
|
||||||
if (responseData instanceof Blob) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(await responseData.text()) as {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
return parsed.message || fallbackMessage;
|
|
||||||
} catch {
|
|
||||||
return fallbackMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
responseData &&
|
|
||||||
typeof responseData === 'object' &&
|
|
||||||
'message' in responseData &&
|
|
||||||
typeof responseData.message === 'string'
|
|
||||||
) {
|
|
||||||
return responseData.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.message || fallbackMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackMessage;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
DailyChecklist,
|
DailyChecklist,
|
||||||
DailyChecklistReport,
|
DailyChecklistReport,
|
||||||
DetailDailyChecklist,
|
DetailDailyChecklist,
|
||||||
UpdateDailyChecklistPayload,
|
|
||||||
} from '@/types/api/daily-checklist/daily-checklist';
|
} from '@/types/api/daily-checklist/daily-checklist';
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -17,39 +16,12 @@ import { toast } from 'sonner';
|
|||||||
export class DailyChecklistApiService extends BaseApiService<
|
export class DailyChecklistApiService extends BaseApiService<
|
||||||
DailyChecklist,
|
DailyChecklist,
|
||||||
CreateDailyChecklistPayload,
|
CreateDailyChecklistPayload,
|
||||||
UpdateDailyChecklistPayload
|
unknown
|
||||||
> {
|
> {
|
||||||
constructor(basePath: string = '/daily-checklists') {
|
constructor(basePath: string = '/daily-checklists') {
|
||||||
super(basePath);
|
super(basePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, payload: UpdateDailyChecklistPayload) {
|
|
||||||
const isFormData =
|
|
||||||
typeof FormData !== 'undefined' && payload instanceof FormData;
|
|
||||||
try {
|
|
||||||
const updatePath = `${this.basePath}/${id}`;
|
|
||||||
|
|
||||||
const headers = isFormData
|
|
||||||
? { ...(this.header ?? {}) }
|
|
||||||
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
|
||||||
|
|
||||||
const updateRes = await httpClient<BaseApiResponse<DailyChecklist>>(
|
|
||||||
updatePath,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: payload,
|
|
||||||
headers,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return updateRes;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (axios.isAxiosError<BaseApiResponse<DailyChecklist>>(error)) {
|
|
||||||
return error.response?.data;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOneDailyChecklist(id: string) {
|
async getOneDailyChecklist(id: string) {
|
||||||
try {
|
try {
|
||||||
const getOneDailyChecklistPath = `${this.basePath}/relation/${id}`;
|
const getOneDailyChecklistPath = `${this.basePath}/relation/${id}`;
|
||||||
@@ -220,29 +192,6 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkApprove(ids: string[]) {
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.append('ids', ids.join(','));
|
|
||||||
formData.append('status', 'APPROVED');
|
|
||||||
formData.append('reject_reason', '');
|
|
||||||
|
|
||||||
const approvePath = `${this.basePath}/bulk-update`;
|
|
||||||
const approveRes = await httpClient<BaseApiResponse>(approvePath, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return approveRes;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
|
||||||
return error.response?.data;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async reject(id: string, rejectReason: string) {
|
async reject(id: string, rejectReason: string) {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -266,29 +215,6 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkReject(ids: string[], rejectReason: string) {
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.append('ids', ids.join(','));
|
|
||||||
formData.append('status', 'REJECTED');
|
|
||||||
formData.append('reject_reason', rejectReason);
|
|
||||||
|
|
||||||
const rejectPath = `${this.basePath}/bulk-update`;
|
|
||||||
const rejectRes = await httpClient<BaseApiResponse>(rejectPath, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return rejectRes;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
|
||||||
return error.response?.data;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadImage(
|
async uploadImage(
|
||||||
id: number,
|
id: number,
|
||||||
status: string,
|
status: string,
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import axios from 'axios';
|
|||||||
import { BaseApiService } from '@/services/api/base';
|
import { BaseApiService } from '@/services/api/base';
|
||||||
import { BaseApiResponse, GroupedApprovals } from '@/types/api/api-general';
|
import { BaseApiResponse, GroupedApprovals } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
BulkApproveExpensePayload,
|
|
||||||
CreateExpensePayload,
|
CreateExpensePayload,
|
||||||
CreateExpenseRealizationPayload,
|
CreateExpenseRealizationPayload,
|
||||||
Expense,
|
Expense,
|
||||||
UpdateExpensePayload,
|
UpdateExpensePayload,
|
||||||
} from '@/types/api/expense';
|
} from '@/types/api/expense';
|
||||||
import { httpClient } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { formatDate } from '@/lib/helper';
|
|
||||||
|
|
||||||
export class ExpenseApiService extends BaseApiService<
|
export class ExpenseApiService extends BaseApiService<
|
||||||
Expense,
|
Expense,
|
||||||
@@ -332,65 +330,6 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkApproveToStatus(
|
|
||||||
payload: BulkApproveExpensePayload
|
|
||||||
): Promise<BaseApiResponse<Expense | Expense[]> | undefined> {
|
|
||||||
try {
|
|
||||||
return await httpClient<BaseApiResponse<Expense | Expense[]>>(
|
|
||||||
`${this.basePath}/approvals/bulk`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: payload,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError<BaseApiResponse<Expense | Expense[]>>(error)) {
|
|
||||||
return error.response?.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async bulkApprovals(
|
|
||||||
ids: number[],
|
|
||||||
status: BulkApproveExpensePayload['status'] | 'SELESAI',
|
|
||||||
date?: string,
|
|
||||||
notes?: string
|
|
||||||
): Promise<BaseApiResponse<Expense | Expense[]> | undefined> {
|
|
||||||
if (status === 'SELESAI') {
|
|
||||||
const responses = await Promise.all(ids.map((id) => this.complete(id)));
|
|
||||||
const failedResponse = responses.find(
|
|
||||||
(response) => response?.status !== 'success'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (failedResponse) {
|
|
||||||
return failedResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
const completedExpenses = responses.flatMap((response) =>
|
|
||||||
response?.status === 'success' ? [response.data] : []
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
status: 'success',
|
|
||||||
message:
|
|
||||||
completedExpenses.length === 1
|
|
||||||
? 'Submit expense approval successfully'
|
|
||||||
: 'Submit expense approvals successfully',
|
|
||||||
data: completedExpenses,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.bulkApproveToStatus({
|
|
||||||
approvable_ids: ids,
|
|
||||||
status,
|
|
||||||
date: date || undefined,
|
|
||||||
notes: notes || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async rejectHeadArea(
|
async rejectHeadArea(
|
||||||
id: number,
|
id: number,
|
||||||
notes?: string
|
notes?: string
|
||||||
@@ -572,25 +511,6 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setExpensePaidOff(id: number) {
|
|
||||||
try {
|
|
||||||
const res = await httpClient<BaseApiResponse<Expense>>(
|
|
||||||
`${this.basePath}/${id}/pay`,
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError<BaseApiResponse<Expense>>(error)) {
|
|
||||||
return error.response?.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteExpenseRequestDocument(
|
async deleteExpenseRequestDocument(
|
||||||
expenseId: number,
|
expenseId: number,
|
||||||
documentId: number
|
documentId: number
|
||||||
@@ -726,60 +646,6 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
async exportToExcel(initialQueryString: string) {
|
|
||||||
const params = new URLSearchParams(initialQueryString);
|
|
||||||
|
|
||||||
params.set('export', 'excel');
|
|
||||||
params.set('type', 'all');
|
|
||||||
params.set('page', '1');
|
|
||||||
params.set('limit', '99999999999');
|
|
||||||
|
|
||||||
const queryString = `?${params.toString()}`;
|
|
||||||
|
|
||||||
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
|
||||||
method: 'GET',
|
|
||||||
responseType: 'blob',
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([res]));
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
|
|
||||||
const fileName = `BOP-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
params.set('export', 'excel');
|
|
||||||
params.set('type', 'progress');
|
|
||||||
params.set('start_date', formatDate(startDate, 'YYYY-MM-DD'));
|
|
||||||
params.set('end_date', formatDate(endDate, 'YYYY-MM-DD'));
|
|
||||||
|
|
||||||
const queryString = `?${params.toString()}`;
|
|
||||||
|
|
||||||
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
|
||||||
method: 'GET',
|
|
||||||
responseType: 'blob',
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([res]));
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
|
|
||||||
const fileName = `input-progres-BOP-${formatDate(startDate, 'DD-MM-YYYY')}-ke-${formatDate(endDate, 'DD-MM-YYYY')}.xlsx`;
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExpenseApi = new ExpenseApiService('/expenses');
|
export const ExpenseApi = new ExpenseApiService('/expenses');
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import {
|
|||||||
CreateInventoryAdjustmentPayload,
|
CreateInventoryAdjustmentPayload,
|
||||||
InventoryAdjustment,
|
InventoryAdjustment,
|
||||||
} from '@/types/api/inventory/adjustment';
|
} from '@/types/api/inventory/adjustment';
|
||||||
import { InventoryProduct, StockLog } from '@/types/api/inventory/product';
|
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||||
import { httpClient } from '../http/client';
|
|
||||||
import { formatDate } from '@/lib/helper';
|
|
||||||
|
|
||||||
export const ProductWarehouseApi = new BaseApiService<
|
export const ProductWarehouseApi = new BaseApiService<
|
||||||
ProductWarehouse,
|
ProductWarehouse,
|
||||||
@@ -67,41 +65,3 @@ export const InventoryProductApi = new BaseApiService<
|
|||||||
unknown,
|
unknown,
|
||||||
unknown
|
unknown
|
||||||
>('/inventory/product-stocks');
|
>('/inventory/product-stocks');
|
||||||
|
|
||||||
export class StockLogService extends BaseApiService<
|
|
||||||
StockLog,
|
|
||||||
unknown,
|
|
||||||
unknown
|
|
||||||
> {
|
|
||||||
constructor(basePath: string = '/inventory/stock-logs') {
|
|
||||||
super(basePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportToExcel(warehouseName: string, initialQueryString: string) {
|
|
||||||
const params = new URLSearchParams(initialQueryString);
|
|
||||||
|
|
||||||
params.set('export', 'excel');
|
|
||||||
params.set('page', '1');
|
|
||||||
params.set('limit', '99999999999');
|
|
||||||
|
|
||||||
const queryString = `?${params.toString()}`;
|
|
||||||
|
|
||||||
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
|
||||||
method: 'GET',
|
|
||||||
responseType: 'blob',
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([res]));
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
|
|
||||||
const fileName = `informasi-stok-produk-${warehouseName.toLowerCase().replaceAll(' ', '-')}-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StockLogApi = new StockLogService('/inventory/stock-logs');
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user