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.
- window.location.reload()}
- >
+ redirectToSSO()}>
Retry
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 = ({
)}
>
- {/*
+
Detail
- */}
-
-
- Detail
-
+
{props.row.original.latest_approval.step_number != 1 && (
<>
- {/*
Deliver
- */}
- {
- if (props.row.original.latest_approval.step_number == 2) {
- deliveryClickHandler?.();
- }
- }}
- variant='ghost'
- color='success'
- className='justify-start text-sm'
- >
-
- Deliver
-
+
>
)}
{props.row.original.latest_approval.step_number != 3 && (
<>
- {/*
+
Edit
- */}
-
-
- Edit
-
+
>
)}
- {/*
+
Delete
- */}
-
-
- Delete
-
+
);
@@ -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 = () => {
}}
/>
- {/*
+
{
Approve
- */}
-
-
- Approve
-
+
- {/*
+
{
Reject
- */}
-
-
- Reject
-
+
{
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 ======