mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
Merge branch 'development' into 'production'
Development See merge request mbugroup/lti-web-client!441
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { SystemConfigContent } from '@/figma-make/components/pages/master-data/system-config/SystemConfigContent';
|
||||||
|
|
||||||
|
const SystemConfigPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full'>
|
||||||
|
<SystemConfigContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemConfigPage;
|
||||||
@@ -265,7 +265,11 @@ const FinanceTable = () => {
|
|||||||
updateFilter('endDate', values.end_date);
|
updateFilter('endDate', values.end_date);
|
||||||
// Save display names for restoration on modal reopen
|
// Save display names for restoration on modal reopen
|
||||||
const toNames = (val: OptionType | OptionType[] | null) =>
|
const toNames = (val: OptionType | OptionType[] | null) =>
|
||||||
val ? (Array.isArray(val) ? val : [val]).map((o) => String(o.label)).join(',') : '';
|
val
|
||||||
|
? (Array.isArray(val) ? val : [val])
|
||||||
|
.map((o) => String(o.label))
|
||||||
|
.join(',')
|
||||||
|
: '';
|
||||||
updateFilter('bankNames', toNames(selectedBank));
|
updateFilter('bankNames', toNames(selectedBank));
|
||||||
updateFilter('customerNames', toNames(selectedCustomerId));
|
updateFilter('customerNames', toNames(selectedCustomerId));
|
||||||
updateFilter('supplierNames', toNames(selectedSupplierId));
|
updateFilter('supplierNames', toNames(selectedSupplierId));
|
||||||
@@ -516,8 +520,9 @@ const FinanceTable = () => {
|
|||||||
|
|
||||||
// Restore sort by
|
// Restore sort by
|
||||||
const restoredSortBy =
|
const restoredSortBy =
|
||||||
sortByOptions.find((opt) => String(opt.value) === tableFilterState.sortBy) ||
|
sortByOptions.find(
|
||||||
null;
|
(opt) => String(opt.value) === tableFilterState.sortBy
|
||||||
|
) || null;
|
||||||
setSelectedSortBy(restoredSortBy);
|
setSelectedSortBy(restoredSortBy);
|
||||||
|
|
||||||
// Restore formik values
|
// Restore formik values
|
||||||
|
|||||||
@@ -153,8 +153,14 @@ const InventoryAdjustmentTable = () => {
|
|||||||
updateFilter('productFilter', values.product_id || '');
|
updateFilter('productFilter', values.product_id || '');
|
||||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
updateFilter('warehouseFilter', values.warehouse_id || '');
|
||||||
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
||||||
updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : '');
|
updateFilter(
|
||||||
updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : '');
|
'productName',
|
||||||
|
productIdValue?.label ? String(productIdValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'warehouseName',
|
||||||
|
warehouseIdValue?.label ? String(warehouseIdValue.label) : ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
@@ -216,7 +222,10 @@ const InventoryAdjustmentTable = () => {
|
|||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
) => {
|
) => {
|
||||||
const warehouse = val as OptionType | null;
|
const warehouse = val as OptionType | null;
|
||||||
formik.setFieldValue('warehouse_id', warehouse?.value ? String(warehouse.value) : null);
|
formik.setFieldValue(
|
||||||
|
'warehouse_id',
|
||||||
|
warehouse?.value ? String(warehouse.value) : null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterTransactionTypeChange = useCallback(
|
const handleFilterTransactionTypeChange = useCallback(
|
||||||
@@ -236,7 +245,10 @@ const InventoryAdjustmentTable = () => {
|
|||||||
);
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.productName) {
|
if (tableFilterState.productName) {
|
||||||
return { value: formik.values.product_id, label: tableFilterState.productName };
|
return {
|
||||||
|
value: formik.values.product_id,
|
||||||
|
label: tableFilterState.productName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
|
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
|
||||||
@@ -248,10 +260,17 @@ const InventoryAdjustmentTable = () => {
|
|||||||
);
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.warehouseName) {
|
if (tableFilterState.warehouseName) {
|
||||||
return { value: formik.values.warehouse_id, label: tableFilterState.warehouseName };
|
return {
|
||||||
|
value: formik.values.warehouse_id,
|
||||||
|
label: tableFilterState.warehouseName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.warehouse_id, warehouseOptions, tableFilterState.warehouseName]);
|
}, [
|
||||||
|
formik.values.warehouse_id,
|
||||||
|
warehouseOptions,
|
||||||
|
tableFilterState.warehouseName,
|
||||||
|
]);
|
||||||
|
|
||||||
const transactionTypeValue = useMemo(() => {
|
const transactionTypeValue = useMemo(() => {
|
||||||
if (!formik.values.transaction_type) return null;
|
if (!formik.values.transaction_type) return null;
|
||||||
|
|||||||
@@ -149,8 +149,14 @@ const MovementTable = () => {
|
|||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('productFilter', values.product_id || '');
|
updateFilter('productFilter', values.product_id || '');
|
||||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
updateFilter('warehouseFilter', values.warehouse_id || '');
|
||||||
updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : '');
|
updateFilter(
|
||||||
updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : '');
|
'productName',
|
||||||
|
productIdValue?.label ? String(productIdValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'warehouseName',
|
||||||
|
warehouseIdValue?.label ? String(warehouseIdValue.label) : ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
@@ -216,7 +222,10 @@ const MovementTable = () => {
|
|||||||
);
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.productName) {
|
if (tableFilterState.productName) {
|
||||||
return { value: formik.values.product_id, label: tableFilterState.productName };
|
return {
|
||||||
|
value: formik.values.product_id,
|
||||||
|
label: tableFilterState.productName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
|
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
|
||||||
@@ -228,10 +237,17 @@ const MovementTable = () => {
|
|||||||
);
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.warehouseName) {
|
if (tableFilterState.warehouseName) {
|
||||||
return { value: formik.values.warehouse_id, label: tableFilterState.warehouseName };
|
return {
|
||||||
|
value: formik.values.warehouse_id,
|
||||||
|
label: tableFilterState.warehouseName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.warehouse_id, warehouseOptions, tableFilterState.warehouseName]);
|
}, [
|
||||||
|
formik.values.warehouse_id,
|
||||||
|
warehouseOptions,
|
||||||
|
tableFilterState.warehouseName,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -403,7 +419,13 @@ const MovementTable = () => {
|
|||||||
|
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={['page', 'pageSize', 'search', 'productName', 'warehouseName']}
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'productName',
|
||||||
|
'warehouseName',
|
||||||
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -113,7 +113,10 @@ const InventoryProductTable = () => {
|
|||||||
validationSchema: object().shape({ category_id: string().nullable() }),
|
validationSchema: object().shape({ category_id: string().nullable() }),
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('categoryFilter', values.category_id || '');
|
updateFilter('categoryFilter', values.category_id || '');
|
||||||
updateFilter('categoryName', categoryIdValue?.label ? String(categoryIdValue.label) : '');
|
updateFilter(
|
||||||
|
'categoryName',
|
||||||
|
categoryIdValue?.label ? String(categoryIdValue.label) : ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
@@ -145,10 +148,17 @@ const InventoryProductTable = () => {
|
|||||||
);
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.categoryName) {
|
if (tableFilterState.categoryName) {
|
||||||
return { value: formik.values.category_id, label: tableFilterState.categoryName };
|
return {
|
||||||
|
value: formik.values.category_id,
|
||||||
|
label: tableFilterState.categoryName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.category_id, categoryOptions, tableFilterState.categoryName]);
|
}, [
|
||||||
|
formik.values.category_id,
|
||||||
|
categoryOptions,
|
||||||
|
tableFilterState.categoryName,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -156,9 +166,14 @@ const InventoryProductTable = () => {
|
|||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterCategoryChange = (val: OptionType | OptionType[] | null) => {
|
const handleFilterCategoryChange = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
const category = val as OptionType | null;
|
const category = val as OptionType | null;
|
||||||
formik.setFieldValue('category_id', category?.value ? String(category.value) : null);
|
formik.setFieldValue(
|
||||||
|
'category_id',
|
||||||
|
category?.value ? String(category.value) : null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
@@ -279,7 +294,11 @@ const InventoryProductTable = () => {
|
|||||||
value={tableFilterState.search ?? ''}
|
value={tableFilterState.search ?? ''}
|
||||||
onChange={searchChangeHandler}
|
onChange={searchChangeHandler}
|
||||||
startAdornment={
|
startAdornment={
|
||||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
<Icon
|
||||||
|
icon='heroicons:magnifying-glass'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-24 max-w-3xs',
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
|
|||||||
@@ -262,9 +262,18 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
updateFilter('kandang_id', values.kandang_id || '');
|
updateFilter('kandang_id', values.kandang_id || '');
|
||||||
updateFilter('category', values.category || '');
|
updateFilter('category', values.category || '');
|
||||||
updateFilter('period', values.period || '');
|
updateFilter('period', values.period || '');
|
||||||
updateFilter('area_name', areaValue?.label ? String(areaValue.label) : '');
|
updateFilter(
|
||||||
updateFilter('location_name', locationValue?.label ? String(locationValue.label) : '');
|
'area_name',
|
||||||
updateFilter('kandang_name', kandangValue?.label ? String(kandangValue.label) : '');
|
areaValue?.label ? String(areaValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'location_name',
|
||||||
|
locationValue?.label ? String(locationValue.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'kandang_name',
|
||||||
|
kandangValue?.label ? String(kandangValue.label) : ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
@@ -329,10 +338,15 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
// ===== FILTER HELPERS =====
|
// ===== FILTER HELPERS =====
|
||||||
const areaValue = useMemo(() => {
|
const areaValue = useMemo(() => {
|
||||||
if (!formik.values.area_id) return null;
|
if (!formik.values.area_id) return null;
|
||||||
const found = areaOptions.find((opt) => String(opt.value) === formik.values.area_id);
|
const found = areaOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.area_id
|
||||||
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.area_name) {
|
if (tableFilterState.area_name) {
|
||||||
return { value: formik.values.area_id, label: tableFilterState.area_name };
|
return {
|
||||||
|
value: formik.values.area_id,
|
||||||
|
label: tableFilterState.area_name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.area_id, areaOptions, tableFilterState.area_name]);
|
}, [formik.values.area_id, areaOptions, tableFilterState.area_name]);
|
||||||
@@ -344,10 +358,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.location_name) {
|
if (tableFilterState.location_name) {
|
||||||
return { value: formik.values.location_id, label: tableFilterState.location_name };
|
return {
|
||||||
|
value: formik.values.location_id,
|
||||||
|
label: tableFilterState.location_name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.location_id, locationOptions, tableFilterState.location_name]);
|
}, [
|
||||||
|
formik.values.location_id,
|
||||||
|
locationOptions,
|
||||||
|
tableFilterState.location_name,
|
||||||
|
]);
|
||||||
|
|
||||||
const kandangValue = useMemo(() => {
|
const kandangValue = useMemo(() => {
|
||||||
if (!formik.values.kandang_id) return null;
|
if (!formik.values.kandang_id) return null;
|
||||||
@@ -356,7 +377,10 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
if (tableFilterState.kandang_name) {
|
if (tableFilterState.kandang_name) {
|
||||||
return { value: formik.values.kandang_id, label: tableFilterState.kandang_name };
|
return {
|
||||||
|
value: formik.values.kandang_id,
|
||||||
|
label: tableFilterState.kandang_name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [formik.values.kandang_id, kandangOptions, tableFilterState.kandang_name]);
|
}, [formik.values.kandang_id, kandangOptions, tableFilterState.kandang_name]);
|
||||||
@@ -446,7 +470,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprovalHandler = async (
|
const confirmApprovalHandler = async (
|
||||||
@@ -984,7 +1008,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
|
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={tableFilterState}
|
values={tableFilterState}
|
||||||
excludeFields={['page', 'pageSize', 'search', 'area_name', 'location_name', 'kandang_name']}
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'area_name',
|
||||||
|
'location_name',
|
||||||
|
'kandang_name',
|
||||||
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type ProjectFlockFormSchemaType = {
|
|||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
location_id: number;
|
location_id: number;
|
||||||
|
periode: number | string;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
project_budgets: ProjectFlockBudgetsSchemaType[];
|
project_budgets: ProjectFlockBudgetsSchemaType[];
|
||||||
};
|
};
|
||||||
@@ -109,6 +110,12 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
|
|||||||
.min(1, 'Lokasi wajib diisi!')
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
.required('Lokasi wajib diisi!'),
|
.required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
|
// Period
|
||||||
|
periode: Yup.number()
|
||||||
|
.typeError('Periode harus berupa angka!')
|
||||||
|
.min(1, 'Periode minimal 1!')
|
||||||
|
.required('Periode wajib diisi!'),
|
||||||
|
|
||||||
kandang_ids: Yup.array()
|
kandang_ids: Yup.array()
|
||||||
.of(Yup.number().required('Kandang tidak valid!'))
|
.of(Yup.number().required('Kandang tidak valid!'))
|
||||||
.min(1, 'Minimal harus ada 1 kandang!')
|
.min(1, 'Minimal harus ada 1 kandang!')
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ export const ProjectFlockFormConfirmationTable = ({
|
|||||||
label: 'Standar Produksi',
|
label: 'Standar Produksi',
|
||||||
value: projectFlockForm?.production_standard?.label ?? '-',
|
value: projectFlockForm?.production_standard?.label ?? '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Periode',
|
||||||
|
value: projectFlockForm?.periode ?? '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Informasi Kandang',
|
label: 'Informasi Kandang',
|
||||||
value: '',
|
value: '',
|
||||||
@@ -529,6 +533,7 @@ const ProjectFlockForm = ({
|
|||||||
kandang_ids: initialValues?.kandangs?.map(
|
kandang_ids: initialValues?.kandangs?.map(
|
||||||
(k: Kandang) => k.id
|
(k: Kandang) => k.id
|
||||||
) as number[],
|
) as number[],
|
||||||
|
periode: initialValues?.period ?? '',
|
||||||
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
project_budgets: initialValues?.project_budgets?.map((budget) => {
|
||||||
return {
|
return {
|
||||||
nonstock: {
|
nonstock: {
|
||||||
@@ -568,6 +573,7 @@ const ProjectFlockForm = ({
|
|||||||
category: values.category as string,
|
category: values.category as string,
|
||||||
production_standard_id: values.production_standard_id as number,
|
production_standard_id: values.production_standard_id as number,
|
||||||
location_id: values.location_id as number,
|
location_id: values.location_id as number,
|
||||||
|
periode: parseInt(values.periode as unknown as string),
|
||||||
kandang_ids: values.kandang_ids as number[],
|
kandang_ids: values.kandang_ids as number[],
|
||||||
project_budgets: values.project_budgets.flatMap((budget) => {
|
project_budgets: values.project_budgets.flatMap((budget) => {
|
||||||
return {
|
return {
|
||||||
@@ -1023,12 +1029,18 @@ const ProjectFlockForm = ({
|
|||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='period'
|
name='periode'
|
||||||
label='Periode'
|
label='Periode'
|
||||||
disabled
|
|
||||||
readOnly
|
|
||||||
placeholder='Periode Flock'
|
placeholder='Periode Flock'
|
||||||
value={selectedLocation ? inputPeriod : ''}
|
value={formik.values.periode}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
allowNegative={false}
|
||||||
|
decimalScale={0}
|
||||||
|
isError={
|
||||||
|
formik.touched.periode && Boolean(formik.errors.periode)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.periode as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -496,11 +496,23 @@ const TransferToLayingsTable = () => {
|
|||||||
|
|
||||||
updateFilter('startDate', values.startDate || '');
|
updateFilter('startDate', values.startDate || '');
|
||||||
updateFilter('endDate', values.endDate || '');
|
updateFilter('endDate', values.endDate || '');
|
||||||
updateFilter('flockSource', flockSourceOpts.map((o) => String(o.value)).join(','));
|
updateFilter(
|
||||||
updateFilter('flockDestination', flockDestOpts.map((o) => String(o.value)).join(','));
|
'flockSource',
|
||||||
|
flockSourceOpts.map((o) => String(o.value)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'flockDestination',
|
||||||
|
flockDestOpts.map((o) => String(o.value)).join(',')
|
||||||
|
);
|
||||||
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
|
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
|
||||||
updateFilter('flockSourceNames', flockSourceOpts.map((o) => String(o.label)).join(','));
|
updateFilter(
|
||||||
updateFilter('flockDestinationNames', flockDestOpts.map((o) => String(o.label)).join(','));
|
'flockSourceNames',
|
||||||
|
flockSourceOpts.map((o) => String(o.label)).join(',')
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'flockDestinationNames',
|
||||||
|
flockDestOpts.map((o) => String(o.label)).join(',')
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
|
|||||||
@@ -445,7 +445,13 @@ const PurchaseOrderDetail = ({
|
|||||||
setEditPoDate('');
|
setEditPoDate('');
|
||||||
editPoDateModal.closeModal();
|
editPoDateModal.closeModal();
|
||||||
refetchData?.();
|
refetchData?.();
|
||||||
}, [initialValues?.id, searchParams, editPoDate, editPoDateModal, refetchData]);
|
}, [
|
||||||
|
initialValues?.id,
|
||||||
|
searchParams,
|
||||||
|
editPoDate,
|
||||||
|
editPoDateModal,
|
||||||
|
refetchData,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== APPROVAL/REJECTION HANDLERS =====
|
// ===== APPROVAL/REJECTION HANDLERS =====
|
||||||
const managerApprovalHandler = async () => {
|
const managerApprovalHandler = async () => {
|
||||||
@@ -873,8 +879,7 @@ const PurchaseOrderDetail = ({
|
|||||||
</span>
|
</span>
|
||||||
<div className='ml-3 flex items-center gap-1'>
|
<div className='ml-3 flex items-center gap-1'>
|
||||||
<span className='text-gray-900'>
|
<span className='text-gray-900'>
|
||||||
:{' '}
|
: {formatDate(purchaseData.po_date, 'DD MMM YYYY')}
|
||||||
{formatDate(purchaseData.po_date, 'DD MMM YYYY')}
|
|
||||||
</span>
|
</span>
|
||||||
<RequirePermission permissions='lti.purchase.update'>
|
<RequirePermission permissions='lti.purchase.update'>
|
||||||
<Button
|
<Button
|
||||||
@@ -884,10 +889,7 @@ const PurchaseOrderDetail = ({
|
|||||||
className='p-1 min-h-0 h-auto'
|
className='p-1 min-h-0 h-auto'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditPoDate(
|
setEditPoDate(
|
||||||
formatDate(
|
formatDate(purchaseData.po_date, 'YYYY-MM-DD')
|
||||||
purchaseData.po_date,
|
|
||||||
'YYYY-MM-DD'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
editPoDateModal.openModal();
|
editPoDateModal.openModal();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -127,23 +127,37 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
const restoredLocation = filterParams.location_id
|
const restoredLocation = filterParams.location_id
|
||||||
? locationOptions.find((opt) => String(opt.value) === filterParams.location_id) ||
|
? locationOptions.find(
|
||||||
{ value: filterParams.location_id, label: filterParams.location_id }
|
(opt) => String(opt.value) === filterParams.location_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.location_id,
|
||||||
|
label: filterParams.location_id,
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
const restoredSupplier = filterParams.supplier_id
|
const restoredSupplier = filterParams.supplier_id
|
||||||
? supplierOptions.find((opt) => String(opt.value) === filterParams.supplier_id) ||
|
? supplierOptions.find(
|
||||||
{ value: filterParams.supplier_id, label: filterParams.supplier_id }
|
(opt) => String(opt.value) === filterParams.supplier_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.supplier_id,
|
||||||
|
label: filterParams.supplier_id,
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
const restoredKandang = filterParams.kandang_id
|
const restoredKandang = filterParams.kandang_id
|
||||||
? projectFlockKandangOptions.find((opt) => String(opt.value) === filterParams.kandang_id) ||
|
? projectFlockKandangOptions.find(
|
||||||
{ value: filterParams.kandang_id, label: filterParams.kandang_id }
|
(opt) => String(opt.value) === filterParams.kandang_id
|
||||||
|
) || { value: filterParams.kandang_id, label: filterParams.kandang_id }
|
||||||
: null;
|
: null;
|
||||||
const restoredNonstock = filterParams.nonstock_id
|
const restoredNonstock = filterParams.nonstock_id
|
||||||
? nonstockOptions.find((opt) => String(opt.value) === filterParams.nonstock_id) ||
|
? nonstockOptions.find(
|
||||||
{ value: filterParams.nonstock_id, label: filterParams.nonstock_id }
|
(opt) => String(opt.value) === filterParams.nonstock_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.nonstock_id,
|
||||||
|
label: filterParams.nonstock_id,
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
const restoredCategory = filterParams.category
|
const restoredCategory = filterParams.category
|
||||||
? categoryOptions.find((opt) => opt.value === filterParams.category) || null
|
? categoryOptions.find((opt) => opt.value === filterParams.category) ||
|
||||||
|
null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
|
|||||||
@@ -731,6 +731,27 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isLoading && data.length > 0 && meta && (
|
||||||
|
<div className='max-w-sm ml-auto'>
|
||||||
|
<Pagination
|
||||||
|
totalItems={meta.total_results || 0}
|
||||||
|
itemsPerPage={meta.limit || 0}
|
||||||
|
currentPage={meta.page || 0}
|
||||||
|
onPrevPage={() =>
|
||||||
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||||
|
}
|
||||||
|
onNextPage={() =>
|
||||||
|
setCurrentPage((curr) =>
|
||||||
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
onRowChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map((customerReport) => {
|
data.map((customerReport) => {
|
||||||
|
|||||||
@@ -663,6 +663,27 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isLoading && data.length > 0 && meta && (
|
||||||
|
<div className='max-w-sm ml-auto'>
|
||||||
|
<Pagination
|
||||||
|
totalItems={meta.total_results || 0}
|
||||||
|
itemsPerPage={meta.limit || 0}
|
||||||
|
currentPage={meta.page || 0}
|
||||||
|
onPrevPage={() =>
|
||||||
|
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
|
||||||
|
}
|
||||||
|
onNextPage={() =>
|
||||||
|
setCurrentPage((curr) =>
|
||||||
|
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
onRowChange={setPageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map((supplierReport) => {
|
data.map((supplierReport) => {
|
||||||
|
|||||||
+21
-8
@@ -264,20 +264,33 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
|
|||||||
|
|
||||||
handleFilterModalOpenRef.current = () => {
|
handleFilterModalOpenRef.current = () => {
|
||||||
const restoredAreaId = filterParams.area_id
|
const restoredAreaId = filterParams.area_id
|
||||||
? areaOptions.find((opt) => String(opt.value) === filterParams.area_id) ||
|
? areaOptions.find(
|
||||||
{ value: filterParams.area_id, label: filterParams.area_id }
|
(opt) => String(opt.value) === filterParams.area_id
|
||||||
|
) || { value: filterParams.area_id, label: filterParams.area_id }
|
||||||
: null;
|
: null;
|
||||||
const restoredLocationId = filterParams.location_id
|
const restoredLocationId = filterParams.location_id
|
||||||
? locationOptions.find((opt) => String(opt.value) === filterParams.location_id) ||
|
? locationOptions.find(
|
||||||
{ value: filterParams.location_id, label: filterParams.location_id }
|
(opt) => String(opt.value) === filterParams.location_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.location_id,
|
||||||
|
label: filterParams.location_id,
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
const restoredProjectFlockId = filterParams.project_flock_id
|
const restoredProjectFlockId = filterParams.project_flock_id
|
||||||
? projectFlockOptions.find((opt) => String(opt.value) === filterParams.project_flock_id) ||
|
? projectFlockOptions.find(
|
||||||
{ value: filterParams.project_flock_id, label: filterParams.project_flock_id }
|
(opt) => String(opt.value) === filterParams.project_flock_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.project_flock_id,
|
||||||
|
label: filterParams.project_flock_id,
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
const restoredKandangId = filterParams.project_flock_kandang_id
|
const restoredKandangId = filterParams.project_flock_kandang_id
|
||||||
? projectFlockKandangOptions.find((opt) => String(opt.value) === filterParams.project_flock_kandang_id) ||
|
? projectFlockKandangOptions.find(
|
||||||
{ value: filterParams.project_flock_kandang_id, label: filterParams.project_flock_kandang_id }
|
(opt) => String(opt.value) === filterParams.project_flock_kandang_id
|
||||||
|
) || {
|
||||||
|
value: filterParams.project_flock_kandang_id,
|
||||||
|
label: filterParams.project_flock_kandang_id,
|
||||||
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
'lti.master.uoms.list',
|
'lti.master.uoms.list',
|
||||||
'lti.master.warehouses.list',
|
'lti.master.warehouses.list',
|
||||||
'lti.master.production_standards.list',
|
'lti.master.production_standards.list',
|
||||||
|
'lti.system_settings.update',
|
||||||
],
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
@@ -303,6 +304,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/master-data/production-standard',
|
link: '/master-data/production-standard',
|
||||||
permission: ['lti.master.production_standards.list'],
|
permission: ['lti.master.production_standards.list'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Konfigurasi Sistem',
|
||||||
|
link: '/master-data/system-config',
|
||||||
|
permission: ['lti.system_settings.update'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -218,4 +218,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/master-data/production-standard/detail/edit/': [
|
'/master-data/production-standard/detail/edit/': [
|
||||||
'lti.master.production_standards.update',
|
'lti.master.production_standards.update',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'/master-data/system-config/': ['lti.system_settings.update'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -880,9 +880,9 @@ export function DailyChecklistContent() {
|
|||||||
setChecklistStatus('SUBMITTED');
|
setChecklistStatus('SUBMITTED');
|
||||||
|
|
||||||
const shareToWhatsApp = () => {
|
const shareToWhatsApp = () => {
|
||||||
const kandangName = kandangOptions.find(
|
const kandangName =
|
||||||
(k) => String(k.value) === kandangId
|
kandangOptions.find((k) => String(k.value) === kandangId)?.label ||
|
||||||
)?.label || kandangId;
|
kandangId;
|
||||||
const statusMsg = getStatusMessage();
|
const statusMsg = getStatusMessage();
|
||||||
const category = selectedCategory || '';
|
const category = selectedCategory || '';
|
||||||
const message = encodeURIComponent(
|
const message = encodeURIComponent(
|
||||||
|
|||||||
+3
-2
@@ -593,7 +593,9 @@ export function DetailDailyChecklistContent() {
|
|||||||
let shareData: ShareData;
|
let shareData: ShareData;
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const htmlBlob = await htmlToImage.toBlob(document.body);
|
const htmlBlob = await htmlToImage.toBlob(document.body, {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
});
|
||||||
const imgFile = new File(
|
const imgFile = new File(
|
||||||
[htmlBlob!],
|
[htmlBlob!],
|
||||||
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
|
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
|
||||||
@@ -606,7 +608,6 @@ export function DetailDailyChecklistContent() {
|
|||||||
files: [imgFile],
|
files: [imgFile],
|
||||||
title: baseTitle,
|
title: baseTitle,
|
||||||
text: fullMessage,
|
text: fullMessage,
|
||||||
url: window.location.href,
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
shareData = {
|
shareData = {
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { SystemSettingsApi } from '@/services/api/system-settings';
|
||||||
|
import { SystemSetting } from '@/types/api/system-settings/system-setting';
|
||||||
|
|
||||||
|
const ALLOW_NEGATIVE_PAKAN_OVK_KEY = 'allow_negative_pakan_ovk';
|
||||||
|
|
||||||
|
function SettingToggle({
|
||||||
|
setting,
|
||||||
|
onToggle,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
setting: SystemSetting;
|
||||||
|
onToggle: (key: string, currentValue: boolean) => void;
|
||||||
|
loading: boolean;
|
||||||
|
}) {
|
||||||
|
const isEnabled = setting.value === 'true';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-start justify-between gap-4 py-5'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<p className='text-sm font-medium text-gray-900'>
|
||||||
|
{setting.key === ALLOW_NEGATIVE_PAKAN_OVK_KEY
|
||||||
|
? 'Mode Migrasi PAKAN & OVK'
|
||||||
|
: setting.key}
|
||||||
|
</p>
|
||||||
|
{setting.description && (
|
||||||
|
<p className='text-sm text-gray-500 mt-0.5'>{setting.description}</p>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center mt-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
isEnabled
|
||||||
|
? 'bg-amber-100 text-amber-700'
|
||||||
|
: 'bg-gray-100 text-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isEnabled ? 'Aktif' : 'Nonaktif'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
role='switch'
|
||||||
|
aria-checked={isEnabled}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => onToggle(setting.key, isEnabled)}
|
||||||
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[#0069e0] focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||||
|
isEnabled ? 'bg-[#0069e0]' : 'bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden='true'
|
||||||
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
isEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemConfigContent() {
|
||||||
|
const [toggling, setToggling] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: settingsResponse,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshSettings,
|
||||||
|
} = useSWR(SystemSettingsApi.basePath, SystemSettingsApi.getAllFetcher, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggle = async (key: string, currentValue: boolean) => {
|
||||||
|
if (key !== ALLOW_NEGATIVE_PAKAN_OVK_KEY) return;
|
||||||
|
|
||||||
|
setToggling(key);
|
||||||
|
try {
|
||||||
|
const res = await SystemSettingsApi.setAllowNegativePakanOvk({
|
||||||
|
value: !currentValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseError(res)) {
|
||||||
|
toast.error(res.message || 'Gagal mengubah pengaturan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSettings();
|
||||||
|
toast.success(
|
||||||
|
!currentValue
|
||||||
|
? 'Mode migrasi PAKAN & OVK diaktifkan'
|
||||||
|
: 'Mode migrasi PAKAN & OVK dinonaktifkan'
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
toast.error('Terjadi kesalahan saat mengubah pengaturan');
|
||||||
|
} finally {
|
||||||
|
setToggling(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = isResponseSuccess(settingsResponse)
|
||||||
|
? settingsResponse.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (isLoading && !settingsResponse) {
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Konfigurasi Sistem
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data •{' '}
|
||||||
|
<span className='text-[#0069e0]'>Konfigurasi Sistem</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-12 text-center text-gray-500'>
|
||||||
|
Memuat data...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
||||||
|
Konfigurasi Sistem
|
||||||
|
</h1>
|
||||||
|
<p className='text-sm text-gray-600 mt-1'>
|
||||||
|
Master Data •{' '}
|
||||||
|
<span className='text-[#0069e0]'>Konfigurasi Sistem</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
|
<CardContent className='p-0'>
|
||||||
|
<div className='px-6 py-4 border-b border-gray-200/60'>
|
||||||
|
<h2 className='text-base font-semibold text-gray-800'>
|
||||||
|
Pengaturan Global
|
||||||
|
</h2>
|
||||||
|
<p className='text-sm text-gray-500 mt-0.5'>
|
||||||
|
Pengaturan ini berlaku untuk seluruh sistem.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='px-6 divide-y divide-gray-200/60'>
|
||||||
|
{settings.length === 0 ? (
|
||||||
|
<p className='py-10 text-center text-sm text-gray-500'>
|
||||||
|
Tidak ada pengaturan tersedia.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
settings.map((setting) => (
|
||||||
|
<SettingToggle
|
||||||
|
key={setting.key}
|
||||||
|
setting={setting}
|
||||||
|
onToggle={handleToggle}
|
||||||
|
loading={toggling === setting.key}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import {
|
||||||
|
SetAllowNegativePakanOvkPayload,
|
||||||
|
SystemSetting,
|
||||||
|
} from '@/types/api/system-settings/system-setting';
|
||||||
|
|
||||||
|
const BASE_PATH = '/system-settings';
|
||||||
|
|
||||||
|
export const SystemSettingsApi = {
|
||||||
|
basePath: BASE_PATH,
|
||||||
|
|
||||||
|
getAllFetcher: (
|
||||||
|
endpoint: string
|
||||||
|
): Promise<BaseApiResponse<SystemSetting[]>> =>
|
||||||
|
httpClientFetcher<BaseApiResponse<SystemSetting[]>>(endpoint),
|
||||||
|
|
||||||
|
async getAll(): Promise<BaseApiResponse<SystemSetting[]> | undefined> {
|
||||||
|
try {
|
||||||
|
return await httpClient<BaseApiResponse<SystemSetting[]>>(BASE_PATH);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<SystemSetting[]>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setAllowNegativePakanOvk(
|
||||||
|
payload: SetAllowNegativePakanOvkPayload
|
||||||
|
): Promise<BaseApiResponse | undefined> {
|
||||||
|
try {
|
||||||
|
return await httpClient<BaseApiResponse>(
|
||||||
|
`${BASE_PATH}/allow-negative-pakan-ovk`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
body: payload,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -38,7 +38,7 @@ export async function httpClient<T, B = unknown>(
|
|||||||
method: opts.method ?? 'GET',
|
method: opts.method ?? 'GET',
|
||||||
params: opts.query,
|
params: opts.query,
|
||||||
data: opts.body,
|
data: opts.body,
|
||||||
timeout: opts.timeoutMs ?? 10_000,
|
timeout: opts.timeoutMs ?? 30_000,
|
||||||
withCredentials: isCookieAuth && !isBearerAuth,
|
withCredentials: isCookieAuth && !isBearerAuth,
|
||||||
responseType: opts.responseType,
|
responseType: opts.responseType,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
+1
@@ -51,6 +51,7 @@ export type CreateProjectFlockPayload = {
|
|||||||
category: string;
|
category: string;
|
||||||
production_standard_id: number;
|
production_standard_id: number;
|
||||||
location_id: number;
|
location_id: number;
|
||||||
|
periode: number;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
project_budgets?: ProjectFlockBudget[];
|
project_budgets?: ProjectFlockBudget[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export type SystemSetting = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetAllowNegativePakanOvkPayload = {
|
||||||
|
value: boolean;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user