diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 46730fed..e80a7e02 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,17 @@ stages: - build - deploy +# ========================================================== +# ✅ Global defaults +# ========================================================== +default: + tags: + - server-development-biznet + interruptible: true + +# ========================================================== +# 🏗️ Build Template +# ========================================================== .build_template: &build_template stage: build image: node:20-alpine @@ -39,6 +50,9 @@ stages: - out/ expire_in: 1 week +# ========================================================== +# 🚀 Deploy Template +# ========================================================== .deploy_template: &deploy_template stage: deploy image: @@ -82,11 +96,11 @@ stages: if [ "$STATUS" = "success" ]; then COLOR=3066993 TITLE="✅ Deployment ${ENVIRONMENT_NAME} Succeeded" - DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully." + DESC="Deployment job on branch \${CI_COMMIT_REF_NAME}\ completed successfully." else COLOR=15158332 TITLE="❌ Deployment ${ENVIRONMENT_NAME} Failed" - DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` encountered issues." + DESC="Deployment job on branch \${CI_COMMIT_REF_NAME}\ encountered issues." fi jq -n \ @@ -114,7 +128,9 @@ stages: curl -sS -H "Content-Type: application/json" -d @payload.json "$DISCORD_WEBHOOK_URL" -# ===== DEVELOPMENT (Branch development) ====== +# ========================================================== +# ==== DEVELOPMENT (Branch development) ====== +# ========================================================== build:dev: <<: *build_template rules: @@ -140,7 +156,9 @@ deploy:dev: name: development url: https://dev-lti-erp.mbugroup.id +# ========================================================== # ====== STAGING (Branch staging) ====== +# ========================================================== build:staging: <<: *build_template rules: @@ -165,25 +183,3 @@ deploy:staging: environment: name: staging url: https://stg-lti-erp.mbugroup.id -# ====== PRODUCTION ====== -# build:production: -# <<: *build_template -# rules: -# # pilih salah satu: pakai branch master ATAU pakai tags rilis -# - if: '$CI_COMMIT_BRANCH == "master"' -# # - if: '$CI_COMMIT_TAG' # kalau mau rilis via tag, uncomment ini dan hapus baris di atas -# environment: -# name: production - -# deploy:production: -# <<: *deploy_template -# needs: ["build:production"] -# rules: -# - if: '$CI_COMMIT_BRANCH == "master"' -# # - if: '$CI_COMMIT_TAG' # selaras dengan rule di build:production -# variables: -# S3_BUCKET: "lti-erp.mbugroup.id" -# CLOUDFRONT_DISTRIBUTION_ID: "ddfd" -# environment: -# name: production - diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index aa7f81b2..a4c9f5e0 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -29,8 +29,8 @@ const RequireAuth = ({ children }: RequireAuthProps) => { >('/sso/userinfo', httpClientFetcher, { shouldRetryOnError: false, - // refresh every 13 minutes - refreshInterval: 13 * 60 * 1000, + // refresh every 12 minutes + refreshInterval: 12 * 60 * 1000, }); useEffect(() => { @@ -61,12 +61,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { async () => { await AuthApi.refresh(); }, - 13 * 60 * 1000 + 12 * 60 * 1000 ); return () => clearInterval(interval); }, []); + useEffect(() => { + const refreshUserSession = async () => { + await AuthApi.refresh(); + }; + + refreshUserSession(); + }, []); + if ( (isLoadingUserResponse && !userResponse && !userErrorResponse) || (!userResponse && !userErrorResponse) @@ -78,7 +86,7 @@ const RequireAuth = ({ children }: RequireAuthProps) => { ); } - if (userErrorResponse) { + if (!isLoadingUserResponse && userErrorResponse) { return (

Authentication Failed

@@ -86,10 +94,7 @@ const RequireAuth = ({ children }: RequireAuthProps) => { Please try refreshing the page or contact support if the problem persists.

-
diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index 1ec3c971..22b4d2e2 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -37,88 +37,88 @@ const ClosingSapronakCalculationTable = ({ ): ColumnDef[] => [ { header: 'Tanggal', - accessorKey: 'tanggal', + accessorKey: 'date', cell: (props) => - props.row.original.tanggal - ? formatDate(props.row.original.tanggal, 'DD MMM YYYY') + props.row.original.date + ? formatDate(props.row.original.date, 'DD MMM YYYY') : '-', footer: 'Total', }, { header: 'No. Referensi', - accessorKey: 'no_referensi', - cell: (props) => (props.row.original.no_referensi as string) || '-', + accessorKey: 'reference_number', + cell: (props) => (props.row.original.reference_number as string) || '-', footer: '', }, { header: 'QTY Masuk', - accessorKey: 'qty_masuk', + accessorKey: 'qty_in', cell: (props) => - props.row.original.qty_masuk - ? formatNumber(props.row.original.qty_masuk as number) + props.row.original.qty_in + ? formatNumber(props.row.original.qty_in as number) : '-', footer: total ? () => (
- {total?.qty_masuk ? formatNumber(total?.qty_masuk) : '-'} + {total?.qty_in ? formatNumber(total?.qty_in) : '-'}
) : '', }, { header: 'QTY Keluar', - accessorKey: 'qty_keluar', + accessorKey: 'qty_out', cell: (props) => - props.row.original.qty_keluar - ? formatNumber(props.row.original.qty_keluar as number) + props.row.original.qty_out + ? formatNumber(props.row.original.qty_out as number) : '-', footer: total ? () => (
- {total?.qty_keluar ? formatNumber(total?.qty_keluar) : '-'} + {total?.qty_out ? formatNumber(total?.qty_out) : '-'}
) : '', }, { header: 'QTY Pakai', - accessorKey: 'qty_pakai', + accessorKey: 'qty_used', cell: (props) => - props.row.original.qty_pakai - ? formatNumber(props.row.original.qty_pakai as number) + props.row.original.qty_used + ? formatNumber(props.row.original.qty_used as number) : '-', footer: total ? () => (
- {total?.qty_pakai ? formatNumber(total?.qty_pakai) : '-'} + {total?.qty_used ? formatNumber(total?.qty_used) : '-'}
) : '', }, { header: 'Uraian', - accessorKey: 'uraian', - cell: (props) => (props.row.original.uraian as string) || '-', + accessorKey: 'description', + cell: (props) => (props.row.original.description as string) || '-', footer: '', }, { header: 'Kategori Produk', - accessorKey: 'kategori_produk', - cell: (props) => (props.row.original.kategori_produk as string) || '-', + accessorKey: 'product_category', + cell: (props) => (props.row.original.product_category as string) || '-', footer: '', }, { header: 'Harga Beli/Qty (Rp)', - accessorKey: 'harga_beli_per_qty', + accessorKey: 'unit_price', cell: (props) => - props.row.original.harga_beli_per_qty - ? formatCurrency(props.row.original.harga_beli_per_qty as number) + props.row.original.unit_price + ? formatCurrency(props.row.original.unit_price as number) : '-', footer: total ? () => (
- {total?.harga_beli_per_qty - ? formatCurrency(total?.harga_beli_per_qty) + {total?.avg_unit_price + ? formatCurrency(total?.avg_unit_price) : '-'}
) @@ -126,32 +126,32 @@ const ClosingSapronakCalculationTable = ({ }, { header: 'Total Harga (Rp)', - accessorKey: 'total_harga', + accessorKey: 'total_amount', cell: (props) => - props.row.original.total_harga - ? formatCurrency(props.row.original.total_harga as number) + props.row.original.total_amount + ? formatCurrency(props.row.original.total_amount as number) : '-', footer: total ? () => (
- {total?.total_harga ? formatCurrency(total?.total_harga) : '-'} + {total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
) : '', }, { header: 'Keterangan', - accessorKey: 'keterangan', - cell: (props) => (props.row.original.keterangan as string) || '-', + accessorKey: 'notes', + cell: (props) => (props.row.original.notes as string) || '-', footer: '', }, ]; // Memoize columns untuk setiap kategori - const docBroilerColumns = useMemo( + const docColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.doc_broiler?.total) + ? createColumns(sapronakCalculation.data?.doc?.total) : createColumns(), [sapronakCalculation] ); @@ -172,10 +172,18 @@ const ClosingSapronakCalculationTable = ({ [sapronakCalculation] ); + const pulletColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.pullet?.total) + : createColumns(), + [sapronakCalculation] + ); + return (
data={ isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc_broiler?.rows ?? []) + ? (sapronakCalculation.data?.doc?.rows ?? []) : [] } - columns={docBroilerColumns} + columns={docColumns} className={{ containerClassName: 'my-4', }} @@ -242,6 +250,29 @@ const ClosingSapronakCalculationTable = ({ renderFooter={isResponseSuccess(sapronakCalculation)} /> + + + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.pullet?.rows ?? []) + : [] + } + columns={pulletColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter={isResponseSuccess(sapronakCalculation)} + /> +
); }; diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 0247fc75..507819e3 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -2,7 +2,10 @@ import Button from '@/components/Button'; import CheckboxInput from '@/components/input/CheckboxInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; @@ -28,6 +31,8 @@ import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; import { useAuth } from '@/services/hooks/useAuth'; +import { CustomerApi, ProductApi } from '@/services/api/master-data'; +import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; const RowsOptionsMenu = ({ type = 'dropdown', @@ -52,7 +57,7 @@ const RowsOptionsMenu = ({ )} >
- {/* + - */} - + {props.row.original.latest_approval.step_number != 1 && ( <> - {/* Deliver - */} - + )} {props.row.original.latest_approval.step_number != 3 && ( <> - {/* + - */} - + )} - {/* + - */} - +
); @@ -175,8 +133,6 @@ const RowsOptionsMenu = ({ const MarketingTable = () => { const [search, setSearch] = useState(''); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' @@ -186,22 +142,68 @@ const MarketingTable = () => { const { permissionCheck } = useAuth(); const router = useRouter(); - - const { - data: marketing, - isLoading: isLoadingMarketing, - mutate: refreshMarketing, - } = useSWR(MarketingApi.basePath, MarketingApi.getAllFetcher); - const deleteModal = useModal(); const confirmationModal = useModal(); const productsModal = useModal(); const deliveryModal = useModal(); + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterToQueryString, + } = useTableFilter({ + initial: { + search: '', + product_ids: '', + status: '', + customer_id: '', + page: 1, + limit: 10, + }, + paramMap: { + page: 'page', + pageSize: 'limit', + product_ids: 'product_ids', + status: 'status', + customer_id: 'customer_id', + }, + }); + // ===== FETCH DATA ===== + const { + data: marketing, + isLoading: isLoadingMarketing, + mutate: refreshMarketing, + } = useSWR( + `${MarketingApi.basePath}${getTableFilterToQueryString()}`, + MarketingApi.getAllFetcher + ); + + // ===== OPTIONS ===== + const { + options: productsOptions, + isLoadingOptions: isLoadingProductsOptions, + } = useSelect(ProductApi.basePath, 'id', 'name', '', { + limit: 'limit', + }); + const { + options: customersOptions, + isLoadingOptions: isLoadingCustomersOptions, + } = useSelect(CustomerApi.basePath, 'id', 'name', '', { + limit: 'limit', + }); + const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({ + value: item.step_number, + label: item.step_name, + })); + + // ===== HANDLER ===== const searchChangeHandler = useCallback( (e: React.ChangeEvent) => { setSearch(e.target.value); - setPage(1); + updateFilter('page', 1); + updateFilter('search', e.target.value); }, [] ); @@ -209,7 +211,8 @@ const MarketingTable = () => { (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; setPageSize(newVal.value as number); - setPage(1); + updateFilter('page', 1); + updateFilter('limit', newVal.value as number); }, [] ); @@ -314,20 +317,6 @@ const MarketingTable = () => { ); }; - const { - state: tableFilterState, - updateFilter, - toQueryString: getTableFilterToQueryString, - } = useTableFilter({ - initial: { - search: '', - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); - const getRowCanSelect = (row: Row): boolean => { const approval = row.original.latest_approval; return approval?.step_number === 1 && approval?.action !== 'REJECTED'; @@ -353,7 +342,7 @@ const MarketingTable = () => { }} />
- {/* + - */} - + - {/* + - */} - +
{ label='Product' isClearable placeholder='Pilih product' - options={[]} + options={productsOptions} + isLoading={isLoadingProductsOptions} + value={ + tableFilterState.product_ids + ?.split(',') + .map((id) => + productsOptions.find( + (option) => option.value === Number(id) + ) + ) + .filter( + (option): option is { value: number; label: string } => + option !== undefined + ) ?? null + } + onChange={(value: OptionType | OptionType[] | null) => + updateFilter( + 'product_ids', + (value as OptionType[]) + ?.map((item: OptionType) => item.value.toString()) + .join(',') || '' + ) + } isMulti /> {/* select status */} @@ -414,14 +407,43 @@ const MarketingTable = () => { label='Status' isClearable placeholder='Pilih status' - options={[]} + options={statusOptions} + value={ + tableFilterState.status + ? statusOptions.find( + (option) => + option.value === Number(tableFilterState.status) + ) + : null + } + onChange={(value: OptionType | OptionType[] | null) => + updateFilter( + 'status', + (value as OptionType)?.value.toString() || '' + ) + } /> {/* select customer */} + option.value === Number(tableFilterState.customer_id) + ) + : null + } + onChange={(value: OptionType | OptionType[] | null) => + updateFilter( + 'customer_id', + (value as OptionType)?.value.toString() || '' + ) + } /> @@ -587,8 +609,8 @@ const MarketingTable = () => { }, }, ]} - pageSize={pageSize} - page={page} + pageSize={tableFilterState.pageSize} + page={tableFilterState.page} onPageChange={setPage} className={{ tableWrapperClassName: 'overflow-x-auto min-h-full!', @@ -712,6 +734,7 @@ const MarketingTable = () => { 'px-6 py-3 last:flex last:flex-row last:justify-end', paginationClassName: 'hidden', }} + isLoading={isLoadingMarketing} /> diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 7c7ab0ac..101dbb6d 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -3,7 +3,6 @@ export const ROUTE_PERMISSIONS: Record = { // Dashboard '/dashboard/': ['lti.dashboard.list'], - '/dashboard': ['lti.dashboard.list'], // Production // Production - Project Flock @@ -58,27 +57,14 @@ export const ROUTE_PERMISSIONS: Record = { '/purchase/detail/edit/': ['lti.purchase.update'], // Marketing - '/marketing/': ['lti.dashboard.list', 'lti.marketing.delivery_order.list'], - '/marketing/add/delivery-orders/': [ - 'lti.dashboard.list', - 'lti.marketing.delivery_order.create', - ], - '/marketing/add/sales-orders/': [ - 'lti.dashboard.list', - 'lti.marketing.sales_order.create', - ], - '/marketing/detail/': [ - 'lti.dashboard.list', - 'lti.marketing.delivery_order.detail', - ], + '/marketing/': ['lti.marketing.delivery_order.list'], + '/marketing/add/delivery-orders/': ['lti.marketing.delivery_order.create'], + '/marketing/add/sales-orders/': ['lti.marketing.sales_order.create'], + '/marketing/detail/': ['lti.marketing.delivery_order.detail'], '/marketing/detail/delivery-orders/edit/': [ - 'lti.dashboard.list', 'lti.marketing.delivery_order.update', ], - '/marketing/detail/sales-orders/edit/': [ - 'lti.dashboard.list', - 'lti.marketing.sales_order.update', - ], + '/marketing/detail/sales-orders/edit/': ['lti.marketing.sales_order.update'], // Expense '/expense/': ['lti.expense.list'], @@ -89,19 +75,12 @@ export const ROUTE_PERMISSIONS: Record = { '/expense/realization/edit/': ['lti.expense.update.realization'], // Finance - '/finance/': ['lti.dashboard.list', 'lti.finance.transaction.list'], - '/finance/detail/': ['lti.dashboard.list', 'lti.finance.transaction.detail'], - '/finance/add/': ['lti.dashboard.list', 'lti.finance.payments.create'], - '/finance/detail/edit/': [ - 'lti.dashboard.list', - 'lti.finance.payments.update', - ], - '/finance/add/initial-balance/': [ - 'lti.dashboard.list', - 'lti.finance.initial_balances.create', - ], + '/finance/': ['lti.finance.transaction.list'], + '/finance/detail/': ['lti.finance.transaction.detail'], + '/finance/add/': ['lti.finance.payments.create'], + '/finance/detail/edit/': ['lti.finance.payments.update'], + '/finance/add/initial-balance/': ['lti.finance.initial_balances.create'], '/finance/detail/edit/initial-balance/': [ - 'lti.dashboard.list', 'lti.finance.initial_balances.update', ], '/finance/add/injection/': ['lti.finance.injections.create'], @@ -203,20 +182,14 @@ export const ROUTE_PERMISSIONS: Record = { '/master-data/flock/detail/': ['lti.master.flocks.detail'], '/master-data/flock/detail/edit/': ['lti.master.flocks.update'], - '/master-data/production-standard/': [ - 'lti.dashboard.list', - 'lti.master.production_standards.list', - ], + '/master-data/production-standard/': ['lti.master.production_standards.list'], '/master-data/production-standard/add/': [ - 'lti.dashboard.list', 'lti.master.production_standards.create', ], '/master-data/production-standard/detail/': [ - 'lti.dashboard.list', 'lti.master.production_standards.detail', ], '/master-data/production-standard/detail/edit/': [ - 'lti.dashboard.list', 'lti.master.production_standards.update', ], }; diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index ecdaebb9..f96f1149 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -147,25 +147,25 @@ export type ClosingProductionData = { export type RowSapronakCalculation = { id: number; - tanggal: string; - no_referensi: string; - qty_masuk: number; - qty_keluar: number; - qty_pakai: number; - uraian: string; - kategori_produk: string; - harga_beli_per_qty: number; - total_harga: number; - keterangan: string; + date: string; + reference_number: string; + qty_in: number; + qty_out: number; + qty_used: number; + description: string; + product_category: string; + unit_price: number; + total_amount: number; + notes: string; }; export type TotalSapronakCalculation = { label: string; - qty_masuk: number; - qty_keluar: number; - qty_pakai: number; - harga_beli_per_qty: number; - total_harga: number; + qty_in: number; + qty_out: number; + qty_used: number; + avg_unit_price: number; + total_amount: number; }; export type ClosingSapronakCalculationItem = { @@ -174,9 +174,10 @@ export type ClosingSapronakCalculationItem = { }; export type ClosingSapronakCalculation = { - doc_broiler: ClosingSapronakCalculationItem; + doc: ClosingSapronakCalculationItem; ovk: ClosingSapronakCalculationItem; pakan: ClosingSapronakCalculationItem; + pullet: ClosingSapronakCalculationItem; }; // ====== OVERHEAD ======