import axios from 'axios'; import { sleep } from '@/lib/helper'; import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse, CreatedUser } from '@/types/api/api-general'; import { CreateExpensePayload, Expense } from '@/types/api/expense'; import { httpClient } from '@/services/http/client'; import { BaseArea } from '@/types/api/master-data/area'; import { BaseLocation, Location } from '@/types/api/master-data/location'; import { Kandang } from '@/types/api/master-data/kandang'; import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { resourceUsage } from 'process'; // Shared base objects const adminUser: CreatedUser = { id: 1, id_user: 1, email: 'admin@example.com', name: 'Admin User', }; const managerAreaUser: CreatedUser = { id: 200, id_user: 200, email: 'manager.area@example.com', name: 'Manager Area', }; const headFinanceUser: CreatedUser = { id: 300, id_user: 300, email: 'head.finance@example.com', name: 'Head Finance', }; const financeStaffUser: CreatedUser = { id: 400, id_user: 400, email: 'finance.staff@example.com', name: 'Finance Staff', }; const auditUser: CreatedUser = { id: 500, id_user: 500, email: 'internal.audit@example.com', name: 'Internal Audit', }; const areaUtara: BaseArea = { id: 1, name: 'Area Utara', }; const areaSelatan: BaseArea = { id: 2, name: 'Area Selatan', }; const baseLocationA: BaseLocation = { id: 1, name: 'Singaparna', address: 'Jl. Kebun Raya 12', area: areaUtara, }; const baseLocationB: BaseLocation = { id: 2, name: 'Cikaum', address: 'Jl. Melati 20', area: areaSelatan, }; const locationA: Location = { ...baseLocationA, created_user: adminUser, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }; const locationB: Location = { ...baseLocationB, created_user: adminUser, created_at: '2025-01-05T00:00:00Z', updated_at: '2025-01-05T00:00:00Z', }; const kandangA1: Kandang = { id: 1, name: 'Singaparna 1', status: 'ACTIVE', capacity: 5000, location: baseLocationA, pic: { id: 101, id_user: 101, email: 'abkA1@example.com', name: 'ABK A1', }, created_user: adminUser, created_at: '2024-12-10T00:00:00Z', updated_at: '2024-12-10T00:00:00Z', }; const kandangA2: Kandang = { id: 2, name: 'Singaparna 2', status: 'ACTIVE', capacity: 4500, location: baseLocationA, pic: { id: 102, id_user: 102, email: 'abkA2@example.com', name: 'ABK A2', }, created_user: adminUser, created_at: '2024-12-12T00:00:00Z', updated_at: '2024-12-12T00:00:00Z', }; const kandangB1: Kandang = { id: 21, name: 'Kandang B1', status: 'ACTIVE', capacity: 3800, location: baseLocationB, pic: { id: 201, id_user: 201, email: 'abkB1@example.com', name: 'ABK B1', }, created_user: adminUser, created_at: '2024-12-15T00:00:00Z', updated_at: '2024-12-15T00:00:00Z', }; const baseSupplierPakan: BaseSupplier = { id: 1, name: 'PT CHAROEN POKPHAND INDONESIA Tbk', alias: 'PPJ', pic: 'Budi', type: 'Pakan', category: 'PAKAN', hatchery: '-', phone: '08121234567', email: 'pakan@example.com', address: 'Jl. Raya Pakan 88', npwp: '1234567890', account_number: '111-222-333', due_date: 30, balance: 5000000, }; const baseSupplierObat: BaseSupplier = { id: 502, name: 'CV Obat Sehat', alias: 'COS', pic: 'Susi', type: 'Obat', category: 'OBAT', hatchery: '-', phone: '085612345678', email: 'cos@example.com', address: 'Jl. Obat Raya 10', npwp: '987654321', account_number: '222-333-444', due_date: 14, }; const supplierPakan: Supplier = { ...baseSupplierPakan, created_user: adminUser, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }; const supplierObat: Supplier = { ...baseSupplierObat, created_user: adminUser, created_at: '2025-01-02T00:00:00Z', updated_at: '2025-01-02T00:00:00Z', }; const nonstockPakanStarter: Nonstock = { id: 3001, name: 'Pakan Ayam Starter', uom_id: 1, uom: { id: 1, name: 'KG' }, suppliers: [baseSupplierPakan], flags: ['PAKAN', 'STARTER'], created_user: adminUser, created_at: '2025-01-10T00:00:00Z', updated_at: '2025-01-10T00:00:00Z', }; const nonstockPakanFinisher: Nonstock = { id: 3002, name: 'Pakan Ayam Finisher', uom_id: 1, uom: { id: 1, name: 'KG' }, suppliers: [baseSupplierPakan], flags: ['PAKAN', 'FINISHER'], created_user: adminUser, created_at: '2025-01-11T00:00:00Z', updated_at: '2025-01-11T00:00:00Z', }; const nonstockObat: Nonstock = { id: 3101, name: 'Obat Antibiotik A', uom_id: 2, uom: { id: 2, name: 'BOTOL' }, suppliers: [baseSupplierObat], flags: ['OBAT'], created_user: adminUser, created_at: '2025-01-12T00:00:00Z', updated_at: '2025-01-12T00:00:00Z', }; const nonstockVitamin: Nonstock = { id: 3102, name: 'Vitamin Ayam B-Complex', uom_id: 2, uom: { id: 2, name: 'BOTOL' }, suppliers: [baseSupplierObat], flags: ['VITAMIN'], created_user: adminUser, created_at: '2025-01-13T00:00:00Z', updated_at: '2025-01-13T00:00:00Z', }; export const DUMMY_EXPENSE: Expense[] = [ // STEP 1 - Pengajuan (OK) { id: 1, reference_number: 'REF-EXP-0001', po_number: 'PO-STEP1-OK', created_user: adminUser, created_at: '2025-02-10T08:00:00Z', updated_at: '2025-02-10T08:00:00Z', location: locationA, transaction_date: '2025-02-10', kandangs: [kandangA1, kandangA2], vendor: supplierPakan, request_documents: [ { name: 'pengajuan_step1_ok.pdf', url: 'https://example.com/pengajuan_step1_ok.pdf', }, ], kandang_expenses: [ { kandang: kandangA1, expenses: [ { nonstock: nonstockPakanStarter, total_quantity: 100, total_expense: 1500000, }, ], }, { kandang: kandangA2, expenses: [ { nonstock: nonstockPakanFinisher, total_quantity: 80, total_expense: 1200000, }, ], }, ], nominal: 2700000, paid: 0, remaining_cost: 2700000, approval: { step_number: 1, step_name: 'Pengajuan', action: 'SUBMITTED', notes: 'Pengajuan pakan untuk kandang A1 dan A2.', action_by: adminUser, action_at: '2025-02-10T08:05:00Z', }, }, // STEP 1 - Pengajuan (REJECTED) { id: 2, reference_number: 'REF-EXP-0002', po_number: 'PO-STEP1-REJECT', created_user: adminUser, created_at: '2025-02-11T09:00:00Z', updated_at: '2025-02-11T09:15:00Z', location: locationA, transaction_date: '2025-02-11', kandangs: [kandangA1], vendor: supplierPakan, request_documents: [], kandang_expenses: [ { kandang: kandangA1, expenses: [ { nonstock: nonstockPakanFinisher, total_quantity: 300, total_expense: 4500000, }, ], }, ], nominal: 4500000, paid: 0, remaining_cost: 4500000, approval: { step_number: 1, step_name: 'Pengajuan', action: 'REJECTED', notes: 'Jumlah terlalu besar untuk pengajuan awal.', action_by: managerAreaUser, action_at: '2025-02-11T09:15:00Z', }, }, // STEP 2 - Approval Manager Area (APPROVED) { id: 3, reference_number: 'REF-EXP-0003', po_number: 'PO-STEP2-OK', created_user: adminUser, created_at: '2025-02-12T07:30:00Z', updated_at: '2025-02-12T08:30:00Z', location: locationA, transaction_date: '2025-02-12', kandangs: [kandangA1, kandangA2], vendor: supplierPakan, request_documents: [ { name: 'pengajuan_step2_ok.pdf', url: 'https://example.com/pengajuan_step2_ok.pdf', }, ], kandang_expenses: [ { kandang: kandangA1, expenses: [ { nonstock: nonstockPakanStarter, total_quantity: 120, total_expense: 1800000, notes: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Non eveniet quos aspernatur magnam mollitia consequatur dolore natus amet libero enim?', }, { nonstock: nonstockPakanFinisher, total_quantity: 50, total_expense: 750000, }, ], }, { kandang: kandangA2, expenses: [ { nonstock: nonstockPakanStarter, total_quantity: 60, total_expense: 900000, }, ], }, ], nominal: 3450000, paid: 0, remaining_cost: 3450000, approval: { step_number: 2, step_name: 'Approval Manager Area', action: 'APPROVED', notes: 'Disetujui, kebutuhan pakan sesuai rencana.', action_by: managerAreaUser, action_at: '2025-02-12T08:30:00Z', }, }, // STEP 2 - Approval Manager Area (REJECTED) { id: 4, reference_number: 'REF-EXP-0004', created_user: adminUser, created_at: '2025-02-13T10:00:00Z', updated_at: '2025-02-13T10:20:00Z', location: locationB, transaction_date: '2025-02-13', kandangs: [kandangB1], vendor: supplierPakan, request_documents: [], kandang_expenses: [ { kandang: kandangB1, expenses: [ { nonstock: nonstockPakanFinisher, total_quantity: 400, total_expense: 6000000, }, ], }, ], nominal: 6000000, paid: 0, remaining_cost: 6000000, approval: { step_number: 2, step_name: 'Approval Manager Area', action: 'REJECTED', notes: 'Tidak sesuai rencana kebutuhan area Selatan.', action_by: managerAreaUser, action_at: '2025-02-13T10:20:00Z', }, }, // STEP 3 - Approval Finance (APPROVED) { id: 5, reference_number: 'REF-EXP-0005', po_number: 'PO-STEP3-OK', created_user: adminUser, created_at: '2025-02-14T09:00:00Z', updated_at: '2025-02-14T09:30:00Z', location: locationB, transaction_date: '2025-02-14', kandangs: [kandangB1], vendor: supplierObat, request_documents: [ { name: 'pengajuan_step3_ok.pdf', url: 'https://example.com/pengajuan_step3_ok.pdf', }, ], kandang_expenses: [ { kandang: kandangB1, expenses: [ { nonstock: nonstockObat, total_quantity: 10, total_expense: 700000, }, { nonstock: nonstockVitamin, total_quantity: 5, total_expense: 250000, }, ], }, ], nominal: 950000, paid: 0, remaining_cost: 950000, approval: { step_number: 3, step_name: 'Approval Finance', action: 'APPROVED', notes: 'Budget tersedia untuk obat & vitamin.', action_by: headFinanceUser, action_at: '2025-02-14T09:30:00Z', }, }, // STEP 3 - Approval Finance (REJECTED) { id: 6, reference_number: 'REF-EXP-0006', created_user: adminUser, created_at: '2025-02-15T11:00:00Z', updated_at: '2025-02-15T11:30:00Z', location: locationB, transaction_date: '2025-02-15', kandangs: [kandangB1], vendor: supplierObat, request_documents: [], kandang_expenses: [ { kandang: kandangB1, expenses: [ { nonstock: nonstockObat, total_quantity: 60, total_expense: 4200000, }, ], }, ], nominal: 4200000, paid: 0, remaining_cost: 4200000, approval: { step_number: 3, step_name: 'Approval Finance', action: 'REJECTED', notes: 'Melebihi plafon budget obat bulan ini.', action_by: headFinanceUser, action_at: '2025-02-15T11:30:00Z', }, }, // STEP 4 - Realisasi (IN_PROGRESS) { id: 7, reference_number: 'REF-EXP-0007', po_number: 'PO-STEP4-OK', created_user: adminUser, created_at: '2025-02-16T08:00:00Z', updated_at: '2025-02-16T12:00:00Z', location: locationA, transaction_date: '2025-02-16', realization_date: '2025-02-17', kandangs: [kandangA1, kandangA2], vendor: supplierPakan, request_documents: [ { name: 'do_step4_ok.pdf', url: 'https://example.com/do_step4_ok.pdf', }, ], kandang_expenses: [ { kandang: kandangA1, expenses: [ { nonstock: nonstockPakanStarter, total_quantity: 70, total_expense: 1050000, }, ], }, { kandang: kandangA2, expenses: [ { nonstock: nonstockPakanFinisher, total_quantity: 40, total_expense: 600000, }, ], }, ], nominal: 1650000, paid: 500000, remaining_cost: 1150000, approval: { step_number: 4, step_name: 'Realisasi', action: 'IN_PROGRESS', notes: 'Barang diterima, pembayaran sebagian.', action_by: financeStaffUser, action_at: '2025-02-16T12:00:00Z', }, }, // STEP 4 - Realisasi (REJECTED) { id: 8, reference_number: 'REF-EXP-0008', created_user: adminUser, created_at: '2025-02-17T09:00:00Z', updated_at: '2025-02-17T09:45:00Z', location: locationA, transaction_date: '2025-02-17', kandangs: [kandangA1], vendor: supplierPakan, request_documents: [ { name: 'invoice_step4_reject.pdf', url: 'https://example.com/invoice_step4_reject.pdf', }, ], kandang_expenses: [ { kandang: kandangA1, expenses: [ { nonstock: nonstockPakanStarter, total_quantity: 50, total_expense: 750000, }, ], }, ], nominal: 750000, paid: 0, remaining_cost: 750000, approval: { step_number: 4, step_name: 'Realisasi', action: 'REJECTED', notes: 'Dokumen realisasi tidak sesuai PO.', action_by: financeStaffUser, action_at: '2025-02-17T09:45:00Z', }, }, // STEP 5 - Selesai (DONE) { id: 9, reference_number: 'REF-EXP-0009', po_number: 'PO-STEP5-OK', created_user: adminUser, created_at: '2025-02-18T08:00:00Z', updated_at: '2025-02-20T15:00:00Z', location: locationB, transaction_date: '2025-02-18', realization_date: '2025-02-19', kandangs: [kandangB1], vendor: supplierObat, request_documents: [ { name: 'invoice_step5_ok.pdf', url: 'https://example.com/invoice_step5_ok.pdf', }, { name: 'bukti_transfer_step5_ok.pdf', url: 'https://example.com/bukti_transfer_step5_ok.pdf', }, ], kandang_expenses: [ { kandang: kandangB1, expenses: [ { nonstock: nonstockObat, total_quantity: 20, total_expense: 1400000, }, ], }, ], nominal: 1400000, paid: 1400000, remaining_cost: 0, approval: { step_number: 5, step_name: 'Selesai', action: 'DONE', notes: 'Proses selesai, sudah lunas.', action_by: financeStaffUser, action_at: '2025-02-20T15:00:00Z', }, }, // STEP 5 - Selesai (REJECTED by audit) { id: 10, reference_number: 'REF-EXP-0010', created_user: adminUser, created_at: '2025-02-19T09:00:00Z', updated_at: '2025-02-21T10:30:00Z', location: locationA, transaction_date: '2025-02-19', kandangs: [kandangA1, kandangA2], vendor: supplierPakan, request_documents: [ { name: 'invoice_step5_recheck.pdf', url: 'https://example.com/invoice_step5_recheck.pdf', }, ], kandang_expenses: [ { kandang: kandangA1, expenses: [ { nonstock: nonstockPakanStarter, total_quantity: 60, total_expense: 900000, }, ], }, { kandang: kandangA2, expenses: [ { nonstock: nonstockPakanFinisher, total_quantity: 40, total_expense: 600000, }, ], }, ], nominal: 1500000, paid: 1500000, remaining_cost: 0, approval: { step_number: 5, step_name: 'Selesai', action: 'REJECTED', notes: 'Dibatalkan saat audit akhir, terdapat perbedaan kuantitas.', action_by: auditUser, action_at: '2025-02-21T10:30:00Z', }, }, ]; export class ExpenseApiService extends BaseApiService< Expense, FormData, FormData > { constructor(basePath: string) { super(basePath); } // TODO: remove dummy data and integrate to real API override async getAllFetcher( endpoint: string ): Promise> { // return await httpClientFetcher>(endpoint); await sleep(750); return { code: 200, status: 'success', message: 'Successfully get all expense data!', meta: { page: 1, limit: 10, total_pages: 1, total_results: 8, }, data: DUMMY_EXPENSE, }; } // TODO: remove this and integrate to real API async getSingle(id: number): Promise | undefined> { await sleep(1000); return { code: 200, status: 'success', message: 'Successfully get expense data!', meta: { page: 1, limit: 10, total_pages: 1, total_results: 8, }, data: DUMMY_EXPENSE[id - 1], }; } // TODO: remove this and integrate to real API async create( payload: FormData ): Promise | undefined> { await sleep(750); const sentPayload = new Map(); for (const pair of payload.entries()) { sentPayload.set(pair[0], pair[1]); } console.log({ sentPayload }); return { code: 200, status: 'success', message: 'Berhasil membuat pengajuan biaya operasional!', data: DUMMY_EXPENSE[0], }; } // TODO: remove this and integrate to real API async update( id: number, payload: FormData ): Promise | undefined> { await sleep(750); const sentPayload = new Map(); for (const pair of payload.entries()) { sentPayload.set(pair[0], pair[1]); } console.log({ sentPayload }); return { code: 200, status: 'success', message: 'Berhasil mengubah pengajuan biaya operasional!', data: DUMMY_EXPENSE[0], }; } // TODO: remove this and integrate to real API async delete(id: number): Promise { await sleep(1000); return { code: 200, status: 'success', message: 'Successfully delete expense data!', data: { id, }, }; } // TODO: remove this and integrate to real API async uploadRequestDocuments( id: number, files: File[] ): Promise { try { // const requestDocumentsFormData = new FormData(); // // request_documents (array of File) // files.forEach((file, index) => { // requestDocumentsFormData.append(`request_documents[${index}]`, file); // }); // const uploadRequestDocumentsRes = await httpClient( // this.basePath, // { // method: 'POST', // body: requestDocumentsFormData, // } // ); // return uploadRequestDocumentsRes; await sleep(1000); return { code: 200, status: 'success', message: 'Berhasil menambahkan dokumen pengajuan!', data: null, }; } catch (error) { if (axios.isAxiosError(error)) { return error.response?.data; } return undefined; } } // TODO: integrate to real API async approve( id: number, notes?: string ): Promise | undefined> { try { // const approveRes = await httpClient>( // `${this.basePath}/approvals`, // { // method: 'POST', // body: { // action: 'APPROVED', // approvable_ids: [id], // notes: notes, // }, // } // ); // // return approveRes; await sleep(1000); return { code: 200, status: 'success', message: 'Successfully approve expense data!', meta: { page: 1, limit: 10, total_pages: 1, total_results: 1, }, data: DUMMY_EXPENSE[id - 1], }; } catch (error) { if (axios.isAxiosError>(error)) { return error.response?.data; } return undefined; } } // TODO: integrate to real API async bulkApprove( ids: number[], notes?: string ): Promise | undefined> { try { // const approveRes = await httpClient>( // `${this.basePath}/approvals`, // { // method: 'POST', // body: { // action: 'APPROVED', // approvable_ids: ids, // notes: notes, // }, // } // ); // // return approveRes; await sleep(1000); return { code: 200, status: 'success', message: 'Successfully bulk approve expense data!', meta: { page: 1, limit: 10, total_pages: 1, total_results: 1, }, data: DUMMY_EXPENSE[ids[0] - 1], }; } catch (error) { if (axios.isAxiosError>(error)) { return error.response?.data; } return undefined; } } // TODO: integrate to real API async reject( id: number, notes?: string ): Promise | undefined> { try { // const rejectRes = await httpClient>( // `${this.basePath}/approvals`, // { // method: 'POST', // body: { // action: 'REJECTED', // approvable_ids: [id], // notes: notes, // }, // } // ); // // return rejectRes; await sleep(1000); return { code: 200, status: 'success', message: 'Successfully reject expense data!', meta: { page: 1, limit: 10, total_pages: 1, total_results: 1, }, data: DUMMY_EXPENSE[id - 1], }; } catch (error) { if (axios.isAxiosError>(error)) { return error.response?.data; } return undefined; } } // TODO: integrate to real API async bulkReject( ids: number[], notes?: string ): Promise | undefined> { try { // const rejectRes = await httpClient>( // `${this.basePath}/approvals`, // { // method: 'POST', // body: { // action: 'REJECTED', // approvable_ids: ids, // notes: notes, // }, // } // ); // // return rejectRes; await sleep(1000); return { code: 200, status: 'success', message: 'Successfully bulk reject expense data!', meta: { page: 1, limit: 10, total_pages: 1, total_results: 1, }, data: DUMMY_EXPENSE[ids[0] - 1], }; } catch (error) { if (axios.isAxiosError>(error)) { return error.response?.data; } return undefined; } } convertPayloadToFormData = (payload: CreateExpensePayload) => { const formData = new FormData(); formData.append('locationId', String(payload.locationId)); formData.append('transaction_date', payload.transaction_date); formData.append('vendorId', String(payload.vendorId)); // kandangIds (array) payload.kandangIds.forEach((id, index) => { formData.append(`kandangIds[${index}]`, String(id)); }); // request_documents (array of File) payload.request_documents.forEach((file, index) => { formData.append(`request_documents[${index}]`, file); }); // kandang_expenses (nested array) payload.kandang_expenses.forEach((kandangExpense, kandangIndex) => { formData.append( `kandang_expenses[${kandangIndex}][kandangId]`, String(kandangExpense.kandangId) ); kandangExpense.expenses.forEach((expenseItem, expenseIndex) => { formData.append( `kandang_expenses[${kandangIndex}][expenses][${expenseIndex}][nonstockId]`, String(expenseItem.nonstockId) ); formData.append( `kandang_expenses[${kandangIndex}][expenses][${expenseIndex}][total_quantity]`, String(expenseItem.total_quantity) ); formData.append( `kandang_expenses[${kandangIndex}][expenses][${expenseIndex}][total_expense]`, String(expenseItem.total_expense) ); formData.append( `kandang_expenses[${kandangIndex}][expenses][${expenseIndex}][notes]`, expenseItem.notes ?? '' ); }); }); return formData; }; } export const ExpenseApi = new ExpenseApiService('/expense');