mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe5a14a764 |
@@ -45,6 +45,3 @@ next-env.d.ts
|
|||||||
|
|
||||||
# claude
|
# claude
|
||||||
.claude
|
.claude
|
||||||
|
|
||||||
# rtk
|
|
||||||
rtk.exe
|
|
||||||
|
|||||||
+3
-50
@@ -15,7 +15,7 @@ default:
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
.build_template: &build_template
|
.build_template: &build_template
|
||||||
stage: build
|
stage: build
|
||||||
image: public.ecr.aws/docker/library/node:20-alpine
|
image: node:20-alpine
|
||||||
cache:
|
cache:
|
||||||
key: npm-cache
|
key: npm-cache
|
||||||
paths:
|
paths:
|
||||||
@@ -30,10 +30,6 @@ default:
|
|||||||
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
- echo "NEXT_PUBLIC_LTI_URL=$NEXT_PUBLIC_LTI_URL"
|
||||||
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
- echo "NEXT_PUBLIC_SSO_LOGIN_URL=$NEXT_PUBLIC_SSO_LOGIN_URL"
|
||||||
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
- echo "NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL"
|
||||||
- echo "NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV"
|
|
||||||
- echo "NEXT_PUBLIC_HELPDESK_URL=$NEXT_PUBLIC_HELPDESK_URL"
|
|
||||||
- echo "NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL=$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
|
||||||
- echo "NEXT_PUBLIC_S3_PUBLIC_BASE_URL=$NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
|
||||||
- echo "Building Next.js static export..."
|
- echo "Building Next.js static export..."
|
||||||
- npx next build
|
- npx next build
|
||||||
- |
|
- |
|
||||||
@@ -45,11 +41,7 @@ default:
|
|||||||
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
"built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||||
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
"NEXT_PUBLIC_LTI_URL": "$NEXT_PUBLIC_LTI_URL",
|
||||||
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
"NEXT_PUBLIC_SSO_LOGIN_URL": "$NEXT_PUBLIC_SSO_LOGIN_URL",
|
||||||
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL",
|
"NEXT_PUBLIC_API_BASE_URL": "$NEXT_PUBLIC_API_BASE_URL"
|
||||||
"NEXT_PUBLIC_APP_ENV": "$NEXT_PUBLIC_APP_ENV",
|
|
||||||
"NEXT_PUBLIC_HELPDESK_URL": "$NEXT_PUBLIC_HELPDESK_URL",
|
|
||||||
"NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL": "$NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL"
|
|
||||||
"NEXT_PUBLIC_S3_PUBLIC_BASE_URL": "NEXT_PUBLIC_S3_PUBLIC_BASE_URL"
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -64,7 +56,7 @@ default:
|
|||||||
.deploy_template: &deploy_template
|
.deploy_template: &deploy_template
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image:
|
image:
|
||||||
name: public.ecr.aws/aws-cli/aws-cli:latest
|
name: amazon/aws-cli:latest
|
||||||
entrypoint: ['/bin/sh', '-c']
|
entrypoint: ['/bin/sh', '-c']
|
||||||
script:
|
script:
|
||||||
- set -e
|
- set -e
|
||||||
@@ -150,10 +142,6 @@ build:dev:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
NEXT_PUBLIC_APP_ENV: 'development'
|
|
||||||
NEXT_PUBLIC_HELPDESK_URL: 'https://dev-helpdesk.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dev-dashboard-ho.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com'
|
|
||||||
|
|
||||||
deploy:dev:
|
deploy:dev:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -182,9 +170,6 @@ build:staging:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://stg-auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://stg-api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
NEXT_PUBLIC_APP_ENV: 'staging'
|
|
||||||
NEXT_PUBLIC_HELPDESK_URL: 'https://stg-helpdesk.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://stg-dashboard-ho.mbugroup.id/'
|
|
||||||
|
|
||||||
deploy:staging:
|
deploy:staging:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
@@ -198,35 +183,3 @@ deploy:staging:
|
|||||||
environment:
|
environment:
|
||||||
name: staging
|
name: staging
|
||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# ====== (Branch production) ======
|
|
||||||
# ==========================================================
|
|
||||||
build:production:
|
|
||||||
<<: *build_template
|
|
||||||
rules:
|
|
||||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
|
||||||
environment:
|
|
||||||
name: staging
|
|
||||||
variables:
|
|
||||||
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
|
|
||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
|
||||||
NEXT_PUBLIC_APP_ENV: 'production'
|
|
||||||
NEXT_PUBLIC_HELPDESK_URL: 'https://helpdesk.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dashboard-ho.mbugroup.id/'
|
|
||||||
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com/'
|
|
||||||
|
|
||||||
deploy:production:
|
|
||||||
<<: *deploy_template
|
|
||||||
needs: ['build:production']
|
|
||||||
rules:
|
|
||||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
|
||||||
variables:
|
|
||||||
S3_BUCKET: 'production-lti-erp.mbugroup.id'
|
|
||||||
AWS_REGION: 'ap-southeast-3'
|
|
||||||
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
|
|
||||||
environment:
|
|
||||||
name: staging
|
|
||||||
url: https://lti-erp.mbugroup.id
|
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npx tsc --noEmit
|
||||||
git add .
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Project-local RTK filters — commit this file with your repo.
|
|
||||||
# Filters here override user-global and built-in filters.
|
|
||||||
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
|
||||||
schema_version = 1
|
|
||||||
|
|
||||||
# Example: suppress build noise from a custom tool
|
|
||||||
# [filters.my-tool]
|
|
||||||
# description = "Compact my-tool output"
|
|
||||||
# match_command = "^my-tool\\s+build"
|
|
||||||
# strip_ansi = true
|
|
||||||
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
|
|
||||||
# max_lines = 30
|
|
||||||
# on_empty = "my-tool: ok"
|
|
||||||
@@ -1,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 -->
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM public.ecr.aws/docker/library/node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
RUN apk add --no-cache git bash build-base curl
|
RUN apk add --no-cache git bash build-base curl
|
||||||
|
|
||||||
@@ -22,4 +22,4 @@ RUN mkdir -p .next/server/app/_next && \
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
CMD ["npx", "serve", ".next/server/app", "-l", "3000"]
|
||||||
+1
-3
@@ -7,10 +7,8 @@
|
|||||||
"build": "next build --turbopack",
|
"build": "next build --turbopack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"typecheck": "next typegen && tsc --noEmit",
|
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write ."
|
||||||
"pre-commit": "npm run format && npm run lint && npm run typecheck && npm run build"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.1",
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import ClosingDetail from '@/components/pages/closing/ClosingDetailTabs';
|
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
|
||||||
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { FlockApi } from '@/services/api/master-data';
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
|
|
||||||
@@ -33,6 +34,33 @@ const ClosingDetailPage = () => {
|
|||||||
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
|
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: salesData, isLoading: isLoadingSales } = useSWR(
|
||||||
|
kandangId
|
||||||
|
? `sales-${closingId}-${kandangId}`
|
||||||
|
: closingId
|
||||||
|
? `sales-${closingId}`
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
kandangId
|
||||||
|
? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId))
|
||||||
|
: ClosingApi.getPenjualan(Number(closingId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR(
|
||||||
|
kandangId
|
||||||
|
? `hpp-ekspedisi-${closingId}-${kandangId}`
|
||||||
|
: closingId
|
||||||
|
? `hpp-ekspedisi-${closingId}`
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
kandangId
|
||||||
|
? ClosingApi.getHppEkspedisiByKandang(
|
||||||
|
Number(closingId),
|
||||||
|
Number(kandangId)
|
||||||
|
)
|
||||||
|
: ClosingApi.getHppEkspedisi(Number(closingId))
|
||||||
|
);
|
||||||
|
|
||||||
if (!closingId) {
|
if (!closingId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
@@ -48,7 +76,12 @@ const ClosingDetailPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang;
|
const isLoading =
|
||||||
|
isLoadingClosing ||
|
||||||
|
isLoadingSales ||
|
||||||
|
isLoadingHppEkspedisi ||
|
||||||
|
isLoadingProject ||
|
||||||
|
isLoadingKandang;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
@@ -58,6 +91,12 @@ const ClosingDetailPage = () => {
|
|||||||
<ClosingDetail
|
<ClosingDetail
|
||||||
id={Number(closingId)}
|
id={Number(closingId)}
|
||||||
initialValue={closing.data}
|
initialValue={closing.data}
|
||||||
|
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
||||||
|
hppExpeditionData={
|
||||||
|
isResponseSuccess(hppEkspedisiData)
|
||||||
|
? hppEkspedisiData.data
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
projectData={
|
projectData={
|
||||||
isResponseSuccess(projectData) ? projectData.data : undefined
|
isResponseSuccess(projectData) ? projectData.data : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable';
|
|||||||
|
|
||||||
const Closing = () => {
|
const Closing = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-3'>
|
<section className='w-full p-4'>
|
||||||
<ClosingsTable />
|
<ClosingsTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
|
|
||||||
|
|
||||||
const MasterKandangPage = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full'>
|
|
||||||
<MasterKandangContent />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MasterKandangPage;
|
|
||||||
@@ -15,8 +15,8 @@ const ExpenseDetailPage = () => {
|
|||||||
const expenseId = searchParams.get('expenseId');
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
['expense-detail', expenseId],
|
expenseId,
|
||||||
([_, id]) => ExpenseApi.getSingle(Number(id))
|
(id: number) => ExpenseApi.getSingle(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!expenseId) {
|
if (!expenseId) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useSWR from 'swr';
|
|||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
||||||
|
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||||
|
|
||||||
const EditFinanceTransactionPage = () => {
|
const EditFinanceTransactionPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import FinanceDetail from '@/components/pages/finance/FinanceDetail';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
const FinanceDetailPage = () => {
|
const FinanceDetailPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
import FinanceTable from '@/components/pages/finance/FinanceTable';
|
import FinanceTable from '@/components/pages/finance/FinanceTable';
|
||||||
|
|
||||||
const Finance = () => {
|
const Finance = () => {
|
||||||
return <FinanceTable />;
|
return (
|
||||||
|
<section className='size-full p-6'>
|
||||||
|
<div className='flex flex-row gap-4'></div>
|
||||||
|
<FinanceTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Finance;
|
export default Finance;
|
||||||
|
|||||||
@@ -68,8 +68,6 @@
|
|||||||
|
|
||||||
--shadow-button-soft:
|
--shadow-button-soft:
|
||||||
0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200);
|
0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200);
|
||||||
|
|
||||||
--shadow-bg: 0px -2px 4px 0px #00000014;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import InventoryAdjustmentTable from '@/components/pages/inventory/adjustment/In
|
|||||||
|
|
||||||
const InventoryAdjustment = () => {
|
const InventoryAdjustment = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full'>
|
<section className='w-full p-4'>
|
||||||
<InventoryAdjustmentTable />
|
<InventoryAdjustmentTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditMarketingDelivery = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='add_deliver'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditMarketingDelivery;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
|
||||||
|
const AddSalesOrder = () => {
|
||||||
|
return (
|
||||||
|
<div className='size-full p-4'>
|
||||||
|
<MarketingForm formType='add' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddSalesOrder;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditMarketingDelivery = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isResponseSuccess(marketing) &&
|
||||||
|
marketing.data.latest_approval.step_number != 3
|
||||||
|
) {
|
||||||
|
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='edit_deliver'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditMarketingDelivery;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const DetailMarketing = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingDetail
|
||||||
|
initialValues={marketing.data}
|
||||||
|
refresh={refreshMarketing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailMarketing;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditSalesOrder = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const soId = searchParams.get('marketingId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: marketing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
mutate: refreshMarketing,
|
||||||
|
} = useSWR(`get-so-${soId}`, () =>
|
||||||
|
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!soId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoading && isResponseSuccess(marketing) && (
|
||||||
|
<MarketingForm
|
||||||
|
formType='edit'
|
||||||
|
initialValues={marketing.data}
|
||||||
|
afterSubmit={() => {
|
||||||
|
refreshMarketing();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default EditSalesOrder;
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import AreasTable from '@/components/pages/master-data/area/AreasTable';
|
import AreasTable from '@/components/pages/master-data/area/AreasTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return <AreasTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<AreasTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import BanksTable from '@/components/pages/master-data/bank/BanksTable';
|
import BanksTable from '@/components/pages/master-data/bank/BanksTable';
|
||||||
|
|
||||||
const Bank = () => {
|
const Bank = () => {
|
||||||
return <BanksTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<BanksTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Bank;
|
export default Bank;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
|
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
|
||||||
|
|
||||||
const Customer = () => {
|
const Customer = () => {
|
||||||
return <CustomersTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<CustomersTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Customer;
|
export default Customer;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||||
|
|
||||||
|
const AddFcr = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<FcrForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddFcr;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||||
|
|
||||||
|
import { FcrApi } from '@/services/api/master-data';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
||||||
|
|
||||||
|
const FcrEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const fcrId = searchParams.get('fcrId');
|
||||||
|
|
||||||
|
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
||||||
|
fcrId,
|
||||||
|
(id: number) =>
|
||||||
|
FcrApi.getSingle(id) as Promise<
|
||||||
|
BaseApiResponse<FcrWithStandards> | undefined
|
||||||
|
>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fcrId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
||||||
|
<FcrForm type='edit' initialValues={fcr.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FcrEdit;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||||
|
|
||||||
|
import { FcrApi } from '@/services/api/master-data';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
|
||||||
|
const FcrDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const fcrId = searchParams.get('fcrId');
|
||||||
|
|
||||||
|
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
||||||
|
fcrId,
|
||||||
|
(id: number) =>
|
||||||
|
FcrApi.getSingle(id) as Promise<
|
||||||
|
BaseApiResponse<FcrWithStandards> | undefined
|
||||||
|
>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fcrId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
||||||
|
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
||||||
|
<FcrForm type='detail' initialValues={fcr.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FcrDetail;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable';
|
||||||
|
|
||||||
|
const Fcr = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<FcrsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Fcr;
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
|
import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
|
||||||
|
|
||||||
const Flock = () => {
|
const Flock = () => {
|
||||||
return <FlockTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<FlockTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Flock;
|
export default Flock;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable';
|
import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return <KandangsTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<KandangsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import LocationsTable from '@/components/pages/master-data/location/LocationsTable';
|
import LocationsTable from '@/components/pages/master-data/location/LocationsTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return <LocationsTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<LocationsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable';
|
import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return <NonstocksTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<NonstocksTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
|
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
|
||||||
|
|
||||||
const ProductCategory = () => {
|
const ProductCategory = () => {
|
||||||
return <ProductCategoryTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ProductCategoryTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProductCategory;
|
export default ProductCategory;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import ProductsTable from '@/components/pages/master-data/product/ProductTable';
|
import ProductsTable from '@/components/pages/master-data/product/ProductTable';
|
||||||
|
|
||||||
const Product = () => {
|
const Product = () => {
|
||||||
return <ProductsTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<ProductsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Product;
|
export default Product;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
|
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
|
||||||
|
|
||||||
const ProductionStandardPage = () => {
|
const ProductionStandardPage = () => {
|
||||||
return <ProductionStandardTable />;
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<ProductionStandardTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProductionStandardPage;
|
export default ProductionStandardPage;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
|
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
|
||||||
|
|
||||||
const Supplier = () => {
|
const Supplier = () => {
|
||||||
return <SuppliersTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<SuppliersTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Supplier;
|
export default Supplier;
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { SystemConfigContent } from '@/figma-make/components/pages/master-data/system-config/SystemConfigContent';
|
|
||||||
|
|
||||||
const SystemConfigPage = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full'>
|
|
||||||
<SystemConfigContent />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemConfigPage;
|
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import UomsTable from '@/components/pages/master-data/uom/UomsTable';
|
import UomsTable from '@/components/pages/master-data/uom/UomsTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return <UomsTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<UomsTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable';
|
import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable';
|
||||||
|
|
||||||
const Warehouse = () => {
|
const Warehouse = () => {
|
||||||
return <WarehousesTable />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<WarehousesTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Warehouse;
|
export default Warehouse;
|
||||||
|
|||||||
+2
-1
@@ -3,9 +3,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
import { redirectToSSO } from '@/lib/auth-helper';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { isLoadingUser } = useAuth();
|
const { user, isLoadingUser } = useAuth();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
import React from 'react';
|
import React, { useImperativeHandle } from 'react';
|
||||||
// import React, { useImperativeHandle } from 'react';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const AddProjectFlock = () => {
|
const AddProjectFlock = () => {
|
||||||
// useImperativeHandle(ref, () => ({
|
// useImperativeHandle(ref, () => ({
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ const ProjectFlockEdit = () => {
|
|||||||
|
|
||||||
const projectFlockId = searchParams.get('projectFlockId');
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
const {
|
||||||
projectFlockId,
|
data: projectFlock,
|
||||||
(id: number) => ProjectFlockApi.getSingle(id)
|
isLoading: isLoadingProjectFlock,
|
||||||
);
|
mutate: refreshProjectFlocks,
|
||||||
|
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
||||||
|
|
||||||
if (!projectFlockId) {
|
if (!projectFlockId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail';
|
import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail';
|
||||||
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
@@ -12,10 +13,11 @@ const ProjectFlockDetailPage = () => {
|
|||||||
|
|
||||||
const projectFlockId = searchParams.get('projectFlockId');
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
const {
|
||||||
projectFlockId,
|
data: projectFlock,
|
||||||
(id: number) => ProjectFlockApi.getSingle(id)
|
isLoading: isLoadingProjectFlock,
|
||||||
);
|
mutate: refreshProjectFlock,
|
||||||
|
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
||||||
|
|
||||||
if (!projectFlockId) {
|
if (!projectFlockId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { ReactNode, useEffect } from 'react';
|
import Drawer from '@/components/Drawer';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
|
||||||
|
|
||||||
export default function ProjectFlockLayout({
|
export default function ProjectFlockLayout({
|
||||||
children,
|
children,
|
||||||
@@ -23,12 +23,9 @@ export default function ProjectFlockLayout({
|
|||||||
|
|
||||||
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
|
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
|
||||||
|
|
||||||
const formModal = useModal();
|
|
||||||
|
|
||||||
const handleBackdropClick = () => {
|
const handleBackdropClick = () => {
|
||||||
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
formModal.closeModal();
|
|
||||||
unsub(); // berhenti listen
|
unsub(); // berhenti listen
|
||||||
router.push('/production/project-flock');
|
router.push('/production/project-flock');
|
||||||
}
|
}
|
||||||
@@ -37,14 +34,6 @@ export default function ProjectFlockLayout({
|
|||||||
toggleValidate();
|
toggleValidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen && !formModal.open) {
|
|
||||||
formModal.openModal();
|
|
||||||
} else {
|
|
||||||
formModal.closeModal();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* List page always rendered */}
|
{/* List page always rendered */}
|
||||||
@@ -54,19 +43,18 @@ export default function ProjectFlockLayout({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Render Modal only on /add */}
|
{/* Render Drawer only on /add */}
|
||||||
<Modal
|
<Drawer
|
||||||
ref={formModal.ref}
|
open={isOpen}
|
||||||
position='end'
|
setOpen={(v) => {
|
||||||
onBackdropClick={handleBackdropClick}
|
if (!v) router.push('/production/project-flock');
|
||||||
className={{
|
|
||||||
modalBox: 'w-full sm:w-fit p-3 rounded-xl bg-transparent shadow-none',
|
|
||||||
}}
|
}}
|
||||||
>
|
closeOnBackdropClick={isDetail ? true : false}
|
||||||
<div className='w-full sm:w-[446px] h-full flex flex-col sm:flex-row items-stretch bg-base-100 rounded-xl overflow-hidden'>
|
onBackdropClick={handleBackdropClick}
|
||||||
{isOpen && children}
|
variant='right'
|
||||||
</div>
|
zIndex='99999'
|
||||||
</Modal>
|
sidebarContent={isOpen && <div className=''>{children}</div>}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ const RecordingEdit = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const recordingId = searchParams.get('recordingId');
|
const recordingId = searchParams.get('recordingId');
|
||||||
const recordingDetailKey = recordingId
|
|
||||||
? ['recording-detail', recordingId]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingDetailKey,
|
recordingId,
|
||||||
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
|
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ const RecordingDetail = () => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const recordingId = searchParams.get('recordingId');
|
const recordingId = searchParams.get('recordingId');
|
||||||
const recordingDetailKey = recordingId
|
|
||||||
? ['recording-detail', recordingId]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
recordingDetailKey,
|
recordingId,
|
||||||
([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id))
|
(id: string) => RecordingApi.getSingle(parseInt(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!recordingId) {
|
if (!recordingId) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab
|
|||||||
|
|
||||||
const Recording = () => {
|
const Recording = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full'>
|
<section className='w-full p-4 sm:p-0'>
|
||||||
<RecordingTable />
|
<RecordingTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ReportExpenseTabs from '@/components/pages/report/expense/ReportExpenseTabs';
|
import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable';
|
||||||
|
|
||||||
const ReportExpense = () => {
|
const ReportExpense = () => {
|
||||||
return <ReportExpenseTabs />;
|
return (
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
<ReportExpenseTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReportExpense;
|
export default ReportExpense;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import MarketingReportContent from '@/components/pages/report/marketing/MarketingTabs';
|
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
||||||
|
|
||||||
const MarketingReportPage = () => {
|
const MarketingReportPage = () => {
|
||||||
return <MarketingReportContent />;
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<MarketingReportContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MarketingReportPage;
|
export default MarketingReportPage;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs';
|
import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent';
|
||||||
|
|
||||||
const ProductionResultReportPage = () => {
|
const ProductionResultReportPage = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full max-w-full'>
|
<section className='w-full max-w-7xl pb-16'>
|
||||||
<ProductionResultTabs />
|
<ProductionResultContent />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { ReactNode, Ref } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
ref?: Ref<HTMLDivElement> | undefined;
|
|
||||||
variant?: 'outline' | 'dash' | 'soft';
|
variant?: 'outline' | 'dash' | 'soft';
|
||||||
color?: 'info' | 'success' | 'warning' | 'error';
|
color?: 'info' | 'success' | 'warning' | 'error';
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Alert = ({ children, ref, variant, color, className }: AlertProps) => {
|
const Alert = ({ children, variant, color, className }: AlertProps) => {
|
||||||
const alertBaseClassName = cn('alert', {
|
const alertBaseClassName = cn('alert', {
|
||||||
'alert-soft': variant === 'soft',
|
'alert-soft': variant === 'soft',
|
||||||
'alert-outline': variant === 'outline',
|
'alert-outline': variant === 'outline',
|
||||||
@@ -22,11 +21,7 @@ const Alert = ({ children, ref, variant, color, className }: AlertProps) => {
|
|||||||
'alert-error': color === 'error',
|
'alert-error': color === 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return <div className={cn(alertBaseClassName, className)}>{children}</div>;
|
||||||
<div ref={ref} className={cn(alertBaseClassName, className)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Alert;
|
export default Alert;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const Button = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(!href || (href && disabled)) && (
|
{!href && (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
type={type}
|
type={type}
|
||||||
@@ -68,9 +68,9 @@ const Button = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{href && !disabled && (
|
{href && (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={disabled ? '#' : href}
|
||||||
target={target}
|
target={target}
|
||||||
rel={rel}
|
rel={rel}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
|||||||
@@ -108,9 +108,7 @@ const Drawer = ({
|
|||||||
if (closeOnBackdropClick) {
|
if (closeOnBackdropClick) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
if (onBackdropClick) {
|
onBackdropClick && onBackdropClick();
|
||||||
onBackdropClick();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@@ -12,6 +13,7 @@ import PermissionNotFound from '@/components/helper/PermissionNotFound';
|
|||||||
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
||||||
|
import { isPathActive } from '@/lib/helper';
|
||||||
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,7 @@ export const useModal = (isNestingModal = false) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
if (open) {
|
open ? closeModal() : openModal();
|
||||||
closeModal();
|
|
||||||
} else {
|
|
||||||
openModal();
|
|
||||||
}
|
|
||||||
}, [open, closeModal, openModal]);
|
}, [open, closeModal, openModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -26,17 +26,13 @@ const Navbar = ({ toggleSidebar }: NavbarProps) => {
|
|||||||
|
|
||||||
const logoutClickHandler = async () => {
|
const logoutClickHandler = async () => {
|
||||||
const logoutRes = await AuthApi.logout();
|
const logoutRes = await AuthApi.logout();
|
||||||
|
|
||||||
if (isResponseError(logoutRes)) {
|
if (isResponseError(logoutRes)) {
|
||||||
toast.error('Gagal logout! Coba lagi!');
|
toast.error('Gagal logout! Coba lagi!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
const redirect = (logoutRes as { redirect?: string })?.redirect;
|
|
||||||
if (redirect) {
|
|
||||||
window.location.href = redirect;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
|
router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const emptyContentDefaultValue = (
|
const emptyContentDefaultValue = (
|
||||||
<div className='w-full text-center py-4'>
|
<div className='w-full p-5 text-center'>
|
||||||
<span className='text-sm opacity-50'>
|
<span className='text-lg opacity-50'>
|
||||||
Tidak ada data yang dapat ditampilkan...
|
Tidak ada data yang dapat ditampilkan...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -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(),
|
||||||
@@ -453,20 +452,6 @@ const Table = <TData extends object>({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
|
|
||||||
!isLoading && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={
|
|
||||||
table.getAllLeafColumns().length + (withCheckbox ? 1 : 0)
|
|
||||||
}
|
|
||||||
className='p-0'
|
|
||||||
>
|
|
||||||
{emptyContent}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot
|
<tfoot
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -504,6 +489,10 @@ const Table = <TData extends object>({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
|
||||||
|
!isLoading &&
|
||||||
|
emptyContent}
|
||||||
|
|
||||||
{data.length > 0 &&
|
{data.length > 0 &&
|
||||||
table.getRowModel().rows.length > 0 &&
|
table.getRowModel().rows.length > 0 &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HTMLAttributes, ReactNode, useState } from 'react';
|
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export interface TabItem {
|
export interface TabItem {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import Button from '@/components/Button';
|
|||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
|
|
||||||
interface ApprovalStepsV2Props {
|
interface ApprovalStepsV2Props {
|
||||||
title?: string;
|
|
||||||
approvals?: BaseApproval[];
|
approvals?: BaseApproval[];
|
||||||
steps: {
|
steps: {
|
||||||
step_number: number;
|
step_number: number;
|
||||||
@@ -24,7 +23,6 @@ interface ApprovalStepsV2Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ApprovalStepsV2 = ({
|
const ApprovalStepsV2 = ({
|
||||||
title = 'Progress Details',
|
|
||||||
approvals,
|
approvals,
|
||||||
steps,
|
steps,
|
||||||
maxVisibleSteps = 2,
|
maxVisibleSteps = 2,
|
||||||
@@ -101,7 +99,7 @@ const ApprovalStepsV2 = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
{title}
|
Progress Details
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,51 +3,15 @@ import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { FormikValues } from 'formik';
|
import { FormikValues } from 'formik';
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export type ButtonFilterProps = ButtonProps & {
|
export type ButtonFilterProps = ButtonProps & {
|
||||||
values: FormikValues;
|
values: FormikValues;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
excludeFields?: string[];
|
|
||||||
fieldGroups?: string[][];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200
|
// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200
|
||||||
|
|
||||||
const ButtonFilter = ({
|
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
||||||
values,
|
|
||||||
onClick,
|
|
||||||
excludeFields = [],
|
|
||||||
fieldGroups = [],
|
|
||||||
...props
|
|
||||||
}: ButtonFilterProps) => {
|
|
||||||
const activeCount = useMemo(() => {
|
|
||||||
const filteredValues: FormikValues = {};
|
|
||||||
Object.keys(values).forEach((key) => {
|
|
||||||
if (!excludeFields.includes(key)) {
|
|
||||||
filteredValues[key] = values[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let count = getFilledFormikValuesCount(filteredValues);
|
|
||||||
|
|
||||||
fieldGroups.forEach((group) => {
|
|
||||||
const groupFields = group.filter(
|
|
||||||
(field) => !excludeFields.includes(field)
|
|
||||||
);
|
|
||||||
const filledGroupFields = groupFields.filter(
|
|
||||||
(field) => filteredValues[field]
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
filledGroupFields.length === groupFields.length &&
|
|
||||||
groupFields.length > 1
|
|
||||||
) {
|
|
||||||
count -= groupFields.length - 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [values, excludeFields, fieldGroups]);
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
@@ -57,7 +21,7 @@ const ButtonFilter = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'rounded-lg max-h-10 font-semibold text-sm gap-1.5',
|
'rounded-lg max-h-10 font-semibold text-sm gap-1.5',
|
||||||
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
||||||
activeCount > 0
|
getFilledFormikValuesCount(values) > 0
|
||||||
? 'border-primary-gradient text-primary rounded-lg!'
|
? 'border-primary-gradient text-primary rounded-lg!'
|
||||||
: 'rounded-lg',
|
: 'rounded-lg',
|
||||||
props.className
|
props.className
|
||||||
@@ -67,12 +31,14 @@ const ButtonFilter = ({
|
|||||||
icon='heroicons:funnel'
|
icon='heroicons:funnel'
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className={activeCount > 0 ? 'text-blue-600' : ''}
|
className={
|
||||||
|
getFilledFormikValuesCount(values) > 0 ? 'text-blue-600' : ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
Filter
|
Filter
|
||||||
{activeCount > 0 && (
|
{getFilledFormikValuesCount(values) > 0 && (
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
||||||
{activeCount}
|
{getFilledFormikValuesCount(values)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
interface StatusBadgeProps {
|
||||||
color: Color;
|
color: Color;
|
||||||
text: ReactNode;
|
text: string;
|
||||||
className?: {
|
className?: {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
};
|
};
|
||||||
onClick?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusBadge = ({
|
const StatusBadge = ({
|
||||||
color = 'neutral',
|
color = 'neutral',
|
||||||
text,
|
text,
|
||||||
className,
|
className,
|
||||||
onClick,
|
|
||||||
}: StatusBadgeProps) => {
|
}: StatusBadgeProps) => {
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
onClick={onClick}
|
|
||||||
className={{
|
className={{
|
||||||
badge: cn(
|
badge: cn(
|
||||||
'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content',
|
'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content',
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface DrawerHeaderProps {
|
|||||||
|
|
||||||
const DrawerHeader = ({
|
const DrawerHeader = ({
|
||||||
leftIcon = 'mdi:close',
|
leftIcon = 'mdi:close',
|
||||||
leftIconSize = 20,
|
leftIconSize = 24,
|
||||||
leftIconHref,
|
leftIconHref,
|
||||||
leftIconOnClick,
|
leftIconOnClick,
|
||||||
leftIconClassName,
|
leftIconClassName,
|
||||||
@@ -43,7 +43,7 @@ const DrawerHeader = ({
|
|||||||
icon={leftIcon}
|
icon={leftIcon}
|
||||||
width={leftIconSize}
|
width={leftIconSize}
|
||||||
height={leftIconSize}
|
height={leftIconSize}
|
||||||
className={cn('cursor-pointer text-base-content ', leftIconClassName)}
|
className={cn('cursor-pointer', leftIconClassName)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ const DrawerHeader = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-row justify-between items-center p-4 border-b border-base-content/10',
|
'flex flex-row justify-between items-center px-4 pt-4 pb-4 border-b border-base-content/10',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -82,7 +82,7 @@ const DrawerHeader = ({
|
|||||||
{renderLeftIcon()}
|
{renderLeftIcon()}
|
||||||
|
|
||||||
{showDivider && subtitle && (
|
{showDivider && subtitle && (
|
||||||
<div className='w-px h-full border-none bg-base-content/10' />
|
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Alert from '@/components/Alert';
|
import Alert from '@/components/Alert';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alert Unique Error List
|
* Alert Unique Error List
|
||||||
@@ -31,22 +29,10 @@ const AlertErrorList = ({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const alertRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formErrorList.length > 0) {
|
|
||||||
alertRef.current?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [formErrorList.length]);
|
|
||||||
|
|
||||||
if (formErrorList.length === 0) return null;
|
if (formErrorList.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
ref={alertRef}
|
|
||||||
color='error'
|
color='error'
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex flex-col gap-2 px-3 rounded-lg',
|
'w-full flex flex-col gap-2 px-3 rounded-lg',
|
||||||
@@ -71,7 +57,6 @@ const AlertErrorList = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
variant='link'
|
variant='link'
|
||||||
className={cn('ml-auto p-0 w-fit text-white', className?.button)}
|
className={cn('ml-auto p-0 w-fit text-white', className?.button)}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
|
||||||
import type { Style } from '@react-pdf/types';
|
|
||||||
|
|
||||||
type PdfParamBadgeProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
style?: Style;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
parameterBadge: {
|
|
||||||
backgroundColor: '#F5F5F5',
|
|
||||||
color: '#333333',
|
|
||||||
padding: 4,
|
|
||||||
borderRadius: 4,
|
|
||||||
fontSize: 8,
|
|
||||||
marginRight: 8,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => {
|
|
||||||
return (
|
|
||||||
<View style={[styles.parameterBadge, ...(style ? [style] : [])]}>
|
|
||||||
<Text>{children}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
|
||||||
import type { Style } from '@react-pdf/types';
|
|
||||||
|
|
||||||
type PdfStatusBadgeProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
style?: Style;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
statusBadge: {
|
|
||||||
paddingVertical: 2,
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
fontSize: 7,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
backgroundColor: '#F5F5F5',
|
|
||||||
borderColor: '#E5E7EB',
|
|
||||||
},
|
|
||||||
statusBadgeText: {
|
|
||||||
fontSize: 7,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#333333',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => {
|
|
||||||
const styleRecord = style as Record<string, unknown>;
|
|
||||||
const color = styleRecord?.color as string | undefined;
|
|
||||||
|
|
||||||
const viewStyle = Object.entries(styleRecord || {}).reduce(
|
|
||||||
(acc, [key, value]) => {
|
|
||||||
if (key !== 'color') {
|
|
||||||
acc[key] = value;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, unknown>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.statusBadge,
|
|
||||||
...(Object.keys(viewStyle).length > 0 ? [viewStyle as Style] : []),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.statusBadgeText, ...(color ? [{ color }] : [])]}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
|
||||||
import type { Style } from '@react-pdf/types';
|
|
||||||
|
|
||||||
type PdfPageNumberProps = {
|
|
||||||
style?: Style;
|
|
||||||
/**
|
|
||||||
* Format template for page number.
|
|
||||||
* Use {pageNumber} and {totalPages} as placeholders.
|
|
||||||
* Default: "{pageNumber} / {totalPages}"
|
|
||||||
*/
|
|
||||||
format?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
footer: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
position: 'absolute',
|
|
||||||
fontSize: 8,
|
|
||||||
bottom: 30,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'grey',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PdfPageNumber = ({
|
|
||||||
style,
|
|
||||||
format = '{pageNumber} / {totalPages}',
|
|
||||||
}: PdfPageNumberProps) => {
|
|
||||||
return (
|
|
||||||
<View style={style || styles.footer} fixed>
|
|
||||||
<Text
|
|
||||||
render={({ pageNumber, totalPages }) =>
|
|
||||||
format
|
|
||||||
.replace('{pageNumber}', String(pageNumber))
|
|
||||||
.replace('{totalPages}', String(totalPages))
|
|
||||||
}
|
|
||||||
fixed
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { View, StyleSheet } from '@react-pdf/renderer';
|
import { View, StyleSheet } from '@react-pdf/renderer';
|
||||||
import type { PdfColumn } from './types';
|
import { PdfThead, PdfColumn } from './PdfThead';
|
||||||
import { PdfThead } from './PdfThead';
|
import { PdfTbody, PdfTbodyCell } from './PdfTbody';
|
||||||
import { PdfTbody } from './PdfTbody';
|
import { PdfTfoot, PdfTfootCell } from './PdfTfoot';
|
||||||
import { PdfTfoot } from './PdfTfoot';
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
table: {
|
table: {
|
||||||
@@ -14,10 +13,10 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTableProps<TData = Record<string, unknown>> {
|
interface PdfTableProps {
|
||||||
columns: PdfColumn<TData>[];
|
columns: PdfColumn[];
|
||||||
data: TData[];
|
data: PdfTbodyCell[][];
|
||||||
showFooter?: boolean;
|
footer?: PdfTfootCell[];
|
||||||
footerLabel?: string;
|
footerLabel?: string;
|
||||||
firstRow?: {
|
firstRow?: {
|
||||||
valueKey: string;
|
valueKey: string;
|
||||||
@@ -27,26 +26,20 @@ interface PdfTableProps<TData = Record<string, unknown>> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfTable = <TData = Record<string, unknown>,>({
|
export const PdfTable = ({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
showFooter = false,
|
footer,
|
||||||
footerLabel = 'Total',
|
footerLabel = 'Total',
|
||||||
firstRow,
|
firstRow,
|
||||||
}: PdfTableProps<TData>) => {
|
}: PdfTableProps) => {
|
||||||
// Check if any column has footer defined
|
|
||||||
const hasFooter =
|
|
||||||
showFooter || columns.some((col) => col.footer !== undefined);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.table}>
|
<View style={styles.table}>
|
||||||
<PdfThead columns={columns} data={data} />
|
<PdfThead columns={columns} />
|
||||||
<PdfTbody columns={columns} data={data} firstRow={firstRow} />
|
<PdfTbody columns={columns} rows={data} firstRow={firstRow} />
|
||||||
{hasFooter && data.length > 0 && (
|
{footer && footer.length > 0 && (
|
||||||
<PdfTfoot columns={columns} data={data} label={footerLabel} />
|
<PdfTfoot columns={columns} cells={footer} label={footerLabel} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { PdfColumn };
|
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import type { PdfColumn } from './types';
|
export interface PdfColumn {
|
||||||
|
key: string;
|
||||||
|
header: string;
|
||||||
|
flex: number;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PdfTbodyCell {
|
||||||
|
key: string;
|
||||||
|
value: string | number | React.ReactNode;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
color?: string;
|
||||||
|
formatAs?: 'text' | 'date' | 'currency' | 'number';
|
||||||
|
formatDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tableRow: {
|
tableRow: {
|
||||||
@@ -57,22 +71,21 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTbodyProps<TData = Record<string, unknown>> {
|
interface PdfTbodyProps {
|
||||||
columns: PdfColumn<TData>[];
|
columns: PdfColumn[];
|
||||||
data: TData[];
|
rows: PdfTbodyCell[][];
|
||||||
firstRow?: {
|
firstRow?: {
|
||||||
valueKey: string;
|
valueKey: string;
|
||||||
value: number;
|
value: number;
|
||||||
align?: 'right';
|
align?: 'right';
|
||||||
color?: string;
|
color?: string;
|
||||||
};
|
};
|
||||||
|
formatDate?: (date: string, format: string) => string;
|
||||||
|
formatNumber?: (num: number) => string;
|
||||||
|
formatCurrency?: (num: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfTbody = <TData = Record<string, unknown>,>({
|
export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
firstRow,
|
|
||||||
}: PdfTbodyProps<TData>) => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* First Row */}
|
{/* First Row */}
|
||||||
@@ -80,17 +93,17 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
<View style={[styles.tableRow, styles.tableBorderBottom]}>
|
<View style={[styles.tableRow, styles.tableBorderBottom]}>
|
||||||
{columns.map((column, index) => {
|
{columns.map((column, index) => {
|
||||||
const isLastColumn = index === columns.length - 1;
|
const isLastColumn = index === columns.length - 1;
|
||||||
const isFirstRowColumn = column.key === firstRow.valueKey;
|
const isfirstRowColumn = column.key === firstRow.valueKey;
|
||||||
const align = column.align || 'left';
|
const align = column.align || 'center';
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
column.key === 'no'
|
column.key === 'no'
|
||||||
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
? [styles.tableCellNo, { flex: column.flex }]
|
||||||
: isFirstRowColumn
|
: isfirstRowColumn
|
||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
color: firstRow.color || 'black',
|
color: firstRow.color || 'black',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
@@ -99,7 +112,7 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -107,7 +120,7 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
? [
|
? [
|
||||||
styles.tableCellCenter,
|
styles.tableCellCenter,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -115,15 +128,15 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
? [
|
? [
|
||||||
styles.tableCellLast,
|
styles.tableCellLast,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
borderRightWidth: 0,
|
borderRightWidth: 0,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [styles.tableCell, { flex: column.flex || 1 }];
|
: [styles.tableCell, { flex: column.flex }];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
<Text>{isFirstRowColumn ? firstRow.value : ''}</Text>
|
<Text>{isfirstRowColumn ? firstRow.value : ''}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -131,8 +144,8 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Data Rows */}
|
{/* Data Rows */}
|
||||||
{data.map((row, rowIndex) => {
|
{rows.map((row, rowIndex) => {
|
||||||
const isLastRow = rowIndex === data.length - 1;
|
const isLastRow = rowIndex === rows.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -143,27 +156,19 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{columns.map((column, colIndex) => {
|
{columns.map((column, colIndex) => {
|
||||||
|
const cell = row.find((c) => c.key === column.key);
|
||||||
const isLastColumn = colIndex === columns.length - 1;
|
const isLastColumn = colIndex === columns.length - 1;
|
||||||
const align = column.align || 'left';
|
const align = cell?.align || column.align || 'center';
|
||||||
|
|
||||||
// Get cell content from column.cell function or fallback to row value
|
|
||||||
let cellContent: ReactNode;
|
|
||||||
if (column.cell) {
|
|
||||||
cellContent = column.cell({ row, index: rowIndex });
|
|
||||||
} else {
|
|
||||||
cellContent =
|
|
||||||
((row as Record<string, unknown>)[column.key] as ReactNode) ??
|
|
||||||
'-';
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
column.key === 'no'
|
column.key === 'no'
|
||||||
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
? [styles.tableCellNo, { flex: column.flex }]
|
||||||
: align === 'right'
|
: align === 'right'
|
||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
|
color: cell?.color || 'black',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -171,30 +176,37 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
? [
|
? [
|
||||||
styles.tableCellCenter,
|
styles.tableCellCenter,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
|
color: cell?.color || 'black',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: isLastColumn
|
: isLastColumn
|
||||||
? [
|
? [
|
||||||
styles.tableCellLast,
|
styles.tableCellLast,
|
||||||
{ flex: column.flex || 1, borderRightWidth: 0 },
|
{ flex: column.flex, borderRightWidth: 0 },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
styles.tableCell,
|
styles.tableCell,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
|
color: cell?.color || 'black',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
{typeof cellContent === 'string' ||
|
{cell?.value !== undefined &&
|
||||||
typeof cellContent === 'number' ? (
|
cell?.value !== null &&
|
||||||
<Text>{String(cellContent)}</Text>
|
cell?.value !== '' ? (
|
||||||
|
typeof cell.value === 'object' ? (
|
||||||
|
cell.value
|
||||||
|
) : (
|
||||||
|
<Text>{String(cell.value)}</Text>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
cellContent
|
<Text>-</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -205,5 +217,3 @@ export const PdfTbody = <TData = Record<string, unknown>,>({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { PdfColumn };
|
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import type { PdfColumn } from './types';
|
export interface PdfColumn {
|
||||||
|
key: string;
|
||||||
|
header: string;
|
||||||
|
flex: number;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PdfTfootCell {
|
||||||
|
key: string;
|
||||||
|
value: string | number;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
flex?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tableRow: {
|
tableRow: {
|
||||||
@@ -56,86 +69,63 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTfootProps<TData = Record<string, unknown>> {
|
interface PdfTfootProps {
|
||||||
columns: PdfColumn<TData>[];
|
columns: PdfColumn[];
|
||||||
data: TData[];
|
cells: PdfTfootCell[];
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfTfoot = <TData = Record<string, unknown>,>({
|
export const PdfTfoot = ({
|
||||||
columns,
|
columns,
|
||||||
data,
|
cells,
|
||||||
label = 'Total',
|
label = 'Total',
|
||||||
}: PdfTfootProps<TData>) => {
|
}: PdfTfootProps) => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.tableRow, styles.summaryRow]}>
|
<View style={[styles.tableRow, styles.summaryRow]}>
|
||||||
{columns.map((column, index) => {
|
{columns.map((column, index) => {
|
||||||
const isLastColumn = index === columns.length - 1;
|
const isLastColumn = index === columns.length - 1;
|
||||||
|
const cellData = cells.find((c) => c.key === column.key);
|
||||||
// Get footer content from column definition
|
|
||||||
let footerContent: ReactNode;
|
|
||||||
if (typeof column.footer === 'function') {
|
|
||||||
footerContent = column.footer(data);
|
|
||||||
} else {
|
|
||||||
footerContent = column.footer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use label for first column (usually 'no' column)
|
|
||||||
const displayContent = column.key === 'no' ? label : footerContent;
|
|
||||||
|
|
||||||
// Determine alignment
|
|
||||||
const align = column.footerAlign || column.align || 'left';
|
|
||||||
const color = column.footerColor || 'black';
|
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
column.key === 'no'
|
column.key === 'no'
|
||||||
? [
|
? [
|
||||||
styles.tableCellNo,
|
styles.tableCellNo,
|
||||||
{
|
{ flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 },
|
||||||
flex: column.flex || 1,
|
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
: align === 'right'
|
: cellData?.align === 'right'
|
||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
color,
|
color: cellData?.color || 'black',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: align === 'center'
|
: cellData?.align === 'center'
|
||||||
? [
|
? [
|
||||||
styles.tableCellCenter,
|
styles.tableCellCenter,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
color,
|
color: cellData?.color || 'black',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: isLastColumn
|
: isLastColumn
|
||||||
? [styles.tableCellLast, { flex: column.flex || 1, color }]
|
? [styles.tableCellLast, { flex: column.flex }]
|
||||||
: [styles.tableCell, { flex: column.flex || 1, color }];
|
: [
|
||||||
|
styles.tableCell,
|
||||||
|
{
|
||||||
|
flex: column.flex,
|
||||||
|
color: cellData?.color || 'black',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
{displayContent !== undefined && displayContent !== null ? (
|
<Text>{column.key === 'no' ? label : cellData?.value || ''}</Text>
|
||||||
typeof displayContent === 'string' ||
|
|
||||||
typeof displayContent === 'number' ? (
|
|
||||||
<Text>{String(displayContent)}</Text>
|
|
||||||
) : (
|
|
||||||
displayContent
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Text>-</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { PdfColumn };
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import type { PdfColumn } from './types';
|
export interface PdfColumn {
|
||||||
|
key: string;
|
||||||
|
header: string;
|
||||||
|
flex: number;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tableRow: {
|
tableRow: {
|
||||||
@@ -43,37 +48,23 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTheadProps<TData = Record<string, unknown>> {
|
interface PdfTheadProps {
|
||||||
columns: PdfColumn<TData>[];
|
columns: PdfColumn[];
|
||||||
data?: TData[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfThead = <TData = Record<string, unknown>,>({
|
export const PdfThead = ({ columns }: PdfTheadProps) => {
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
}: PdfTheadProps<TData>) => {
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||||
{columns.map((column, index) => {
|
{columns.map((column, index) => {
|
||||||
|
const align = column.align || 'center';
|
||||||
const isLastColumn = index === columns.length - 1;
|
const isLastColumn = index === columns.length - 1;
|
||||||
|
|
||||||
// Get header content from column definition
|
|
||||||
let headerContent: ReactNode;
|
|
||||||
if (typeof column.header === 'function') {
|
|
||||||
headerContent = column.header(data || []);
|
|
||||||
} else {
|
|
||||||
headerContent = column.header || column.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine alignment - columns align right by default for numeric data
|
|
||||||
const align = column.align || 'left';
|
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
align === 'right'
|
align === 'right'
|
||||||
? [
|
? [
|
||||||
styles.tableCellHeaderRight,
|
styles.tableCellHeaderRight,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
textAlign: 'right' as const,
|
textAlign: 'right' as const,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
@@ -81,7 +72,7 @@ export const PdfThead = <TData = Record<string, unknown>,>({
|
|||||||
: [
|
: [
|
||||||
styles.tableCellHeader,
|
styles.tableCellHeader,
|
||||||
{
|
{
|
||||||
flex: column.flex || 1,
|
flex: column.flex,
|
||||||
textAlign: align as 'left' | 'center' | 'right',
|
textAlign: align as 'left' | 'center' | 'right',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
@@ -89,16 +80,10 @@ export const PdfThead = <TData = Record<string, unknown>,>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
{typeof headerContent === 'string' ? (
|
<Text>{column.header}</Text>
|
||||||
<Text>{headerContent}</Text>
|
|
||||||
) : (
|
|
||||||
headerContent
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { PdfColumn };
|
|
||||||
|
|||||||
@@ -2,4 +2,6 @@ export { PdfTable } from './PdfTable';
|
|||||||
export { PdfThead } from './PdfThead';
|
export { PdfThead } from './PdfThead';
|
||||||
export { PdfTbody } from './PdfTbody';
|
export { PdfTbody } from './PdfTbody';
|
||||||
export { PdfTfoot } from './PdfTfoot';
|
export { PdfTfoot } from './PdfTfoot';
|
||||||
export type { PdfColumn } from './types';
|
export type { PdfColumn } from './PdfThead';
|
||||||
|
export type { PdfTbodyCell } from './PdfTbody';
|
||||||
|
export type { PdfTfootCell } from './PdfTfoot';
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PdfColumn - Mirip dengan ColumnDef di TanStack Table
|
|
||||||
* Mengatur header (thead), body (tbody), dan footer (tfoot) dalam satu definisi
|
|
||||||
*/
|
|
||||||
export interface PdfColumn<TData = Record<string, unknown>> {
|
|
||||||
key: string;
|
|
||||||
flex?: number;
|
|
||||||
|
|
||||||
// Header configuration (thead)
|
|
||||||
header?: string | ((data: TData[]) => ReactNode);
|
|
||||||
|
|
||||||
// Body configuration (tbody)
|
|
||||||
align?: 'left' | 'center' | 'right';
|
|
||||||
cell?: (props: { row: TData; index: number }) => ReactNode | string | number;
|
|
||||||
|
|
||||||
// Footer configuration (tfoot)
|
|
||||||
footer?: string | number | ((data: TData[]) => ReactNode | string | number);
|
|
||||||
footerAlign?: 'left' | 'center' | 'right';
|
|
||||||
footerColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { PdfColumn as default };
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { Color } from '@/types/theme';
|
|
||||||
import { Text, StyleSheet } from '@react-pdf/renderer';
|
|
||||||
import type { Style } from '@react-pdf/types';
|
|
||||||
|
|
||||||
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
|
|
||||||
|
|
||||||
type TypographyVariant = Color | 'default';
|
|
||||||
|
|
||||||
type PdfTypographyProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
size?: TypographySize;
|
|
||||||
variant?: TypographyVariant;
|
|
||||||
color?: string;
|
|
||||||
style?: Style;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
h1: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 5,
|
|
||||||
},
|
|
||||||
h2: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
h3: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
h4: {
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 3,
|
|
||||||
},
|
|
||||||
p: {
|
|
||||||
fontSize: 10,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
fontSize: 8,
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: 9,
|
|
||||||
marginBottom: 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const variantColors: Record<TypographyVariant, string> = {
|
|
||||||
default: '#333333',
|
|
||||||
primary: '#1f74bf',
|
|
||||||
secondary: '#6B7280',
|
|
||||||
accent: '#8B5CF6',
|
|
||||||
neutral: '#6B7280',
|
|
||||||
info: '#3B82F6',
|
|
||||||
success: '#065F46',
|
|
||||||
warning: '#92400E',
|
|
||||||
error: '#DC2626',
|
|
||||||
none: '#333333',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PdfTypography = ({
|
|
||||||
children,
|
|
||||||
size = 'p',
|
|
||||||
variant = 'default',
|
|
||||||
color,
|
|
||||||
style,
|
|
||||||
}: PdfTypographyProps) => {
|
|
||||||
const sizeStyle = styles[size];
|
|
||||||
const textColor = color || variantColors[variant];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text style={[sizeStyle, { color: textColor }, ...(style ? [style] : [])]}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
export type StatusColor = {
|
|
||||||
bg: string;
|
|
||||||
text: string;
|
|
||||||
border: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Due status colors (for debt supplier reports)
|
|
||||||
export const dueStatusColors: Record<string, StatusColor> = {
|
|
||||||
'SUDAH JATUH TEMPO': {
|
|
||||||
bg: '#FEE2E2',
|
|
||||||
text: '#991B1B',
|
|
||||||
border: '#F87171',
|
|
||||||
}, // error/red
|
|
||||||
'BELUM JATUH TEMPO': {
|
|
||||||
bg: '#D1FAE5',
|
|
||||||
text: '#065F46',
|
|
||||||
border: '#34D399',
|
|
||||||
}, // success/green
|
|
||||||
'MENDEKATI JATUH TEMPO': {
|
|
||||||
bg: '#FEF3C7',
|
|
||||||
text: '#92400E',
|
|
||||||
border: '#FBBF24',
|
|
||||||
}, // warning/yellow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Payment status colors (for customer payment & debt supplier reports)
|
|
||||||
export const paymentStatusColors: Record<string, StatusColor> = {
|
|
||||||
'BELUM LUNAS': {
|
|
||||||
bg: '#FEF3C7',
|
|
||||||
text: '#92400E',
|
|
||||||
border: '#FBBF24',
|
|
||||||
}, // warning/yellow
|
|
||||||
LUNAS: {
|
|
||||||
bg: '#DBEAFE',
|
|
||||||
text: '#1E40AF',
|
|
||||||
border: '#60A5FA',
|
|
||||||
}, // primary/blue
|
|
||||||
'PEMBAYARAN SEBAGIAN': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
|
||||||
PEMBAYARAN: {
|
|
||||||
bg: '#D1FAE5',
|
|
||||||
text: '#065F46',
|
|
||||||
border: '#34D399',
|
|
||||||
}, // success/green
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fallback color for unknown statuses
|
|
||||||
export const fallbackStatusColor: StatusColor = {
|
|
||||||
bg: '#F3F4F6',
|
|
||||||
text: '#374151',
|
|
||||||
border: '#D1D5DB',
|
|
||||||
}; // neutral
|
|
||||||
|
|
||||||
export const getPDFBadgeStyle = (
|
|
||||||
statusText: string,
|
|
||||||
type: 'due' | 'payment' = 'payment'
|
|
||||||
): StatusColor => {
|
|
||||||
const normalizedStatus = statusText.toUpperCase().trim();
|
|
||||||
|
|
||||||
const colors =
|
|
||||||
type === 'due'
|
|
||||||
? dueStatusColors[normalizedStatus]
|
|
||||||
: paymentStatusColors[normalizedStatus];
|
|
||||||
|
|
||||||
return colors || fallbackStatusColor;
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import IconSkeleton from '@/components/helper/skeleton/IconSkeleton';
|
import IconSkeleton from '@/components/helper/skeleton/IconSkeleton';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
const DataStateSkeleton = ({
|
const DataStateSkeleton = ({
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -134,20 +134,14 @@ const DropFileInput: React.FC<DropFileInputProps> = ({
|
|||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn('w-full text-sm opacity-60', className?.bottomLabel)}
|
||||||
'w-full mt-1.5 text-xs opacity-60',
|
|
||||||
className?.bottomLabel
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{bottomLabel}
|
{bottomLabel}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{isError && (
|
{isError && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn('w-full text-sm text-error', className?.errorMessage)}
|
||||||
'w-full mt-1.5 text-xs text-error',
|
|
||||||
className?.errorMessage
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ const NumberInput = ({
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (newChangeEvent) {
|
if (newChangeEvent) {
|
||||||
newChangeEvent.target.value = parseFloat(
|
newChangeEvent.target.value = numberFormatValues.value;
|
||||||
numberFormatValues.value
|
|
||||||
) as unknown as string;
|
|
||||||
|
|
||||||
onChange?.(newChangeEvent);
|
onChange?.(newChangeEvent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ChangeEvent } from 'react';
|
|||||||
import {
|
import {
|
||||||
PatternFormat,
|
PatternFormat,
|
||||||
NumberFormatBase,
|
NumberFormatBase,
|
||||||
|
NumberFormatBaseProps,
|
||||||
OnValueChange,
|
OnValueChange,
|
||||||
} from 'react-number-format';
|
} from 'react-number-format';
|
||||||
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ export const RadioGroup = ({
|
|||||||
|
|
||||||
{/* Label bawah */}
|
{/* Label bawah */}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
<p className='text-sm opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pesan error */}
|
{/* Pesan error */}
|
||||||
{isError && errorMessage && (
|
{isError && errorMessage && (
|
||||||
<p className='mt-1.5 text-xs text-error'>{errorMessage}</p>
|
<p className='text-sm text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</RadioGroupContext.Provider>
|
</RadioGroupContext.Provider>
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import {
|
|||||||
} from '@/types/api/api-general';
|
} from '@/types/api/api-general';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
export interface OptionType<T = string | number> {
|
export interface OptionType {
|
||||||
value: T;
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
@@ -246,8 +246,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-base-100 border-base-content/10': !isDisabled,
|
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||||
'bg-base-200 border-base-content/10': isDisabled,
|
'bg-gray-50 border-base-content/10': isDisabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
},
|
},
|
||||||
className?.inputPrefix
|
className?.inputPrefix
|
||||||
@@ -278,28 +278,28 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn('w-full flex-1', className?.select)}
|
className={cn('w-full flex-1', className?.select)}
|
||||||
classNames={{
|
classNames={{
|
||||||
control: ({ isFocused, isDisabled }) =>
|
control: ({ isFocused, isDisabled }) =>
|
||||||
cn('w-full border transition-shadow', 'rounded-lg!', {
|
cn('w-full border bg-white transition-shadow', 'rounded-lg!', {
|
||||||
'bg-base-100!': !isDisabled && !readOnly,
|
'cursor-pointer!': !readOnly && !isDisabled,
|
||||||
'bg-base-200! text-gray-400 cursor-not-allowed':
|
'border-red-500! ring-2 ring-red-200': isError,
|
||||||
|
'border-indigo-500 ring-2 ring-indigo-200':
|
||||||
|
isFocused && !startAdornment,
|
||||||
|
'border-base-content/10!': !isError && !isFocused,
|
||||||
|
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
||||||
isDisabled && !readOnly,
|
isDisabled && !readOnly,
|
||||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||||
'cursor-pointer!': !readOnly && !isDisabled,
|
|
||||||
'border-error!': isError,
|
|
||||||
'ring-2 ring-error/20': isError,
|
|
||||||
'border-indigo-500 ring-2 ring-indigo-200':
|
|
||||||
isFocused && !startAdornment && !isError,
|
|
||||||
'border-base-content/10!': !isError && !isFocused,
|
|
||||||
'rounded-l-none!': inputPrefix && !startAdornment,
|
'rounded-l-none!': inputPrefix && !startAdornment,
|
||||||
'rounded-r-none!': inputSuffix && !startAdornment,
|
'rounded-r-none!': inputSuffix && !startAdornment,
|
||||||
}),
|
}),
|
||||||
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||||
placeholder: () =>
|
placeholder: () =>
|
||||||
cn('text-gray-400 text-sm leading-tight', {
|
cn({
|
||||||
'text-error!': isError,
|
'text-gray-400 text-sm leading-tight': !isError,
|
||||||
|
'text-red-300!': isError,
|
||||||
}),
|
}),
|
||||||
singleValue: () =>
|
singleValue: () =>
|
||||||
cn('m-0! text-gray-900 text-sm leading-tight', {
|
cn({
|
||||||
'text-error!': isError && !readOnly,
|
'm-0! text-gray-900 text-sm leading-tight': !isError,
|
||||||
|
'text-error!': isError,
|
||||||
'text-gray-900!': readOnly,
|
'text-gray-900!': readOnly,
|
||||||
}),
|
}),
|
||||||
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
||||||
@@ -370,8 +370,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-base-100 border-base-content/10': !isDisabled,
|
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||||
'bg-base-200 border-base-content/10': isDisabled,
|
'bg-gray-50 border-base-content/10': isDisabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
},
|
},
|
||||||
className?.inputSuffix
|
className?.inputSuffix
|
||||||
@@ -403,26 +403,31 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn('w-full', className?.select)}
|
className={cn('w-full', className?.select)}
|
||||||
classNames={{
|
classNames={{
|
||||||
control: ({ isFocused, isDisabled }) =>
|
control: ({ isFocused, isDisabled }) =>
|
||||||
cn('w-full border transition-shadow rounded-lg!', {
|
cn(
|
||||||
'bg-base-100!': !isDisabled && !readOnly,
|
'w-full border bg-white transition-shadow',
|
||||||
'bg-base-200! text-gray-400 cursor-not-allowed':
|
// Gunakan rounded-lg untuk semua kasus
|
||||||
isDisabled && !readOnly,
|
'rounded-lg!',
|
||||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
{
|
||||||
'cursor-pointer!': !readOnly && !isDisabled,
|
'cursor-pointer!': !readOnly && !isDisabled,
|
||||||
'border-error!': isError,
|
'border-red-500! ring-2 ring-red-200': isError,
|
||||||
'ring-2 ring-error/20': isError,
|
'border-indigo-500 ring-2 ring-indigo-200':
|
||||||
'border-indigo-500 ring-2 ring-indigo-200':
|
isFocused && !startAdornment,
|
||||||
isFocused && !startAdornment && !isError,
|
'border-base-content/10!': !isError && !isFocused,
|
||||||
'border-base-content/10!': !isError && !isFocused,
|
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
||||||
}),
|
isDisabled && !readOnly,
|
||||||
|
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||||
|
}
|
||||||
|
),
|
||||||
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||||
placeholder: () =>
|
placeholder: () =>
|
||||||
cn('text-gray-400 text-sm leading-tight', {
|
cn({
|
||||||
'text-error!': isError,
|
'text-gray-400 text-sm leading-tight': !isError,
|
||||||
|
'text-red-300!': isError,
|
||||||
}),
|
}),
|
||||||
singleValue: () =>
|
singleValue: () =>
|
||||||
cn('m-0! text-gray-900 text-sm leading-tight', {
|
cn({
|
||||||
'text-error!': isError && !readOnly,
|
'm-0! text-gray-900 text-sm leading-tight': !isError,
|
||||||
|
'text-error!': isError,
|
||||||
'text-gray-900!': readOnly,
|
'text-gray-900!': readOnly,
|
||||||
}),
|
}),
|
||||||
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
||||||
@@ -488,11 +493,9 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isError && (
|
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||||
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -523,7 +526,7 @@ const useSelect = <T,>(
|
|||||||
|
|
||||||
const qs = new URLSearchParams({
|
const qs = new URLSearchParams({
|
||||||
...(params ?? {}),
|
...(params ?? {}),
|
||||||
[searchKey ? searchKey : 'search']: inputValue ?? '',
|
[searchKey]: inputValue ?? '',
|
||||||
[pageKey]: String(pageIndex + 1),
|
[pageKey]: String(pageIndex + 1),
|
||||||
[limitKey]: String(limit),
|
[limitKey]: String(limit),
|
||||||
}).toString();
|
}).toString();
|
||||||
@@ -566,31 +569,23 @@ const useSelect = <T,>(
|
|||||||
setSize(size + 1);
|
setSize(size + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
|
||||||
|
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
|
||||||
|
|
||||||
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
|
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
|
||||||
|
|
||||||
const { formattedSuccessRawData, formattedErrorRawData } = useMemo(() => {
|
if (isResponseSuccess(pages?.[latestPagesIndex])) {
|
||||||
let successData: SuccessApiResponse<T[]> | undefined = undefined;
|
formattedSuccessRawData = {
|
||||||
let errorData: ErrorApiResponse | undefined = undefined;
|
...pages?.[latestPagesIndex],
|
||||||
|
data:
|
||||||
if (isResponseSuccess(pages?.[latestPagesIndex])) {
|
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
|
||||||
successData = {
|
[],
|
||||||
...pages![latestPagesIndex],
|
|
||||||
data:
|
|
||||||
pages?.flatMap((page) =>
|
|
||||||
isResponseSuccess(page) ? page.data : []
|
|
||||||
) ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isResponseError(pages?.[latestPagesIndex])) {
|
|
||||||
errorData = pages![latestPagesIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
formattedSuccessRawData: successData,
|
|
||||||
formattedErrorRawData: errorData,
|
|
||||||
};
|
};
|
||||||
}, [pages, latestPagesIndex]);
|
}
|
||||||
|
|
||||||
|
if (isResponseError(pages?.[latestPagesIndex])) {
|
||||||
|
formattedErrorRawData = pages?.[latestPagesIndex];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputValue,
|
inputValue,
|
||||||
|
|||||||
@@ -159,11 +159,9 @@ const TagInput: React.FC<TagInputProps> = ({
|
|||||||
|
|
||||||
{/* Bottom label or error message */}
|
{/* Bottom label or error message */}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
|
||||||
{isError && (
|
|
||||||
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
|
||||||
)}
|
)}
|
||||||
|
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ const TextInput = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-base-100 border-base-content/10': !disabled,
|
'bg-gray-100 border-base-content/10': !disabled,
|
||||||
'bg-base-200 border-base-content/10': disabled,
|
'bg-gray-50 border-base-content/10': disabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
},
|
},
|
||||||
@@ -118,7 +118,7 @@ const TextInput = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -126,8 +126,7 @@ const TextInput = ({
|
|||||||
'rounded-r-none!': inputSuffix,
|
'rounded-r-none!': inputSuffix,
|
||||||
'input-disabled': disabled,
|
'input-disabled': disabled,
|
||||||
'cursor-not-allowed': disabled,
|
'cursor-not-allowed': disabled,
|
||||||
'bg-base-100': !disabled,
|
'bg-gray-50': disabled,
|
||||||
'bg-base-200': disabled,
|
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
)}
|
||||||
@@ -168,8 +167,8 @@ const TextInput = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-base-100 border-base-content/10': !disabled,
|
'bg-gray-100 border-base-content/10': !disabled,
|
||||||
'bg-base-200 border-base-content/10': disabled,
|
'bg-gray-50 border-base-content/10': disabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
},
|
},
|
||||||
@@ -183,12 +182,10 @@ const TextInput = ({
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
'bg-base-100': !disabled,
|
|
||||||
'bg-base-200': disabled,
|
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
)}
|
||||||
@@ -204,14 +201,7 @@ const TextInput = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn('grow', className?.input)}
|
||||||
'grow bg-transparent outline-none',
|
|
||||||
{
|
|
||||||
'cursor-not-allowed': disabled,
|
|
||||||
'text-gray-500': disabled,
|
|
||||||
},
|
|
||||||
className?.input
|
|
||||||
)}
|
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
closeOnBackdrop={closeOnBackdrop}
|
closeOnBackdrop={closeOnBackdrop}
|
||||||
primaryButton={{
|
primaryButton={{
|
||||||
...primaryButton,
|
...primaryButton,
|
||||||
onClick: () => {
|
onClick: (e) => {
|
||||||
if (primaryButton && primaryButton?.onClick) {
|
if (primaryButton && primaryButton?.onClick) {
|
||||||
primaryButton?.onClick?.(notes);
|
primaryButton?.onClick?.(notes);
|
||||||
} else {
|
} else {
|
||||||
@@ -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) {
|
||||||
|
|||||||
+37
-34
@@ -5,23 +5,28 @@ import { useMemo, useState } from 'react';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import ClosingGeneralInformationTable from '@/components/pages/closing/table/ClosingGeneralInformationTable';
|
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
||||||
import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab';
|
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
||||||
import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab';
|
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
||||||
|
|
||||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
import {
|
||||||
import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab';
|
ClosingGeneralInformation,
|
||||||
import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab';
|
BaseClosingSales,
|
||||||
import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab';
|
ClosingHppExpedition,
|
||||||
import SalesClosingTab from '@/components/pages/closing/tab/SalesClosingTab';
|
} from '@/types/api/closing';
|
||||||
import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab';
|
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
||||||
|
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
||||||
|
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
||||||
|
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||||
|
import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable';
|
||||||
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
|
||||||
interface ClosingDetailProps {
|
interface ClosingDetailProps {
|
||||||
id: number;
|
id: number;
|
||||||
initialValue?: ClosingGeneralInformation;
|
initialValue?: ClosingGeneralInformation;
|
||||||
|
salesData?: BaseClosingSales;
|
||||||
|
hppExpeditionData?: ClosingHppExpedition;
|
||||||
projectData?: ProjectFlock;
|
projectData?: ProjectFlock;
|
||||||
kandangData?: ProjectFlockKandang;
|
kandangData?: ProjectFlockKandang;
|
||||||
}
|
}
|
||||||
@@ -29,24 +34,25 @@ interface ClosingDetailProps {
|
|||||||
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||||
id,
|
id,
|
||||||
initialValue,
|
initialValue,
|
||||||
|
salesData,
|
||||||
|
hppExpeditionData,
|
||||||
projectData,
|
projectData,
|
||||||
kandangData,
|
kandangData,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTabId, setActiveTabId] = useState<string>('sapronak');
|
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
||||||
const tabActions = useTabActionsStore((state) => state.tabActions);
|
|
||||||
|
|
||||||
const closingDetailTabs = useMemo(() => {
|
const closingDetailTabs = useMemo(() => {
|
||||||
const validTabs = [
|
const validTabs = [
|
||||||
{
|
{
|
||||||
id: 'sapronak',
|
id: 'sapronak',
|
||||||
label: 'Sapronak',
|
label: 'Sapronak',
|
||||||
content: <SapronakClosingTab projectFlockId={id} />,
|
content: <ClosingSapronakTabContent projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'perhitunganSapronak',
|
id: 'perhitunganSapronak',
|
||||||
label: 'Perhitungan Sapronak',
|
label: 'Perhitungan Sapronak',
|
||||||
content: (
|
content: (
|
||||||
<SapronakCalculationClosingTab
|
<ClosingSapronakCalculationTabContent
|
||||||
closingGeneralInformation={initialValue}
|
closingGeneralInformation={initialValue}
|
||||||
projectFlockId={id}
|
projectFlockId={id}
|
||||||
/>
|
/>
|
||||||
@@ -55,13 +61,13 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
{
|
{
|
||||||
id: 'penjualan',
|
id: 'penjualan',
|
||||||
label: 'Penjualan',
|
label: 'Penjualan',
|
||||||
content: <SalesClosingTab projectFlockId={id} />,
|
content: <SalesReportTable initialValues={salesData} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'overhead',
|
id: 'overhead',
|
||||||
label: 'Overhead',
|
label: 'Overhead',
|
||||||
content: (
|
content: (
|
||||||
<OverheadClosingTab
|
<ClosingOverheadTabContent
|
||||||
projectFlockId={id}
|
projectFlockId={id}
|
||||||
generalInformation={initialValue}
|
generalInformation={initialValue}
|
||||||
kandangData={kandangData}
|
kandangData={kandangData}
|
||||||
@@ -71,26 +77,26 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
{
|
{
|
||||||
id: 'hppEkspedisi',
|
id: 'hppEkspedisi',
|
||||||
label: 'HPP Ekspedisi',
|
label: 'HPP Ekspedisi',
|
||||||
content: <HppExpeditionClosingTab projectFlockId={id} />,
|
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dataProduksi',
|
id: 'dataProduksi',
|
||||||
label: 'Data Produksi',
|
label: 'Data Produksi',
|
||||||
content: <ProductionDataClosingTab projectFlockId={id} />,
|
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'keuangan',
|
id: 'keuangan',
|
||||||
label: 'Keuangan',
|
label: 'Keuangan',
|
||||||
content: <FinanceClosingTab projectFlockId={id} />,
|
content: <ClosingFinanceTabContent projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return validTabs;
|
return validTabs;
|
||||||
}, [initialValue, kandangData, id]);
|
}, [initialValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full'>
|
<section className='w-full max-w-7xl pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href={
|
href={
|
||||||
@@ -112,24 +118,21 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
kandangData={kandangData}
|
kandangData={kandangData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ClosingKandangList
|
{!kandangData && (
|
||||||
initialValue={initialValue}
|
<ClosingKandangList
|
||||||
projectData={projectData}
|
initialValue={initialValue}
|
||||||
selectedKandangId={kandangData?.id}
|
projectData={projectData}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTab}
|
||||||
onTabChange={setActiveTabId}
|
onTabChange={setActiveTab}
|
||||||
tabs={closingDetailTabs}
|
tabs={closingDetailTabs}
|
||||||
variant='boxed'
|
variant='lifted'
|
||||||
className={{
|
className={{
|
||||||
tabHeaderWrapper:
|
wrapper: 'w-full mt-4',
|
||||||
'relative justify-between items-center py-3 before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10',
|
|
||||||
tab: 'w-fit',
|
|
||||||
content: 'p-0 m-0',
|
|
||||||
}}
|
}}
|
||||||
sideContent={tabActions[activeTabId] || null}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable';
|
||||||
|
|
||||||
|
const ClosingFinanceTabContent = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: {
|
||||||
|
projectFlockId: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{projectFlockId && (
|
||||||
|
<ClosingFinanceTable projectFlockId={projectFlockId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingFinanceTabContent;
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
import Card from '@/components/Card';
|
||||||
|
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { HppItem, ProfitLossItem } from '@/types/api/closing';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const ClosingFinanceTable = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: {
|
||||||
|
projectFlockId: number;
|
||||||
|
}) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
|
const { data: finance, isLoading } = useSWR(
|
||||||
|
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||||
|
() =>
|
||||||
|
ClosingApi.getFinance(
|
||||||
|
projectFlockId,
|
||||||
|
kandangId ? Number(kandangId) : undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hppTableData: HppItem[] = useMemo(() => {
|
||||||
|
if (isResponseSuccess(finance)) {
|
||||||
|
const customItems = {
|
||||||
|
label: 'HPP dan Pengeluaran',
|
||||||
|
code: 'custom_row',
|
||||||
|
} as HppItem;
|
||||||
|
const purchases = finance.data.hpp.items.filter(
|
||||||
|
(item) => item.category === 'purchase'
|
||||||
|
);
|
||||||
|
const totalBudgeting = {
|
||||||
|
label: 'HPP dan Bahan Baku',
|
||||||
|
code: 'custom_row',
|
||||||
|
} as HppItem;
|
||||||
|
const overheads = finance.data.hpp.items.filter(
|
||||||
|
(item) => item.category === 'overhead'
|
||||||
|
);
|
||||||
|
return [customItems, ...purchases, totalBudgeting, ...overheads];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [finance]);
|
||||||
|
|
||||||
|
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
|
||||||
|
if (isResponseSuccess(finance)) {
|
||||||
|
const incomes = finance.data.profit_loss.items.filter(
|
||||||
|
(item) => item.type === 'income'
|
||||||
|
);
|
||||||
|
const purchases = finance.data.profit_loss.items.filter(
|
||||||
|
(item) => item.type === 'purchase'
|
||||||
|
);
|
||||||
|
const overheads = finance.data.profit_loss.items.filter(
|
||||||
|
(item) => item.type === 'overhead'
|
||||||
|
);
|
||||||
|
const grossProfit = {
|
||||||
|
label: 'LABA RUGI BRUTO',
|
||||||
|
code: 'custom_row',
|
||||||
|
type: 'gross_profit',
|
||||||
|
rp_per_bird:
|
||||||
|
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
|
||||||
|
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
|
||||||
|
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
|
||||||
|
} as ProfitLossItem;
|
||||||
|
const subtotal = {
|
||||||
|
label: 'Subtotal',
|
||||||
|
code: 'custom_row',
|
||||||
|
type: 'subtotal',
|
||||||
|
rp_per_bird:
|
||||||
|
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
|
||||||
|
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
|
||||||
|
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
|
||||||
|
} as ProfitLossItem;
|
||||||
|
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [finance]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-2 gap-6'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<div>Laba Rugi Brutto</div>
|
||||||
|
<div className='text-lg font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.summary.gross_profit.amount
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<div>Laba Rugi Netto</div>
|
||||||
|
<div className='text-lg font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.summary.net_profit.amount
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title='HPP Purchases'
|
||||||
|
variant='bordered'
|
||||||
|
collapsible
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mt-6 p-0 mb-0'>
|
||||||
|
<Table<HppItem>
|
||||||
|
data={hppTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'No.',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item, index) => {
|
||||||
|
if (item.code === 'custom_row') return '-';
|
||||||
|
const dataRowsBefore = hppTableData
|
||||||
|
.slice(0, index)
|
||||||
|
.filter((row) => row.code !== 'custom_row').length;
|
||||||
|
return dataRowsBefore + 1;
|
||||||
|
},
|
||||||
|
footer: (props) => {
|
||||||
|
return 'HPP';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jenis',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatTitleCase(item.label || '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Budgeting',
|
||||||
|
enableSorting: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
id: 'budgeting_rp_per_bird',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.budgeting?.rp_per_bird || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp.summary?.budgeting
|
||||||
|
?.rp_per_bird || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Kg',
|
||||||
|
id: 'budgeting_rp_per_kg',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.budgeting?.rp_per_kg || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
id: 'budgeting_amount',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.budgeting?.amount || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'budgeting_amount' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp.summary?.budgeting?.amount || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Realization',
|
||||||
|
enableSorting: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
id: 'realization_rp_per_bird',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.realization?.rp_per_bird || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'realization_rp_per_bird' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp.summary?.realization
|
||||||
|
?.rp_per_bird || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Kg',
|
||||||
|
id: 'realization_rp_per_kg',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.realization?.rp_per_kg || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'realization_rp_per_kg' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp.summary?.realization
|
||||||
|
?.rp_per_kg || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
id: 'realization_amount',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) =>
|
||||||
|
formatCurrency(item.realization?.amount || 0),
|
||||||
|
footer: (props) => {
|
||||||
|
return props.column.id === 'realization_amount' &&
|
||||||
|
isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.hpp.summary?.realization?.amount || 0
|
||||||
|
)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderCustomRow={(row) => {
|
||||||
|
const rowData = row.original;
|
||||||
|
if (rowData.code === 'custom_row') {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
></td>
|
||||||
|
<td
|
||||||
|
colSpan={7}
|
||||||
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
|
>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatTitleCase(rowData.label ?? '-')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
renderFooter={isResponseSuccess(finance)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title='Profit/Loss'
|
||||||
|
variant='bordered'
|
||||||
|
collapsible
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mt-6 p-0 mb-0'>
|
||||||
|
<Table<ProfitLossItem>
|
||||||
|
data={profitLossTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Jenis',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => item.label,
|
||||||
|
cell: (item) => (
|
||||||
|
<div className=''>
|
||||||
|
{formatTitleCase(item.row.original.label || '-')}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.summary.net_profit
|
||||||
|
.rp_per_bird || 0
|
||||||
|
)
|
||||||
|
: formatCurrency(0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Rp/Kg',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.summary.net_profit
|
||||||
|
.rp_per_kg || 0
|
||||||
|
)
|
||||||
|
: formatCurrency(0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
enableSorting: false,
|
||||||
|
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-bold'>
|
||||||
|
{isResponseSuccess(finance)
|
||||||
|
? formatCurrency(
|
||||||
|
finance.data.profit_loss.summary.net_profit
|
||||||
|
.amount || 0
|
||||||
|
)
|
||||||
|
: formatCurrency(0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderCustomRow={(row) => {
|
||||||
|
const rowData = row.original;
|
||||||
|
if (rowData.code === 'custom_row') {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||||
|
>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold ps-6 uppercase'>
|
||||||
|
{formatTitleCase(rowData.label ?? '-')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.amount ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
className={{
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
renderFooter={isResponseSuccess(finance)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingFinanceTable;
|
||||||
+54
-86
@@ -1,20 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Badge from '@/components/Badge';
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
import { cn, formatNumber } 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 { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
|
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
|
||||||
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
|
||||||
|
|
||||||
interface ClosingIncomingSapronaksSummaryTableProps {
|
interface ClosingIncomingSapronaksSummaryTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
@@ -55,60 +55,20 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
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 incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
|
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: '#',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: (props) => {
|
|
||||||
const categories = props.row.original.category
|
|
||||||
.split(' ')
|
|
||||||
.filter((cat) => cat.trim());
|
|
||||||
const maxBadges = 4;
|
|
||||||
const visibleCategories = categories.slice(0, maxBadges);
|
|
||||||
const remainingCount = categories.length - maxBadges;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
|
|
||||||
{visibleCategories.map((category, index) => (
|
|
||||||
<Badge
|
|
||||||
key={index}
|
|
||||||
variant='soft'
|
|
||||||
className={{
|
|
||||||
badge: cn(
|
|
||||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
title={category}
|
|
||||||
>
|
|
||||||
{category.length > 12
|
|
||||||
? `${category.slice(0, 12)}...`
|
|
||||||
: category}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{remainingCount > 0 && (
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
className={{
|
|
||||||
badge: cn(
|
|
||||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
title={categories.join(' ')}
|
|
||||||
>
|
|
||||||
+{remainingCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'total_qty',
|
accessorKey: 'total_qty',
|
||||||
@@ -118,6 +78,10 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
@@ -129,35 +93,44 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(incomingSapronakSummaries)
|
||||||
|
? incomingSapronakSummaries.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [incomingSapronakSummaries, isResponseSuccess]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<Card
|
||||||
<Card
|
className={{
|
||||||
className={{
|
wrapper: 'w-full',
|
||||||
wrapper: 'w-full rounded-lg',
|
body: 'p-4 shadow',
|
||||||
body: 'p-0',
|
}}
|
||||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
>
|
||||||
collapsible: 'rounded-lg',
|
<Collapse
|
||||||
}}
|
open={open}
|
||||||
variant='bordered'
|
onOpenChange={setOpen}
|
||||||
title='Ringkasan Sapronak Masuk'
|
title={
|
||||||
collapsible
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
defaultCollapsed={false}
|
<div className='card-title'>Ringkasan Sapronak Masuk</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
>
|
>
|
||||||
{isLoadingIncomingSapronakSummaries ? (
|
<div className='w-full p-0'>
|
||||||
<SapronakClosingSkeleton
|
|
||||||
type='incoming'
|
|
||||||
columns={incomingSapronaksColumns}
|
|
||||||
/>
|
|
||||||
) : isResponseSuccess(incomingSapronakSummaries) &&
|
|
||||||
incomingSapronakSummaries.data.length === 0 ? (
|
|
||||||
<SapronakClosingSkeleton
|
|
||||||
type='incoming'
|
|
||||||
columns={incomingSapronaksColumns}
|
|
||||||
iconName='heroicons:chart-bar'
|
|
||||||
title='Ringkasan Sapronak Masuk Tidak Ditemukan'
|
|
||||||
subtitle='Tidak ada ringkasan sapronak masuk untuk periode ini.'
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Table<ClosingIncomingSapronakSummary>
|
<Table<ClosingIncomingSapronakSummary>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(incomingSapronakSummaries)
|
isResponseSuccess(incomingSapronakSummaries)
|
||||||
@@ -185,21 +158,16 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-5!',
|
containerClassName: cn({
|
||||||
tableWrapperClassName:
|
'w-full mb-20':
|
||||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
isResponseSuccess(incomingSapronakSummaries) &&
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
incomingSapronakSummaries?.data?.length === 0,
|
||||||
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-b border-gray-200',
|
|
||||||
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</Card>
|
</Collapse>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
+58
-107
@@ -9,14 +9,13 @@ 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 Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Badge from '@/components/Badge';
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } 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 { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { ClosingIncomingSapronak } from '@/types/api/closing';
|
import { ClosingIncomingSapronak } from '@/types/api/closing';
|
||||||
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
|
||||||
|
|
||||||
interface ClosingIncomingSapronaksTableProps {
|
interface ClosingIncomingSapronaksTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
@@ -52,12 +51,14 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
ClosingApi.getAllIncomingSapronakFetcher
|
ClosingApi.getAllIncomingSapronakFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
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 incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
|
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: '#',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -80,48 +81,6 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
{
|
{
|
||||||
accessorKey: 'product_category',
|
accessorKey: 'product_category',
|
||||||
header: 'Kategori Produk',
|
header: 'Kategori Produk',
|
||||||
cell: (props) => {
|
|
||||||
const categories = props.row.original.product_category
|
|
||||||
.split(' ')
|
|
||||||
.filter((cat) => cat.trim());
|
|
||||||
const maxBadges = 4;
|
|
||||||
const visibleCategories = categories.slice(0, maxBadges);
|
|
||||||
const remainingCount = categories.length - maxBadges;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
|
|
||||||
{visibleCategories.map((category, index) => (
|
|
||||||
<Badge
|
|
||||||
key={index}
|
|
||||||
variant='soft'
|
|
||||||
className={{
|
|
||||||
badge: cn(
|
|
||||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
title={category}
|
|
||||||
>
|
|
||||||
{category.length > 12
|
|
||||||
? `${category.slice(0, 12)}...`
|
|
||||||
: category}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{remainingCount > 0 && (
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
className={{
|
|
||||||
badge: cn(
|
|
||||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
title={categories.join(' ')}
|
|
||||||
>
|
|
||||||
+{remainingCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'source_warehouse',
|
accessorKey: 'source_warehouse',
|
||||||
@@ -158,59 +117,56 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(incomingSapronaks)
|
||||||
|
? incomingSapronaks.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [incomingSapronaks, isResponseSuccess]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full pt-3'>
|
<Card
|
||||||
<Card
|
className={{
|
||||||
className={{
|
wrapper: 'w-full',
|
||||||
wrapper: 'w-full rounded-lg',
|
body: 'p-4 shadow',
|
||||||
body: 'p-0',
|
}}
|
||||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
>
|
||||||
collapsible: 'rounded-lg',
|
<Collapse
|
||||||
}}
|
open={open}
|
||||||
variant='bordered'
|
onOpenChange={setOpen}
|
||||||
title='Sapronak Masuk'
|
title={
|
||||||
collapsible
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
defaultCollapsed={false}
|
<div className='card-title'>Sapronak Masuk</div>
|
||||||
>
|
|
||||||
<div className='flex flex-col gap-2 my-4'>
|
<Icon
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
<DebouncedTextInput
|
width={24}
|
||||||
name='search'
|
height={24}
|
||||||
placeholder='Cari Sapronak Masuk'
|
className={cn('text-primary transition-transform', {
|
||||||
value={tableFilterState.search}
|
'-rotate-180': open,
|
||||||
onChange={searchChangeHandler}
|
})}
|
||||||
startAdornment={
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:magnifying-glass'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
|
||||||
inputWrapper: 'rounded-xl! shadow-button-soft',
|
|
||||||
input:
|
|
||||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<div className='w-full p-0'>
|
||||||
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
|
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Sapronak Masuk'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLoadingIncomingSapronaks ? (
|
|
||||||
<SapronakClosingSkeleton
|
|
||||||
type='incoming'
|
|
||||||
columns={incomingSapronaksColumns}
|
|
||||||
/>
|
|
||||||
) : isResponseSuccess(incomingSapronaks) &&
|
|
||||||
incomingSapronaks.data.length === 0 ? (
|
|
||||||
<SapronakClosingSkeleton
|
|
||||||
type='incoming'
|
|
||||||
columns={incomingSapronaksColumns}
|
|
||||||
iconName='heroicons:chart-bar'
|
|
||||||
title='Data Sapronak Masuk Tidak Ditemukan'
|
|
||||||
subtitle='Tidak ada data sapronak masuk untuk periode ini.'
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Table<ClosingIncomingSapronak>
|
<Table<ClosingIncomingSapronak>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(incomingSapronaks)
|
isResponseSuccess(incomingSapronaks)
|
||||||
@@ -238,21 +194,16 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-5!',
|
containerClassName: cn({
|
||||||
tableWrapperClassName:
|
'w-full mb-20':
|
||||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
isResponseSuccess(incomingSapronaks) &&
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
incomingSapronaks?.data?.length === 0,
|
||||||
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-b border-gray-200',
|
|
||||||
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</Card>
|
</Collapse>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5,28 +5,23 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
|
|||||||
const ClosingKandangList = ({
|
const ClosingKandangList = ({
|
||||||
initialValue,
|
initialValue,
|
||||||
projectData,
|
projectData,
|
||||||
selectedKandangId,
|
|
||||||
}: {
|
}: {
|
||||||
initialValue?: ClosingGeneralInformation;
|
initialValue?: ClosingGeneralInformation;
|
||||||
projectData?: ProjectFlock;
|
projectData?: ProjectFlock;
|
||||||
selectedKandangId?: number;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
|
<div className='w-full my-4 @container'>
|
||||||
<div className='flex flex-col @sm:flex-row gap-4'>
|
<div className='flex flex-col @sm:flex-row gap-4'>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
<h1 className='font-bold mb-3'>Kandang</h1>
|
<h1 className='font-bold my-4'>Kandang</h1>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2 mb-4'>
|
||||||
{projectData?.kandangs?.map((kandang) => (
|
{projectData?.kandangs?.map((kandang) => (
|
||||||
<Button
|
<Button
|
||||||
key={kandang.id}
|
key={kandang.id}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
|
|
||||||
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
||||||
disabled={
|
className='min-w-32'
|
||||||
selectedKandangId === kandang.project_flock_kandang_id
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{kandang.name}
|
{kandang.name}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+54
-86
@@ -1,20 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Badge from '@/components/Badge';
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
import { cn, formatNumber } 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 { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
|
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
|
||||||
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
|
||||||
|
|
||||||
interface ClosingOutgoingSapronaksSummaryTableProps {
|
interface ClosingOutgoingSapronaksSummaryTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
@@ -55,60 +55,20 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
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 outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
|
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
header: 'No',
|
header: '#',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: (props) => {
|
|
||||||
const categories = props.row.original.category
|
|
||||||
.split(' ')
|
|
||||||
.filter((cat) => cat.trim());
|
|
||||||
const maxBadges = 4;
|
|
||||||
const visibleCategories = categories.slice(0, maxBadges);
|
|
||||||
const remainingCount = categories.length - maxBadges;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
|
|
||||||
{visibleCategories.map((category, index) => (
|
|
||||||
<Badge
|
|
||||||
key={index}
|
|
||||||
variant='soft'
|
|
||||||
className={{
|
|
||||||
badge: cn(
|
|
||||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
title={category}
|
|
||||||
>
|
|
||||||
{category.length > 12
|
|
||||||
? `${category.slice(0, 12)}...`
|
|
||||||
: category}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{remainingCount > 0 && (
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
className={{
|
|
||||||
badge: cn(
|
|
||||||
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
title={categories.join(' ')}
|
|
||||||
>
|
|
||||||
+{remainingCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'total_qty',
|
accessorKey: 'total_qty',
|
||||||
@@ -118,6 +78,10 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
@@ -129,35 +93,44 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(outgoingSapronakSummaries)
|
||||||
|
? outgoingSapronakSummaries.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [outgoingSapronakSummaries, isResponseSuccess]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<Card
|
||||||
<Card
|
className={{
|
||||||
className={{
|
wrapper: 'w-full',
|
||||||
wrapper: 'w-full rounded-lg border-none',
|
body: 'p-4 shadow',
|
||||||
body: 'p-0',
|
}}
|
||||||
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
>
|
||||||
collapsible: 'rounded-lg',
|
<Collapse
|
||||||
}}
|
open={open}
|
||||||
variant='bordered'
|
onOpenChange={setOpen}
|
||||||
title='Ringkasan Sapronak Keluar'
|
title={
|
||||||
collapsible
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
defaultCollapsed={false}
|
<div className='card-title'>Ringkasan Sapronak Keluar</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
>
|
>
|
||||||
{isLoadingOutgoingSapronakSummaries ? (
|
<div className='w-full p-0'>
|
||||||
<SapronakClosingSkeleton
|
|
||||||
type='outgoing'
|
|
||||||
columns={outgoingSapronaksColumns}
|
|
||||||
/>
|
|
||||||
) : isResponseSuccess(outgoingSapronakSummaries) &&
|
|
||||||
outgoingSapronakSummaries.data.length === 0 ? (
|
|
||||||
<SapronakClosingSkeleton
|
|
||||||
type='outgoing'
|
|
||||||
columns={outgoingSapronaksColumns}
|
|
||||||
iconName='heroicons:chart-bar'
|
|
||||||
title='Ringkasan Sapronak Keluar Tidak Ditemukan'
|
|
||||||
subtitle='Tidak ada ringkasan sapronak keluar untuk periode ini.'
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Table<ClosingOutgoingSapronakSummary>
|
<Table<ClosingOutgoingSapronakSummary>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(outgoingSapronakSummaries)
|
isResponseSuccess(outgoingSapronakSummaries)
|
||||||
@@ -185,21 +158,16 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full mb-5!',
|
containerClassName: cn({
|
||||||
tableWrapperClassName:
|
'w-full mb-20':
|
||||||
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
isResponseSuccess(outgoingSapronakSummaries) &&
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
outgoingSapronakSummaries?.data?.length === 0,
|
||||||
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-b border-gray-200',
|
|
||||||
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</Card>
|
</Collapse>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { ClosingOutgoingSapronak } from '@/types/api/closing';
|
||||||
|
|
||||||
|
interface ClosingOutgoingSapronaksTableProps {
|
||||||
|
projectFlockId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingOutgoingSapronaksTable = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: ClosingOutgoingSapronaksTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
||||||
|
useSWR(
|
||||||
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
|
ClosingApi.getAllOutgoingSapronakFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: 'Tanggal',
|
||||||
|
cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'reference_number',
|
||||||
|
header: 'No. Referensi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transaction_type',
|
||||||
|
header: 'Jenis Transaksi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'product_name',
|
||||||
|
header: 'Produk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'product_category',
|
||||||
|
header: 'Kategori Produk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'source_warehouse',
|
||||||
|
header: 'Gudang Asal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'destination_warehouse',
|
||||||
|
header: 'Gudang Tujuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'quantity',
|
||||||
|
header: 'Kuantitas',
|
||||||
|
cell: (props) =>
|
||||||
|
`${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'notes',
|
||||||
|
header: 'Keterangan',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// track sorting
|
||||||
|
useEffect(() => {
|
||||||
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
|
if (!isNameSorted) {
|
||||||
|
updateFilter('nameSort', '');
|
||||||
|
} else {
|
||||||
|
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [outgoingSapronaks, isResponseSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>Sapronak Keluar</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<div className='w-full p-0'>
|
||||||
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
|
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Sapronak Keluar'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<ClosingOutgoingSapronak>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={outgoingSapronaksColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(outgoingSapronaks)
|
||||||
|
? outgoingSapronaks?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingOutgoingSapronaks}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(outgoingSapronaks) &&
|
||||||
|
outgoingSapronaks?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingOutgoingSapronaksTable;
|
||||||
+6
-6
@@ -1,22 +1,22 @@
|
|||||||
import OverheadClosingTable from '@/components/pages/closing/table/OverheadClosingTable';
|
import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable';
|
||||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
|
||||||
interface OverheadClosingTabProps {
|
interface ClosingOverheadTabContentProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
generalInformation?: ClosingGeneralInformation;
|
generalInformation?: ClosingGeneralInformation;
|
||||||
kandangData?: ProjectFlockKandang;
|
kandangData?: ProjectFlockKandang;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OverheadClosingTab = ({
|
const ClosingOverheadTabContent = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
generalInformation,
|
generalInformation,
|
||||||
kandangData,
|
kandangData,
|
||||||
}: OverheadClosingTabProps) => {
|
}: ClosingOverheadTabContentProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
{projectFlockId && (
|
{projectFlockId && (
|
||||||
<OverheadClosingTable
|
<ClosingOverheadTable
|
||||||
projectFlockId={projectFlockId}
|
projectFlockId={projectFlockId}
|
||||||
generalInformation={generalInformation}
|
generalInformation={generalInformation}
|
||||||
kandangData={kandangData}
|
kandangData={kandangData}
|
||||||
@@ -26,4 +26,4 @@ const OverheadClosingTab = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OverheadClosingTab;
|
export default ClosingOverheadTabContent;
|
||||||
+38
-81
@@ -1,5 +1,5 @@
|
|||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Table from '@/components/Table';
|
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
@@ -14,19 +14,18 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton';
|
|
||||||
|
|
||||||
interface OverheadClosingTableProps {
|
interface ClosingOverheadTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
generalInformation?: ClosingGeneralInformation;
|
generalInformation?: ClosingGeneralInformation;
|
||||||
kandangData?: ProjectFlockKandang;
|
kandangData?: ProjectFlockKandang;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OverheadClosingTable = ({
|
const ClosingOverheadTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
generalInformation,
|
generalInformation,
|
||||||
kandangData,
|
kandangData,
|
||||||
}: OverheadClosingTableProps) => {
|
}: ClosingOverheadTableProps) => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const kandangId = searchParams.get('kandangId');
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ const OverheadClosingTable = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: overheadKandang } = useSWR(
|
const { data: overheadKandang, isLoading: isLoadingOverheadKandang } = useSWR(
|
||||||
kandangId
|
kandangId
|
||||||
? `${ClosingApi.basePath}/${projectFlockId}/${kandangId}/overhead`
|
? `${ClosingApi.basePath}/${projectFlockId}/${kandangId}/overhead`
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -209,84 +208,42 @@ const OverheadClosingTable = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full pt-3'>
|
<>
|
||||||
<Card
|
<Card
|
||||||
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',
|
|
||||||
collapsible: 'rounded-lg',
|
|
||||||
}}
|
|
||||||
variant='bordered'
|
|
||||||
title='Pengeluaran Overhead'
|
title='Pengeluaran Overhead'
|
||||||
collapsible
|
collapsible
|
||||||
defaultCollapsed={false}
|
defaultCollapsed={false}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isLoadingOverhead ? (
|
<Table<Overhead>
|
||||||
<OverheadClosingSkeleton columns={columns} />
|
data={
|
||||||
) : !isResponseSuccess(overhead) ? (
|
kandangId
|
||||||
<OverheadClosingSkeleton
|
? isResponseSuccess(overheadKandang)
|
||||||
columns={columns}
|
? (overheadKandang.data?.overheads ?? [])
|
||||||
iconName='heroicons:chart-bar'
|
: []
|
||||||
title='Data Overhead Tidak Ditemukan'
|
: isResponseSuccess(overhead)
|
||||||
subtitle='Tidak ada data overhead untuk periode ini.'
|
? (overhead.data?.overheads ?? [])
|
||||||
/>
|
: []
|
||||||
) : kandangId && !isResponseSuccess(overheadKandang) ? (
|
}
|
||||||
<OverheadClosingSkeleton
|
columns={columns}
|
||||||
columns={columns}
|
className={{
|
||||||
iconName='heroicons:chart-bar'
|
containerClassName: 'my-4',
|
||||||
title='Data Overhead Tidak Ditemukan'
|
headerColumnClassName: cn(
|
||||||
subtitle='Tidak ada data overhead untuk periode ini.'
|
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
||||||
/>
|
'whitespace-nowrap'
|
||||||
) : (!kandangId && overhead.data?.overheads.length === 0) ||
|
),
|
||||||
(kandangId &&
|
}}
|
||||||
isResponseSuccess(overheadKandang) &&
|
isLoading={isLoadingOverhead}
|
||||||
overheadKandang.data?.overheads.length === 0) ? (
|
renderFooter={
|
||||||
<OverheadClosingSkeleton
|
isResponseSuccess(overhead)
|
||||||
columns={columns}
|
? overhead.data?.overheads.length > 0
|
||||||
iconName='heroicons:chart-bar'
|
: false
|
||||||
/>
|
}
|
||||||
) : (
|
/>
|
||||||
<Table<Overhead>
|
{kandangId && (
|
||||||
data={
|
|
||||||
kandangId
|
|
||||||
? isResponseSuccess(overheadKandang)
|
|
||||||
? (overheadKandang.data?.overheads ?? [])
|
|
||||||
: []
|
|
||||||
: isResponseSuccess(overhead)
|
|
||||||
? (overhead.data?.overheads ?? [])
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
columns={columns}
|
|
||||||
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: cn(
|
|
||||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
|
||||||
'whitespace-nowrap'
|
|
||||||
),
|
|
||||||
bodyRowClassName:
|
|
||||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
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',
|
|
||||||
}}
|
|
||||||
isLoading={isLoadingOverhead}
|
|
||||||
renderFooter={
|
|
||||||
isResponseSuccess(overhead)
|
|
||||||
? overhead.data?.overheads.length > 0
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && (
|
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full',
|
||||||
@@ -341,8 +298,8 @@ const OverheadClosingTable = ({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OverheadClosingTable;
|
export default ClosingOverheadTable;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user