mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' into 'production'
Development See merge request mbugroup/lti-web-client!468
This commit is contained in:
+21
-2
@@ -30,6 +30,10 @@ 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
|
||||||
- |
|
- |
|
||||||
@@ -41,7 +45,11 @@ 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:
|
||||||
@@ -142,6 +150,10 @@ 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
|
||||||
@@ -170,6 +182,9 @@ 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
|
||||||
@@ -185,7 +200,7 @@ deploy:staging:
|
|||||||
url: https://stg-lti-erp.mbugroup.id
|
url: https://stg-lti-erp.mbugroup.id
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# ====== STAGING (Branch production) ======
|
# ====== (Branch production) ======
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
build:production:
|
build:production:
|
||||||
<<: *build_template
|
<<: *build_template
|
||||||
@@ -198,6 +213,10 @@ build:production:
|
|||||||
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
|
||||||
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
|
||||||
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
|
||||||
|
NEXT_PUBLIC_APP_ENV: 'production'
|
||||||
|
NEXT_PUBLIC_HELPDESK_URL: 'https://helpdesk.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_DASHBOARD_ACCOUNTING_URL: 'https://dashboard-ho.mbugroup.id/'
|
||||||
|
NEXT_PUBLIC_S3_PUBLIC_BASE_URL: 'https://mbu-lti-storage.s3.ap-southeast-3.amazonaws.com/'
|
||||||
|
|
||||||
deploy:production:
|
deploy:production:
|
||||||
<<: *deploy_template
|
<<: *deploy_template
|
||||||
|
|||||||
@@ -161,6 +161,47 @@ const handleFilterLocationChange = useCallback(
|
|||||||
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
||||||
- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.)
|
- 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -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'>
|
||||||
Page {currentPage} of {totalPages}
|
Total Item: {totalItems} | Page {currentPage} of {totalPages}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -548,21 +548,15 @@ const ExpenseRequestContent = ({
|
|||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
{initialValues?.documents.map(
|
{initialValues?.documents.map(
|
||||||
(requestDocument, requestDocumentIdx) => {
|
(requestDocument, requestDocumentIdx) => {
|
||||||
const path = requestDocument.path.startsWith(
|
|
||||||
'/'
|
|
||||||
)
|
|
||||||
? requestDocument.path.slice(1)
|
|
||||||
: requestDocument.path;
|
|
||||||
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
|
|
||||||
return (
|
return (
|
||||||
<li key={requestDocumentIdx}>
|
<li key={requestDocumentIdx}>
|
||||||
<Link
|
<Link
|
||||||
href={documentUrl}
|
href={requestDocument.path}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
className='text-blue-500 underline'
|
className='text-blue-500 underline'
|
||||||
>
|
>
|
||||||
{requestDocument.path}{' '}
|
{requestDocument.name}{' '}
|
||||||
<Icon
|
<Icon
|
||||||
icon='cuida:open-in-new-tab-outline'
|
icon='cuida:open-in-new-tab-outline'
|
||||||
width={12}
|
width={12}
|
||||||
|
|||||||
@@ -14,18 +14,7 @@ export type ExpensesFilterType = {
|
|||||||
|
|
||||||
export const ExpensesFilterSchema = yup.object({
|
export const ExpensesFilterSchema = yup.object({
|
||||||
transaction_date: yup.string().nullable(),
|
transaction_date: yup.string().nullable(),
|
||||||
realization_date: yup
|
realization_date: yup.string().nullable(),
|
||||||
.string()
|
|
||||||
.nullable()
|
|
||||||
.test(
|
|
||||||
'is-greater-or-equal-transaction',
|
|
||||||
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
|
|
||||||
function (value) {
|
|
||||||
const { transaction_date } = this.parent;
|
|
||||||
if (!transaction_date || !value) return true;
|
|
||||||
return new Date(value) >= new Date(transaction_date);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
location: yup
|
location: yup
|
||||||
.object({
|
.object({
|
||||||
value: yup.number().required(),
|
value: yup.number().required(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
||||||
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
||||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
@@ -11,18 +12,6 @@ const InventoryProductDetail = ({
|
|||||||
}: {
|
}: {
|
||||||
inventoryProduct?: InventoryProduct;
|
inventoryProduct?: InventoryProduct;
|
||||||
}) => {
|
}) => {
|
||||||
const stockLogs = useMemo(() => {
|
|
||||||
return (
|
|
||||||
inventoryProduct?.product_warehouses?.flatMap((warehouse) =>
|
|
||||||
warehouse.stock_logs.map((log) => ({
|
|
||||||
...log,
|
|
||||||
warehouse_name: warehouse.warehouse_name,
|
|
||||||
warehouse_id: warehouse.warehouse_id,
|
|
||||||
}))
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
}, [inventoryProduct]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
<FormHeader
|
<FormHeader
|
||||||
@@ -114,7 +103,14 @@ const InventoryProductDetail = ({
|
|||||||
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StockLogTable stockLogs={stockLogs} />
|
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
||||||
|
{inventoryProduct?.product_warehouses?.map((productWarehouse) => (
|
||||||
|
<StockLogTable
|
||||||
|
key={productWarehouse.id}
|
||||||
|
productWarehouse={productWarehouse}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,84 +1,151 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
|
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
|
||||||
import { StockLog } from '@/types/api/inventory/product';
|
import { StockLogApi } from '@/services/api/inventory';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ProductWarehouseStock, StockLog } from '@/types/api/inventory/product';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { FileDown } from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
||||||
|
warehouseName
|
||||||
|
) => [
|
||||||
|
{
|
||||||
|
header: 'ID',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal',
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Gudang',
|
||||||
|
accessorKey: 'warehouse_name',
|
||||||
|
cell: warehouseName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Stock Akhir',
|
||||||
|
accessorKey: 'stock',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.stock);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Peningkatan',
|
||||||
|
accessorKey: 'increase',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.increase);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Penurunan',
|
||||||
|
accessorKey: 'decrease',
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.decrease);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jenis Transaksi',
|
||||||
|
accessorKey: 'loggable_type',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.loggable_type
|
||||||
|
? formatTitleCase(props.row.original.loggable_type)
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Catatan',
|
||||||
|
accessorKey: 'notes',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.notes ? props.row.original.notes : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Oleh',
|
||||||
|
accessorKey: 'created_user.name',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const StockLogTable = ({
|
const StockLogTable = ({
|
||||||
stockLogs,
|
productWarehouse,
|
||||||
}: {
|
}: {
|
||||||
stockLogs: (StockLog & { warehouse_name: string; warehouse_id: number })[];
|
productWarehouse: ProductWarehouseStock;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isExportLoading, setIsExportLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
product_warehouse_id: productWarehouse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleExportExcel = async () => {
|
||||||
|
setIsExportLoading(true);
|
||||||
|
try {
|
||||||
|
await StockLogApi.exportToExcel(
|
||||||
|
productWarehouse.warehouse_name,
|
||||||
|
getTableFilterQueryString()
|
||||||
|
);
|
||||||
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
|
} finally {
|
||||||
|
setIsExportLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
||||||
|
`${StockLogApi.basePath}${getTableFilterQueryString()}`,
|
||||||
|
StockLogApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const stockLogs = isResponseSuccess(stockLogsResponse)
|
||||||
|
? stockLogsResponse.data
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title='Informasi Stock Produk'
|
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
||||||
collapsible
|
collapsible
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className='flex justify-end px-6 pt-4'>
|
||||||
|
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
||||||
|
<FileDown size={16} />
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Table<StockLog>
|
<Table<StockLog>
|
||||||
data={stockLogs}
|
data={stockLogs}
|
||||||
columns={[
|
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
||||||
{
|
page={tableFilterState.page ?? 0}
|
||||||
header: 'ID',
|
pageSize={tableFilterState.pageSize}
|
||||||
accessorKey: 'id',
|
onPageChange={setPage}
|
||||||
},
|
onPageSizeChange={setPageSize}
|
||||||
{
|
isLoading={isLoadingStockLogs}
|
||||||
header: 'Tanggal',
|
totalItems={
|
||||||
accessorKey: 'created_at',
|
isResponseSuccess(stockLogsResponse)
|
||||||
cell: (props) => {
|
? stockLogsResponse.meta?.total_results
|
||||||
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
|
: 0
|
||||||
},
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Gudang',
|
|
||||||
accessorKey: 'warehouse_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Stock Akhir',
|
|
||||||
accessorKey: 'stock',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.stock);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Peningkatan',
|
|
||||||
accessorKey: 'increase',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.increase);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Penurunan',
|
|
||||||
accessorKey: 'decrease',
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.decrease);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Jenis Transaksi',
|
|
||||||
accessorKey: 'loggable_type',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.loggable_type
|
|
||||||
? formatTitleCase(props.row.original.loggable_type)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Catatan',
|
|
||||||
accessorKey: 'notes',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.notes ? props.row.original.notes : '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Oleh',
|
|
||||||
accessorKey: 'created_user.name',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-6',
|
containerClassName: 'mt-4 mb-0',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
|||||||
@@ -1,13 +1,42 @@
|
|||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const stockProductWarehouseTableColumns: ColumnDef<ProductWarehouseStock>[] = [
|
||||||
|
{
|
||||||
|
header: 'Nama Gudang',
|
||||||
|
accessorKey: 'warehouse_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Lokasi',
|
||||||
|
accessorKey: 'location',
|
||||||
|
cell: (props) => {
|
||||||
|
return props.row.original.location != null
|
||||||
|
? props.row.original.location.name
|
||||||
|
: '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Stok',
|
||||||
|
accessorFn(row) {
|
||||||
|
return row.current_stock;
|
||||||
|
},
|
||||||
|
cell: (props) => {
|
||||||
|
return formatNumber(props.row.original.current_stock);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const StockProductWarehouseTable = ({
|
const StockProductWarehouseTable = ({
|
||||||
productWarehouseStock,
|
productWarehouseStock,
|
||||||
}: {
|
}: {
|
||||||
productWarehouseStock?: ProductWarehouseStock[];
|
productWarehouseStock?: ProductWarehouseStock[];
|
||||||
}) => {
|
}) => {
|
||||||
|
const { state: tableFilterState, setPage, setPageSize } = useTableFilter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title='Informasi Gudang'
|
title='Informasi Gudang'
|
||||||
@@ -19,32 +48,14 @@ const StockProductWarehouseTable = ({
|
|||||||
>
|
>
|
||||||
<Table<ProductWarehouseStock>
|
<Table<ProductWarehouseStock>
|
||||||
data={productWarehouseStock ?? []}
|
data={productWarehouseStock ?? []}
|
||||||
columns={[
|
columns={stockProductWarehouseTableColumns}
|
||||||
{
|
pageSize={tableFilterState.pageSize}
|
||||||
header: 'Nama Gudang',
|
page={tableFilterState.page ?? 0}
|
||||||
accessorKey: 'warehouse_name',
|
totalItems={productWarehouseStock?.length ?? 0}
|
||||||
},
|
onPageChange={setPage}
|
||||||
{
|
onPageSizeChange={setPageSize}
|
||||||
header: 'Lokasi',
|
|
||||||
accessorKey: 'location',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.location != null
|
|
||||||
? props.row.original.location.name
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Stok',
|
|
||||||
accessorFn(row) {
|
|
||||||
return row.current_stock;
|
|
||||||
},
|
|
||||||
cell: (props) => {
|
|
||||||
return formatNumber(props.row.original.current_stock);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-6',
|
containerClassName: 'mt-6 mb-0',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
@@ -27,7 +26,13 @@ import {
|
|||||||
MarketingFilter,
|
MarketingFilter,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { CellContext, ColumnDef, Row } from '@tanstack/react-table';
|
import {
|
||||||
|
CellContext,
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
SortingState,
|
||||||
|
Updater,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -198,6 +203,8 @@ const MarketingTable = () => {
|
|||||||
project_flock_name: '',
|
project_flock_name: '',
|
||||||
project_flock_kandang_id: '',
|
project_flock_kandang_id: '',
|
||||||
project_flock_kandang_name: '',
|
project_flock_kandang_name: '',
|
||||||
|
sort_by: '',
|
||||||
|
order_by: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -207,6 +214,8 @@ const MarketingTable = () => {
|
|||||||
customer_id: 'customer_id',
|
customer_id: 'customer_id',
|
||||||
project_flock_id: 'project_flock_id',
|
project_flock_id: 'project_flock_id',
|
||||||
project_flock_kandang_id: 'project_flock_kandang_id',
|
project_flock_kandang_id: 'project_flock_kandang_id',
|
||||||
|
sort_by: 'sort_by',
|
||||||
|
order_by: 'sort_order',
|
||||||
},
|
},
|
||||||
excludeKeysFromUrl: [
|
excludeKeysFromUrl: [
|
||||||
'product_names',
|
'product_names',
|
||||||
@@ -220,6 +229,26 @@ const MarketingTable = () => {
|
|||||||
storeName: 'marketing-table',
|
storeName: 'marketing-table',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sorting: SortingState = tableFilterState.sort_by
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: tableFilterState.sort_by,
|
||||||
|
desc: tableFilterState.order_by === 'desc',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleSortingChange = (updater: Updater<SortingState>) => {
|
||||||
|
const next = typeof updater === 'function' ? updater(sorting) : updater;
|
||||||
|
if (next.length > 0) {
|
||||||
|
updateFilter('sort_by', next[0].id, true);
|
||||||
|
updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true);
|
||||||
|
} else {
|
||||||
|
updateFilter('sort_by', '', true);
|
||||||
|
updateFilter('order_by', '', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
data: marketing,
|
data: marketing,
|
||||||
@@ -359,55 +388,50 @@ const MarketingTable = () => {
|
|||||||
? 'DELIVERY_ORDER'
|
? 'DELIVERY_ORDER'
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const marketingFilterInitialValues = useMemo(() => {
|
const productIds = tableFilterState.product_ids
|
||||||
const productIds = tableFilterState.product_ids
|
? tableFilterState.product_ids
|
||||||
? tableFilterState.product_ids
|
.split(',')
|
||||||
.split(',')
|
.map((item) => item.trim())
|
||||||
.map((item) => item.trim())
|
.filter(Boolean)
|
||||||
.filter(Boolean)
|
: [];
|
||||||
: [];
|
|
||||||
|
|
||||||
const productLabels = tableFilterState.product_names
|
const productLabels = tableFilterState.product_names
|
||||||
? tableFilterState.product_names
|
? tableFilterState.product_names
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
const marketingFilterInitialValues = {
|
||||||
product_ids: productIds.map((value, idx) => ({
|
product_ids: productIds.map((value, idx) => ({
|
||||||
value: Number(value),
|
value: Number(value),
|
||||||
label: productLabels[idx] || '-',
|
label: productLabels[idx] || '-',
|
||||||
})),
|
})),
|
||||||
status: tableFilterState.status
|
status: tableFilterState.status
|
||||||
? {
|
? {
|
||||||
value: tableFilterState.status,
|
value: tableFilterState.status,
|
||||||
label: tableFilterState.status_name,
|
label: tableFilterState.status_name,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
customer: tableFilterState.customer_id
|
||||||
customer: tableFilterState.customer_id
|
? {
|
||||||
? {
|
value: Number(tableFilterState.customer_id),
|
||||||
value: Number(tableFilterState.customer_id),
|
label: tableFilterState.customer_name,
|
||||||
label: tableFilterState.customer_name,
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
project_flock: tableFilterState.project_flock_id
|
||||||
|
? {
|
||||||
project_flock: tableFilterState.project_flock_id
|
value: Number(tableFilterState.project_flock_id),
|
||||||
? {
|
label: tableFilterState.project_flock_name,
|
||||||
value: Number(tableFilterState.project_flock_id),
|
}
|
||||||
label: tableFilterState.project_flock_name,
|
: null,
|
||||||
}
|
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
||||||
: null,
|
? {
|
||||||
|
value: Number(tableFilterState.project_flock_kandang_id),
|
||||||
project_flock_kandang: tableFilterState.project_flock_kandang_id
|
label: tableFilterState.project_flock_kandang_name,
|
||||||
? {
|
}
|
||||||
value: Number(tableFilterState.project_flock_kandang_id),
|
: null,
|
||||||
label: tableFilterState.project_flock_kandang_name,
|
};
|
||||||
}
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const approveMarketingHandler = async (notes: string) => {
|
const approveMarketingHandler = async (notes: string) => {
|
||||||
if (idsToProcess.length === 0) {
|
if (idsToProcess.length === 0) {
|
||||||
@@ -542,27 +566,29 @@ const MarketingTable = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetExportProgressForm = useCallback(() => {
|
const resetExportProgressForm = () => {
|
||||||
setExportProgressStartDate('');
|
setExportProgressStartDate('');
|
||||||
setExportProgressEndDate('');
|
setExportProgressEndDate('');
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const exportProgressStartDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
const exportProgressStartDateChangeHandler: ChangeEventHandler<
|
||||||
useCallback((e) => {
|
HTMLInputElement
|
||||||
setExportProgressStartDate(e.target.value);
|
> = (e) => {
|
||||||
}, []);
|
setExportProgressStartDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const exportProgressEndDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
|
const exportProgressEndDateChangeHandler: ChangeEventHandler<
|
||||||
useCallback((e) => {
|
HTMLInputElement
|
||||||
setExportProgressEndDate(e.target.value);
|
> = (e) => {
|
||||||
}, []);
|
setExportProgressEndDate(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const exportProgressInputToExcelClickHandler = useCallback(() => {
|
const exportProgressInputToExcelClickHandler = () => {
|
||||||
resetExportProgressForm();
|
resetExportProgressForm();
|
||||||
exportProgressInputModal.openModal();
|
exportProgressInputModal.openModal();
|
||||||
}, [exportProgressInputModal, resetExportProgressForm]);
|
};
|
||||||
|
|
||||||
const submitExportProgressInputHandler = useCallback(async () => {
|
const submitExportProgressInputHandler = async () => {
|
||||||
if (!exportProgressStartDate || !exportProgressEndDate) {
|
if (!exportProgressStartDate || !exportProgressEndDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -585,12 +611,7 @@ const MarketingTable = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsExportProgressLoading(false);
|
setIsExportProgressLoading(false);
|
||||||
}
|
}
|
||||||
}, [
|
};
|
||||||
exportProgressEndDate,
|
|
||||||
exportProgressInputModal,
|
|
||||||
exportProgressStartDate,
|
|
||||||
resetExportProgressForm,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
const columns = useMemo<ColumnDef<Marketing>[]>(() => {
|
||||||
return [
|
return [
|
||||||
@@ -656,7 +677,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'so_do_number',
|
accessorKey: 'so_number',
|
||||||
header: 'No. Order',
|
header: 'No. Order',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
return props.row.original.do_number
|
return props.row.original.do_number
|
||||||
@@ -672,7 +693,7 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'approval.step_name',
|
accessorKey: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.latest_approval;
|
const approval = props.row.original.latest_approval;
|
||||||
@@ -707,10 +728,12 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer.name',
|
accessorKey: 'customer',
|
||||||
header: 'Customer',
|
header: 'Customer',
|
||||||
|
cell: (props) => props.row.original.customer.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'grand_total',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.sales_order
|
row.sales_order
|
||||||
?.map((product) => product.total_price)
|
?.map((product) => product.total_price)
|
||||||
@@ -727,6 +750,7 @@ const MarketingTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'marketing_products.length',
|
accessorKey: 'marketing_products.length',
|
||||||
header: 'Product Details',
|
header: 'Product Details',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
if (props?.row?.original?.sales_order?.length) {
|
if (props?.row?.original?.sales_order?.length) {
|
||||||
if (props?.row?.original?.sales_order?.length > 1) {
|
if (props?.row?.original?.sales_order?.length > 1) {
|
||||||
@@ -859,6 +883,8 @@ const MarketingTable = () => {
|
|||||||
'customer_name',
|
'customer_name',
|
||||||
'project_flock_name',
|
'project_flock_name',
|
||||||
'project_flock_kandang_name',
|
'project_flock_kandang_name',
|
||||||
|
'sort_by',
|
||||||
|
'order_by',
|
||||||
]}
|
]}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
@@ -949,6 +975,9 @@ const MarketingTable = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
pageSize={tableFilterState.pageSize}
|
pageSize={tableFilterState.pageSize}
|
||||||
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={handleSortingChange}
|
||||||
|
manualSorting
|
||||||
totalItems={
|
totalItems={
|
||||||
isResponseSuccess(marketing)
|
isResponseSuccess(marketing)
|
||||||
? marketing?.meta?.total_results
|
? marketing?.meta?.total_results
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||||
import React, {
|
|
||||||
useCallback,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
} from 'react';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
@@ -46,8 +39,6 @@ import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
@@ -77,6 +68,26 @@ const getStatusBadgeColor = (status: string): Color => {
|
|||||||
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRecordingApproved = (recording: Recording): boolean => {
|
||||||
|
return (
|
||||||
|
recording.approval?.action === 'APPROVED' &&
|
||||||
|
recording.approval?.step_name === 'Disetujui'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== FILTER HELPERS =====
|
||||||
|
const recordingApprovalStatusOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'CREATED', label: 'Pengajuan' },
|
||||||
|
{ value: 'UPDATED', label: 'Diperbarui' },
|
||||||
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectFlockCategoryOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'GROWING', label: 'Growing' },
|
||||||
|
{ value: 'LAYING', label: 'Laying' },
|
||||||
|
];
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
@@ -268,25 +279,31 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RecordingTable = () => {
|
const RecordingTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter<{
|
||||||
|
search: string;
|
||||||
|
areaFilter: OptionType<number> | null;
|
||||||
|
locationFilter: OptionType<number> | null;
|
||||||
|
projectFlockFilter: OptionType<number> | null;
|
||||||
|
kandangFilter: OptionType<number> | null;
|
||||||
|
projectFlockKandangFilter: number | null;
|
||||||
|
approvalStatusFilter: OptionType<string> | null;
|
||||||
|
projectFlockCategoryFilter: OptionType<string> | null;
|
||||||
|
}>({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
areaFilter: '',
|
areaFilter: null,
|
||||||
locationFilter: '',
|
locationFilter: null,
|
||||||
projectFlockFilter: '',
|
projectFlockFilter: null,
|
||||||
kandangFilter: '',
|
kandangFilter: null,
|
||||||
projectFlockKandangFilter: '',
|
projectFlockKandangFilter: null,
|
||||||
approvalStatusFilter: '',
|
approvalStatusFilter: null,
|
||||||
projectFlockCategoryFilter: '',
|
projectFlockCategoryFilter: null,
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -300,68 +317,73 @@ const RecordingTable = () => {
|
|||||||
approvalStatusFilter: 'approval_status',
|
approvalStatusFilter: 'approval_status',
|
||||||
projectFlockCategoryFilter: 'project_flock_category',
|
projectFlockCategoryFilter: 'project_flock_category',
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
persist: true,
|
||||||
updateFilter('search', searchValue);
|
storeName: 'recording-table',
|
||||||
}, [searchValue, updateFilter]);
|
});
|
||||||
|
|
||||||
// ===== FILTER MODAL STATE =====
|
// ===== FILTER MODAL STATE =====
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
// ===== FILTER STATE =====
|
|
||||||
const [filterArea, setFilterArea] = useState<OptionType | null>(null);
|
|
||||||
const [filterLocation, setFilterLocation] = useState<OptionType | null>(null);
|
|
||||||
const [filterProjectFlock, setFilterProjectFlock] =
|
|
||||||
useState<OptionType | null>(null);
|
|
||||||
const [filterKandang, setFilterKandang] = useState<OptionType | null>(null);
|
|
||||||
const [, setFilterProjectFlockKandangId] = useState<number | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [filterLocationAreaId, setFilterLocationAreaId] = useState<string>('');
|
|
||||||
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
|
||||||
useState<string>('');
|
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
const formik = useFormik<RecordingFilterType>({
|
const formik = useFormik<RecordingFilterType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area_id: tableFilterState.areaFilter,
|
||||||
location_id: null,
|
location_id: tableFilterState.locationFilter,
|
||||||
project_flock_id: null,
|
project_flock_id: tableFilterState.projectFlockFilter,
|
||||||
kandang_id: null,
|
kandang_id: tableFilterState.kandangFilter,
|
||||||
project_flock_kandang_id: null,
|
project_flock_kandang_id: tableFilterState.projectFlockKandangFilter,
|
||||||
approval_status: null,
|
approval_status: tableFilterState.approvalStatusFilter,
|
||||||
project_flock_category: null,
|
project_flock_category: tableFilterState.projectFlockCategoryFilter,
|
||||||
},
|
},
|
||||||
validationSchema: RecordingFilterSchema,
|
validationSchema: RecordingFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area_id, true);
|
||||||
updateFilter('locationFilter', values.location_id || '');
|
updateFilter('locationFilter', values.location_id, true);
|
||||||
updateFilter('projectFlockFilter', values.project_flock_id || '');
|
updateFilter('projectFlockFilter', values.project_flock_id, true);
|
||||||
updateFilter('kandangFilter', values.kandang_id || '');
|
updateFilter('kandangFilter', values.kandang_id, true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'projectFlockKandangFilter',
|
'projectFlockKandangFilter',
|
||||||
values.project_flock_kandang_id || ''
|
values.project_flock_kandang_id,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
updateFilter('approvalStatusFilter', values.approval_status || '');
|
updateFilter('approvalStatusFilter', values.approval_status, true);
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'projectFlockCategoryFilter',
|
'projectFlockCategoryFilter',
|
||||||
values.project_flock_category || ''
|
values.project_flock_category,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
updateFilter('areaFilter', '');
|
|
||||||
updateFilter('locationFilter', '');
|
|
||||||
updateFilter('projectFlockFilter', '');
|
|
||||||
updateFilter('kandangFilter', '');
|
|
||||||
updateFilter('projectFlockKandangFilter', '');
|
|
||||||
updateFilter('approvalStatusFilter', '');
|
|
||||||
updateFilter('projectFlockCategoryFilter', '');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formikResetHandler = () => {
|
||||||
|
updateFilter('areaFilter', null, true);
|
||||||
|
updateFilter('locationFilter', null, true);
|
||||||
|
updateFilter('projectFlockFilter', null, true);
|
||||||
|
updateFilter('kandangFilter', null, true);
|
||||||
|
updateFilter('projectFlockKandangFilter', null, true);
|
||||||
|
updateFilter('approvalStatusFilter', null, true);
|
||||||
|
updateFilter('projectFlockCategoryFilter', null, true);
|
||||||
|
|
||||||
|
formik.resetForm({
|
||||||
|
values: {
|
||||||
|
area_id: null,
|
||||||
|
location_id: null,
|
||||||
|
project_flock_id: null,
|
||||||
|
kandang_id: null,
|
||||||
|
project_flock_kandang_id: null,
|
||||||
|
approval_status: null,
|
||||||
|
project_flock_category: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
filterModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { project_flock_id, kandang_id } = formik.values;
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||||
@@ -396,13 +418,6 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
// ===== LOCATION, AREA, KANDANG OPTIONS =====
|
||||||
const locationParams = useMemo(() => {
|
|
||||||
if (filterLocationAreaId) {
|
|
||||||
return { area_id: filterLocationAreaId };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [filterLocationAreaId]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
@@ -413,7 +428,9 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
'search',
|
'search',
|
||||||
locationParams
|
{
|
||||||
|
area_id: String(formik.values.area_id?.value),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -428,13 +445,6 @@ const RecordingTable = () => {
|
|||||||
'search'
|
'search'
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectFlockParams = useMemo(() => {
|
|
||||||
if (filterProjectFlockLocationId) {
|
|
||||||
return { location_id: filterProjectFlockLocationId };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [filterProjectFlockLocationId]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setProjectFlockInputValue,
|
setInputValue: setProjectFlockInputValue,
|
||||||
options: projectFlockOptions,
|
options: projectFlockOptions,
|
||||||
@@ -446,34 +456,41 @@ const RecordingTable = () => {
|
|||||||
'id',
|
'id',
|
||||||
'flock_name',
|
'flock_name',
|
||||||
'search',
|
'search',
|
||||||
projectFlockParams
|
{
|
||||||
|
location_id: String(formik.values.location_id?.value),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const kandangOptions = useMemo(() => {
|
const kandangOptions = useMemo(() => {
|
||||||
if (!filterProjectFlock || !projectFlocksRawData) return [];
|
if (!project_flock_id || !projectFlocksRawData) return [];
|
||||||
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
if (!isResponseSuccess(projectFlocksRawData)) return [];
|
||||||
|
|
||||||
const data = projectFlocksRawData.data as ProjectFlock[];
|
const data = projectFlocksRawData.data as ProjectFlock[];
|
||||||
const selectedProjectFlockData = data.find(
|
const selectedProjectFlockData = data.find((pf) =>
|
||||||
(pf) => pf.id === filterProjectFlock.value
|
pf.id === formik.values.project_flock_id?.value
|
||||||
|
? Number(formik.values.project_flock_id.value)
|
||||||
|
: 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedProjectFlockData?.kandangs) return [];
|
if (!selectedProjectFlockData?.kandangs) return [];
|
||||||
|
|
||||||
return selectedProjectFlockData.kandangs.map((k) => ({
|
return selectedProjectFlockData.kandangs.map((k) => ({
|
||||||
value: k.id,
|
value: k.id,
|
||||||
label: k.name || '',
|
label: k.name || '',
|
||||||
}));
|
}));
|
||||||
}, [filterProjectFlock, projectFlocksRawData]);
|
}, [project_flock_id, projectFlocksRawData]);
|
||||||
|
|
||||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
if (!filterProjectFlock || !filterKandang) return null;
|
if (!project_flock_id?.value || !kandang_id?.value) return null;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
project_flock_id: filterProjectFlock.value.toString(),
|
project_flock_id: project_flock_id.value.toString(),
|
||||||
kandang_id: filterKandang.value.toString(),
|
kandang_id: kandang_id.value.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||||
}, [filterProjectFlock, filterKandang]);
|
}, [project_flock_id, kandang_id]);
|
||||||
|
|
||||||
const { data: projectFlockKandangLookupData } = useSWR(
|
const { data: projectFlockKandangLookupData } = useSWR(
|
||||||
projectFlockKandangLookupUrl,
|
projectFlockKandangLookupUrl,
|
||||||
@@ -495,154 +512,45 @@ const RecordingTable = () => {
|
|||||||
? projectFlockKandangLookupData.data
|
? projectFlockKandangLookupData.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const formikRef = useRef(formik);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
formikRef.current = formik;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectFlockKandangLookup?.id) {
|
if (projectFlockKandangLookup?.id) {
|
||||||
const pfkId = String(projectFlockKandangLookup.id);
|
const pfkId = String(projectFlockKandangLookup.id);
|
||||||
setFilterProjectFlockKandangId(projectFlockKandangLookup.id);
|
formik.setFieldValue('project_flock_kandang_id', pfkId);
|
||||||
formikRef.current.setFieldValue('project_flock_kandang_id', pfkId);
|
|
||||||
} else {
|
} else {
|
||||||
setFilterProjectFlockKandangId(undefined);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
formikRef.current.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
}
|
}
|
||||||
}, [projectFlockKandangLookup]);
|
}, [projectFlockKandangLookup]);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterAreaChange = useCallback(
|
const handleFilterAreaChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('area_id', val);
|
||||||
const area = val as OptionType | null;
|
formik.setFieldValue('location_id', null);
|
||||||
const areaId = area?.value ? String(area.value) : null;
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
formik.setFieldValue('kandang_id', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
};
|
||||||
|
|
||||||
formik.setFieldValue('area_id', areaId);
|
const handleFilterLocationChange = (
|
||||||
formik.setFieldValue('location_id', null);
|
val: OptionType | OptionType[] | null
|
||||||
formik.setFieldValue('project_flock_id', null);
|
) => {
|
||||||
formik.setFieldValue('kandang_id', null);
|
formik.setFieldValue('location_id', val);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_id', null);
|
||||||
|
formik.setFieldValue('kandang_id', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
};
|
||||||
|
|
||||||
setFilterArea(area);
|
const handleFilterProjectFlockChange = (
|
||||||
setFilterLocation(null);
|
val: OptionType | OptionType[] | null
|
||||||
setFilterProjectFlock(null);
|
) => {
|
||||||
setFilterKandang(null);
|
formik.setFieldValue('project_flock_id', val);
|
||||||
setFilterLocationAreaId(areaId || '');
|
formik.setFieldValue('kandang_id', null);
|
||||||
setFilterProjectFlockLocationId('');
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
},
|
};
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterLocationChange = useCallback(
|
const handleFilterKandangChange = (val: OptionType | OptionType[] | null) => {
|
||||||
(val: OptionType | OptionType[] | null) => {
|
formik.setFieldValue('kandang_id', val);
|
||||||
const location = val as OptionType | null;
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
const locationId = location?.value ? String(location.value) : null;
|
};
|
||||||
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
|
||||||
formik.setFieldValue('project_flock_id', null);
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterLocation(location);
|
|
||||||
setFilterProjectFlock(null);
|
|
||||||
setFilterKandang(null);
|
|
||||||
setFilterProjectFlockLocationId(locationId || '');
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterProjectFlockChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const projectFlock = val as OptionType | null;
|
|
||||||
const projectFlockId = projectFlock?.value
|
|
||||||
? String(projectFlock.value)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
formik.setFieldValue('project_flock_id', projectFlockId);
|
|
||||||
formik.setFieldValue('kandang_id', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterProjectFlock(projectFlock);
|
|
||||||
setFilterKandang(null);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterKandangChange = useCallback(
|
|
||||||
(val: OptionType | OptionType[] | null) => {
|
|
||||||
const kandang = val as OptionType | null;
|
|
||||||
const kandangId = kandang?.value ? String(kandang.value) : null;
|
|
||||||
|
|
||||||
formik.setFieldValue('kandang_id', kandangId);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
|
||||||
|
|
||||||
setFilterKandang(kandang);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== FILTER HELPERS =====
|
|
||||||
const areaIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.area_id) return null;
|
|
||||||
return (
|
|
||||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}, [formik.values.area_id, areaOptions]);
|
|
||||||
|
|
||||||
const locationIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.location_id) return null;
|
|
||||||
return (
|
|
||||||
locationOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.location_id, locationOptions]);
|
|
||||||
|
|
||||||
const projectFlockIdValue = useMemo(() => {
|
|
||||||
if (!filterProjectFlock) return null;
|
|
||||||
return filterProjectFlock;
|
|
||||||
}, [filterProjectFlock]);
|
|
||||||
|
|
||||||
const kandangIdValue = useMemo(() => {
|
|
||||||
if (!formik.values.kandang_id) return null;
|
|
||||||
return (
|
|
||||||
kandangOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.kandang_id
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.kandang_id, kandangOptions]);
|
|
||||||
|
|
||||||
const recordingApprovalStatusOptions: OptionType<string>[] = [
|
|
||||||
{ value: 'CREATED', label: 'Pengajuan' },
|
|
||||||
{ value: 'UPDATED', label: 'Diperbarui' },
|
|
||||||
{ value: 'APPROVED', label: 'Disetujui' },
|
|
||||||
{ value: 'REJECTED', label: 'Ditolak' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const projectFlockCategoryOptions: OptionType<string>[] = [
|
|
||||||
{ value: 'GROWING', label: 'Growing' },
|
|
||||||
{ value: 'LAYING', label: 'Laying' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const approvalStatusValue = useMemo(() => {
|
|
||||||
if (!formik.values.approval_status) return null;
|
|
||||||
return (
|
|
||||||
recordingApprovalStatusOptions.find(
|
|
||||||
(opt) => opt.value === formik.values.approval_status
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.approval_status]);
|
|
||||||
|
|
||||||
const projectFlockCategoryValue = useMemo(() => {
|
|
||||||
if (!formik.values.project_flock_category) return null;
|
|
||||||
return (
|
|
||||||
projectFlockCategoryOptions.find(
|
|
||||||
(opt) => opt.value === formik.values.project_flock_category
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}, [formik.values.project_flock_category]);
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
@@ -650,25 +558,9 @@ const RecordingTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecordingApproved = useCallback((recording: Recording): boolean => {
|
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
return (
|
updateFilter('search', e.target.value, true);
|
||||||
recording.approval?.action === 'APPROVED' &&
|
};
|
||||||
recording.approval?.step_name === 'Disetujui'
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableState('recording-table', pathname);
|
|
||||||
}, [pathname, setTableState]);
|
|
||||||
|
|
||||||
const searchChangeHandler = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
setSearchValue(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
},
|
|
||||||
[updateFilter, setSearchValue, setPage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const singleDeleteHandler = async () => {
|
const singleDeleteHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
@@ -1220,7 +1112,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)}%`
|
? `${value.toFixed(2)} butir`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1236,7 +1128,7 @@ const RecordingTable = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='text-center text-gray-600'>
|
<div className='text-center text-gray-600'>
|
||||||
{value !== null && value !== undefined
|
{value !== null && value !== undefined
|
||||||
? `${value.toFixed(2)}%`
|
? `${value.toFixed(2)} btr`
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1572,13 +1464,13 @@ const RecordingTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
<form onSubmit={formik.handleSubmit} onReset={formikResetHandler}>
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
placeholder='Pilih Area'
|
placeholder='Pilih Area'
|
||||||
options={areaOptions}
|
options={areaOptions}
|
||||||
value={areaIdValue}
|
value={formik.values.area_id}
|
||||||
onChange={handleFilterAreaChange}
|
onChange={handleFilterAreaChange}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
isLoading={isLoadingAreaOptions}
|
isLoading={isLoadingAreaOptions}
|
||||||
@@ -1591,13 +1483,13 @@ const RecordingTable = () => {
|
|||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationIdValue}
|
value={formik.values.location_id}
|
||||||
onChange={handleFilterLocationChange}
|
onChange={handleFilterLocationChange}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isDisabled={!filterArea}
|
isDisabled={!formik.values.area_id?.value}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1605,13 +1497,13 @@ const RecordingTable = () => {
|
|||||||
label='Project Flock'
|
label='Project Flock'
|
||||||
placeholder='Pilih Project Flock'
|
placeholder='Pilih Project Flock'
|
||||||
options={projectFlockOptions}
|
options={projectFlockOptions}
|
||||||
value={projectFlockIdValue}
|
value={formik.values.project_flock_id}
|
||||||
onChange={handleFilterProjectFlockChange}
|
onChange={handleFilterProjectFlockChange}
|
||||||
onInputChange={setProjectFlockInputValue}
|
onInputChange={setProjectFlockInputValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!filterLocation}
|
isDisabled={!formik.values.location_id?.value}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1619,11 +1511,11 @@ const RecordingTable = () => {
|
|||||||
label='Kandang'
|
label='Kandang'
|
||||||
placeholder='Pilih Kandang'
|
placeholder='Pilih Kandang'
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
value={kandangIdValue}
|
value={formik.values.kandang_id}
|
||||||
onChange={handleFilterKandangChange}
|
onChange={handleFilterKandangChange}
|
||||||
isLoading={!filterProjectFlock}
|
isLoading={!formik.values.project_flock_id?.value}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!filterProjectFlock}
|
isDisabled={!formik.values.project_flock_id?.value}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1631,12 +1523,9 @@ const RecordingTable = () => {
|
|||||||
label='Kategori'
|
label='Kategori'
|
||||||
placeholder='Pilih Kategori'
|
placeholder='Pilih Kategori'
|
||||||
options={projectFlockCategoryOptions}
|
options={projectFlockCategoryOptions}
|
||||||
value={projectFlockCategoryValue}
|
value={formik.values.project_flock_category}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue('project_flock_category', val);
|
||||||
'project_flock_category',
|
|
||||||
!Array.isArray(val) && val ? String(val.value) : null
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -1646,12 +1535,9 @@ const RecordingTable = () => {
|
|||||||
label='Status Approval'
|
label='Status Approval'
|
||||||
placeholder='Pilih Status Approval'
|
placeholder='Pilih Status Approval'
|
||||||
options={recordingApprovalStatusOptions}
|
options={recordingApprovalStatusOptions}
|
||||||
value={approvalStatusValue}
|
value={formik.values.approval_status}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue('approval_status', val);
|
||||||
'approval_status',
|
|
||||||
!Array.isArray(val) && val ? String(val.value) : null
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -1661,19 +1547,9 @@ const RecordingTable = () => {
|
|||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
onClick={() => {
|
|
||||||
formik.resetForm();
|
|
||||||
setFilterArea(null);
|
|
||||||
setFilterLocation(null);
|
|
||||||
setFilterProjectFlock(null);
|
|
||||||
setFilterKandang(null);
|
|
||||||
setFilterLocationAreaId('');
|
|
||||||
setFilterProjectFlockLocationId('');
|
|
||||||
filterModal.closeModal();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,21 +1,40 @@
|
|||||||
import { string, object } from 'yup';
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const RecordingFilterSchema = object().shape({
|
export const RecordingFilterSchema = Yup.object().shape({
|
||||||
area_id: string().nullable(),
|
area_id: Yup.object({
|
||||||
location_id: string().nullable(),
|
value: Yup.number().nullable(),
|
||||||
project_flock_id: string().nullable(),
|
label: Yup.string().nullable(),
|
||||||
kandang_id: string().nullable(),
|
}).nullable(),
|
||||||
project_flock_kandang_id: string().nullable(),
|
location_id: Yup.object({
|
||||||
approval_status: string().nullable(),
|
value: Yup.number().nullable(),
|
||||||
project_flock_category: string().nullable(),
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
kandang_id: Yup.object({
|
||||||
|
value: Yup.number().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_kandang_id: Yup.number().nullable(),
|
||||||
|
approval_status: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_category: Yup.object({
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
label: Yup.string().nullable(),
|
||||||
|
}).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RecordingFilterType = {
|
export type RecordingFilterType = {
|
||||||
area_id: string | null;
|
area_id: OptionType<number> | null;
|
||||||
location_id: string | null;
|
location_id: OptionType<number> | null;
|
||||||
project_flock_id: string | null;
|
project_flock_id: OptionType<number> | null;
|
||||||
kandang_id: string | null;
|
kandang_id: OptionType<number> | null;
|
||||||
project_flock_kandang_id: string | null;
|
project_flock_kandang_id: number | null;
|
||||||
approval_status: string | null;
|
approval_status: OptionType<string> | null;
|
||||||
project_flock_category: string | null;
|
project_flock_category: OptionType<string> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
CreateGrowingRecordingPayload,
|
CreateGrowingRecordingPayload,
|
||||||
CreateLayingRecordingPayload,
|
CreateLayingRecordingPayload,
|
||||||
CreateEggPayload,
|
CreateEggPayload,
|
||||||
|
RecordingStock,
|
||||||
} from '@/types/api/production/recording';
|
} from '@/types/api/production/recording';
|
||||||
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse';
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
product_warehouse_id?: {
|
product_warehouse_id?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
} | null;
|
||||||
source_product_warehouse_id?: number;
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
@@ -53,7 +54,7 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
|||||||
product_warehouse_id?: {
|
product_warehouse_id?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
} | null;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
}[];
|
}[];
|
||||||
@@ -71,7 +72,7 @@ export type DepletionSchema = {
|
|||||||
product_warehouse_id?: {
|
product_warehouse_id?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
} | null;
|
||||||
source_product_warehouse_id?: number;
|
source_product_warehouse_id?: number;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
};
|
};
|
||||||
@@ -80,7 +81,7 @@ export type EggSchema = {
|
|||||||
product_warehouse_id?: {
|
product_warehouse_id?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
} | null;
|
||||||
qty?: number | string;
|
qty?: number | string;
|
||||||
weight?: number | string;
|
weight?: number | string;
|
||||||
};
|
};
|
||||||
@@ -104,7 +105,7 @@ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
|||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Depletions harus berupa angka!'),
|
.nullable(),
|
||||||
source_product_warehouse_id: Yup.number()
|
source_product_warehouse_id: Yup.number()
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Gudang sumber harus berupa angka!'),
|
.typeError('Gudang sumber harus berupa angka!'),
|
||||||
@@ -119,7 +120,7 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
|||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.typeError('Kondisi telur harus berupa angka!'),
|
.nullable(),
|
||||||
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
||||||
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||||
});
|
});
|
||||||
@@ -282,8 +283,9 @@ export const getRecordingGrowingFormInitialValues = (
|
|||||||
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
label: getProductWarehouseOptionLabel(stock.product_warehouse),
|
||||||
},
|
},
|
||||||
qty:
|
qty:
|
||||||
(stock as { qty?: number; usage_amount?: number }).qty ||
|
(stock as RecordingStock).qty ||
|
||||||
(stock as { qty?: number; usage_amount?: number }).usage_amount ||
|
((stock as RecordingStock).usage_amount || 0) +
|
||||||
|
((stock as RecordingStock).pending_qty || 0) ||
|
||||||
'',
|
'',
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
@@ -324,7 +326,7 @@ export const getRecordingLayingFormInitialValues = (
|
|||||||
weight: egg.weight,
|
weight: egg.weight,
|
||||||
})) ?? [
|
})) ?? [
|
||||||
{
|
{
|
||||||
product_warehouse_id: undefined,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1472,7 +1472,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
(productWarehouseId: number) => {
|
(productWarehouseId: number) => {
|
||||||
if ((type === 'edit' || type === 'detail') && initialValues?.stocks) {
|
if ((type === 'edit' || type === 'detail') && initialValues?.stocks) {
|
||||||
const existingStock = initialValues.stocks.find(
|
const existingStock = initialValues.stocks.find(
|
||||||
(s) => s.product_warehouse_id === productWarehouseId
|
(s) => Number(s.product_warehouse_id) === Number(productWarehouseId)
|
||||||
) as RecordingStock | undefined;
|
) as RecordingStock | undefined;
|
||||||
if (existingStock) {
|
if (existingStock) {
|
||||||
return {
|
return {
|
||||||
@@ -1508,9 +1508,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
if (pendingQty > 0) {
|
if (pendingQty > 0) {
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||||
(tersedia: {formatNumber(requestedUsage)} | pending:{' '}
|
(tersedia: {formatNumber(availableStock)} | pending:{' '}
|
||||||
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
<span className='text-error'>{formatNumber(pendingQty)}</span> |
|
||||||
pakai: {formatNumber(requestedUsage + pendingQty)})
|
pakai: {formatNumber(requestedUsage)})
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1731,14 +1731,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1746,7 +1746,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1795,14 +1795,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1810,7 +1810,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -1848,14 +1848,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('stocks', false, false);
|
formik.setFieldTouched('stocks', false, false);
|
||||||
formik.setFieldValue('stocks', [
|
formik.setFieldValue('stocks', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
formik.setFieldTouched('depletions', false, false);
|
formik.setFieldTouched('depletions', false, false);
|
||||||
formik.setFieldValue('depletions', [
|
formik.setFieldValue('depletions', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1863,7 +1863,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
formik.setFieldTouched('eggs', false, false);
|
formik.setFieldTouched('eggs', false, false);
|
||||||
formik.setFieldValue('eggs', [
|
formik.setFieldValue('eggs', [
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
weight: '',
|
weight: '',
|
||||||
},
|
},
|
||||||
@@ -2076,7 +2076,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newStocks = [
|
const newStocks = [
|
||||||
...(formik.values.stocks || []),
|
...(formik.values.stocks || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2108,7 +2108,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newDepletions = [
|
const newDepletions = [
|
||||||
...(formik.values.depletions || []),
|
...(formik.values.depletions || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2142,7 +2142,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const newEggs = [
|
const newEggs = [
|
||||||
...((formik.values as RecordingLayingFormValues).eggs || []),
|
...((formik.values as RecordingLayingFormValues).eggs || []),
|
||||||
{
|
{
|
||||||
product_warehouse_id: 0,
|
product_warehouse_id: null,
|
||||||
qty: '',
|
qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2185,7 +2185,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
|
if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') {
|
||||||
const layingValues = formik.values as RecordingLayingFormValues;
|
const layingValues = formik.values as RecordingLayingFormValues;
|
||||||
if (!layingValues.eggs || layingValues.eggs.length === 0) {
|
if (!layingValues.eggs || layingValues.eggs.length === 0) {
|
||||||
setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]);
|
setFieldValue('eggs', [{ product_warehouse_id: null, qty: '' }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
}, [isLayingCategory, type, formik.values, setFieldValue]);
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && data.length > 0 && meta && (
|
{!isLoading && data.length > 0 && meta && (
|
||||||
<div className='max-w-sm ml-auto'>
|
<div className='w-full ml-auto'>
|
||||||
<Pagination
|
<Pagination
|
||||||
totalItems={meta.total_results || 0}
|
totalItems={meta.total_results || 0}
|
||||||
itemsPerPage={meta.limit || 0}
|
itemsPerPage={meta.limit || 0}
|
||||||
|
|||||||
@@ -664,7 +664,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && data.length > 0 && meta && (
|
{!isLoading && data.length > 0 && meta && (
|
||||||
<div className='max-w-sm ml-auto'>
|
<div className='w-full ml-auto'>
|
||||||
<Pagination
|
<Pagination
|
||||||
totalItems={meta.total_results || 0}
|
totalItems={meta.total_results || 0}
|
||||||
itemsPerPage={meta.limit || 0}
|
itemsPerPage={meta.limit || 0}
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
icon: 'heroicons-outline:folder',
|
icon: 'heroicons-outline:folder',
|
||||||
permission: [
|
permission: [
|
||||||
'lti.inventory.product_stock.list',
|
'lti.inventory.product_stock.list',
|
||||||
|
'lti.inventory.stock_log.list',
|
||||||
'lti.inventory.product_warehouses.list',
|
'lti.inventory.product_warehouses.list',
|
||||||
'lti.inventory.transfer.list',
|
'lti.inventory.transfer.list',
|
||||||
],
|
],
|
||||||
@@ -204,7 +205,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
{
|
{
|
||||||
text: 'Stok Produk',
|
text: 'Stok Produk',
|
||||||
link: '/inventory/product',
|
link: '/inventory/product',
|
||||||
permission: ['lti.inventory.product_stock.list'],
|
permission: [
|
||||||
|
'lti.inventory.product_stock.list',
|
||||||
|
'lti.inventory.stock_log.list',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Penyesuaian Stok',
|
text: 'Penyesuaian Stok',
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
CreateInventoryAdjustmentPayload,
|
CreateInventoryAdjustmentPayload,
|
||||||
InventoryAdjustment,
|
InventoryAdjustment,
|
||||||
} from '@/types/api/inventory/adjustment';
|
} from '@/types/api/inventory/adjustment';
|
||||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
import { InventoryProduct, StockLog } from '@/types/api/inventory/product';
|
||||||
|
import { httpClient } from '../http/client';
|
||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
|
|
||||||
export const ProductWarehouseApi = new BaseApiService<
|
export const ProductWarehouseApi = new BaseApiService<
|
||||||
ProductWarehouse,
|
ProductWarehouse,
|
||||||
@@ -65,3 +67,41 @@ export const InventoryProductApi = new BaseApiService<
|
|||||||
unknown,
|
unknown,
|
||||||
unknown
|
unknown
|
||||||
>('/inventory/product-stocks');
|
>('/inventory/product-stocks');
|
||||||
|
|
||||||
|
export class StockLogService extends BaseApiService<
|
||||||
|
StockLog,
|
||||||
|
unknown,
|
||||||
|
unknown
|
||||||
|
> {
|
||||||
|
constructor(basePath: string = '/inventory/stock-logs') {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportToExcel(warehouseName: string, initialQueryString: string) {
|
||||||
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('export', 'excel');
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
|
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const fileName = `informasi-stok-produk-${warehouseName.toLowerCase().replaceAll(' ', '-')}-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StockLogApi = new StockLogService('/inventory/stock-logs');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { isResponseError } from '@/lib/api-helper';
|
|
||||||
import { BaseApiService } from '@/services/api/base';
|
import { BaseApiService } from '@/services/api/base';
|
||||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
@@ -11,9 +10,8 @@ import {
|
|||||||
CreateDeliveryOrderPayload,
|
CreateDeliveryOrderPayload,
|
||||||
UpdateDeliveryOrderPayload,
|
UpdateDeliveryOrderPayload,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import * as XLSX from 'xlsx';
|
import { formatDate } from '@/lib/helper';
|
||||||
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 💡 Helper untuk membuat respons dummy
|
* 💡 Helper untuk membuat respons dummy
|
||||||
|
|||||||
@@ -249,6 +249,9 @@ export function useTableFilter<
|
|||||||
const mapKey = useCallback(
|
const mapKey = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
const m = options?.paramMap as Record<string, string> | undefined;
|
const m = options?.paramMap as Record<string, string> | undefined;
|
||||||
|
|
||||||
|
if (key === 'pageSize' && ((m && !m[key]) || !m)) return 'limit';
|
||||||
|
|
||||||
return (m && m[key]) || key;
|
return (m && m[key]) || key;
|
||||||
},
|
},
|
||||||
[options?.paramMap]
|
[options?.paramMap]
|
||||||
|
|||||||
Vendored
+1
@@ -10,6 +10,7 @@ export type BaseExpense = {
|
|||||||
category: 'BOP' | 'NON-BOP';
|
category: 'BOP' | 'NON-BOP';
|
||||||
documents?: {
|
documents?: {
|
||||||
id: number;
|
id: number;
|
||||||
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
}[];
|
}[];
|
||||||
realization_docs?: {
|
realization_docs?: {
|
||||||
|
|||||||
+1
@@ -62,6 +62,7 @@ export type RecordingDepletion = {
|
|||||||
|
|
||||||
export type RecordingStock = {
|
export type RecordingStock = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id: number;
|
||||||
|
qty?: number;
|
||||||
usage_amount?: number;
|
usage_amount?: number;
|
||||||
pending_qty: number;
|
pending_qty: number;
|
||||||
product_warehouse: ProductWarehouse;
|
product_warehouse: ProductWarehouse;
|
||||||
|
|||||||
Reference in New Issue
Block a user