Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into dev/randy

This commit is contained in:
randy-ar
2026-01-13 10:47:32 +07:00
33 changed files with 1718 additions and 1514 deletions
+2 -126
View File
@@ -9,7 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@react-pdf/renderer": "^4.3.1",
"@supabase/supabase-js": "^2.89.0",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.12.2",
@@ -3951,86 +3950,6 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@supabase/auth-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz",
"integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz",
"integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz",
"integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz",
"integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz",
"integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.89.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz",
"integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.89.0",
"@supabase/functions-js": "2.89.0",
"@supabase/postgrest-js": "2.89.0",
"@supabase/realtime-js": "2.89.0",
"@supabase/storage-js": "2.89.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -4471,6 +4390,7 @@
"version": "20.19.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -4488,12 +4408,6 @@
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
@@ -4542,15 +4456,6 @@
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@@ -7511,15 +7416,6 @@
"integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==",
"license": "ISC"
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -10717,6 +10613,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unicode-properties": {
@@ -11048,27 +10945,6 @@
"node": ">=0.10.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xlsx": {
"version": "0.20.3",
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
-1
View File
@@ -12,7 +12,6 @@
},
"dependencies": {
"@react-pdf/renderer": "^4.3.1",
"@supabase/supabase-js": "^2.89.0",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.12.2",
@@ -0,0 +1,11 @@
import { MasterConfigurationContent } from '@/figma-make/components/pages/master-data/configuration/MasterConfigurationContent';
const MasterConfigurationPage = () => {
return (
<section className='w-full'>
<MasterConfigurationContent />
</section>
);
};
export default MasterConfigurationPage;
@@ -48,6 +48,13 @@ const ExpenseRealizationContent = ({
const realizationDocumentsChangeHandler = (val: File[]) => {
formik.setFieldTouched('documents', true);
const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024);
if (invalidFiles.length > 0) {
toast.error('Ukuran dokumen maksimal 5 MB!');
return;
}
formik.setFieldValue('documents', val);
};
@@ -251,6 +251,13 @@ const ExpenseRequestContent = ({
const requestDocumentsChangeHandler = (val: File[]) => {
formik.setFieldTouched('documents', true);
const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024);
if (invalidFiles.length > 0) {
toast.error('Ukuran dokumen maksimal 5 MB!');
return;
}
formik.setFieldValue('documents', val);
};
@@ -39,6 +39,10 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
case 5:
expenseStatusPillBadgeColor = 'green';
break;
case 6:
expenseStatusPillBadgeColor = 'green';
break;
}
if (isLatestApprovalRejected) {
@@ -223,6 +223,13 @@ const ExpenseRealizationForm = ({
const realizationDocumentsChangeHandler = (val: File[]) => {
formik.setFieldTouched('documents', true);
const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024);
if (invalidFiles.length > 0) {
toast.error('Ukuran dokumen maksimal 5 MB!');
return;
}
formik.setFieldValue('documents', val);
};
@@ -31,7 +31,10 @@ import { MarketingReportApi } from '@/services/api/report/marketing-report';
import { MARKETING_TYPE_OPTIONS } from '@/config/constant';
import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import { DailyMarketingReport } from '@/types/api/report/marketing';
import {
DailyMarketingReport,
DailyMarketingReportResponse,
} from '@/types/api/report/marketing';
import { isResponseError } from '@/lib/api-helper';
const DailyMarketingReportContent = () => {
@@ -191,9 +194,10 @@ const DailyMarketingReportContent = () => {
const queryString = `?${params.toString()}`;
try {
const dailyMarketingsReport = await httpClient<
BaseApiResponse<DailyMarketingReport>
>(`${MarketingReportApi.basePath}${queryString}`);
const dailyMarketingsReport =
await httpClient<DailyMarketingReportResponse>(
`${MarketingReportApi.basePath}${queryString}`
);
if (isResponseError(dailyMarketingsReport)) {
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
@@ -202,7 +206,10 @@ const DailyMarketingReportContent = () => {
const openPdf = async () => {
const dailyMarketingReportPdfBlob = await pdf(
<DailyMarketingReportPDF data={dailyMarketingsReport.data} />
<DailyMarketingReportPDF
data={dailyMarketingsReport.data}
total={dailyMarketingsReport.total}
/>
).toBlob();
const dailyMarketingReportPdfUrl = URL.createObjectURL(
@@ -213,7 +220,10 @@ const DailyMarketingReportContent = () => {
const downloadPdf = async () => {
const blob = await pdf(
<DailyMarketingReportPDF data={dailyMarketingsReport.data} />
<DailyMarketingReportPDF
data={dailyMarketingsReport.data}
total={dailyMarketingsReport.total}
/>
).toBlob();
const url = URL.createObjectURL(blob);
@@ -9,11 +9,15 @@ import {
View,
} from '@react-pdf/renderer';
import { DailyMarketingReport } from '@/types/api/report/marketing';
import {
DailyMarketingReport,
SalesSummary,
} from '@/types/api/report/marketing';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
interface DailyMarketingReportPDFProps {
data?: DailyMarketingReport;
total?: SalesSummary;
}
const DailyMarketingReportPDFStyle = StyleSheet.create({
@@ -267,9 +271,12 @@ const DailyMarketingReportPDFStyle = StyleSheet.create({
},
});
const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => {
const rows = data?.rows || [];
const summary = data?.summary;
const DailyMarketingReportPDF = ({
data,
total,
}: DailyMarketingReportPDFProps) => {
const rows = data || [];
const summary = total;
return (
<Document>
@@ -409,7 +416,7 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => {
</View>
<View style={DailyMarketingReportPDFStyle.colDoDate}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatDate(row.do_date, 'DD/MM/YYYY')}
{formatDate(row.realization_date, 'DD/MM/YYYY')}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colAging}>
@@ -429,7 +436,7 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => {
</View>
<View style={DailyMarketingReportPDFStyle.colSales}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.sales}
{row.sales.name}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colProduct}>
@@ -518,6 +525,19 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => {
{formatCurrency(summary?.total_sales_amount ?? 0)}
</Text>
</View>
<View
style={[
DailyMarketingReportPDFStyle.summaryRow,
{ borderBottomWidth: 0 },
]}
>
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
Total HPP Per KG:
</Text>
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
{formatCurrency(summary?.total_hpp_price_per_kg ?? 0)}
</Text>
</View>
<View
style={[
DailyMarketingReportPDFStyle.summaryRow,
@@ -60,9 +60,10 @@ const DailyMarketingsTable = ({
footer: 'Total',
},
{
accessorKey: 'do_date',
header: 'Tanggal DO',
cell: (props) => formatDate(props.row.original.do_date, 'DD-MMM-YYYY'),
accessorKey: 'realization_date',
header: 'Tanggal Realisasi',
cell: (props) =>
formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'),
},
{
accessorKey: 'aging_days',
@@ -84,6 +85,7 @@ const DailyMarketingsTable = ({
{
accessorKey: 'sales',
header: 'Sales/Marketing',
cell: (props) => props.row.original.sales.name,
},
{
accessorKey: 'vehicle_number',
@@ -106,10 +108,10 @@ const DailyMarketingsTable = ({
cell: (props) => formatNumber(props.row.original.qty),
footer: () => {
const totalQty = isResponseSuccess(dailyMarketings)
? dailyMarketings.data.summary.total_qty
? dailyMarketings?.total?.total_qty
: 0;
return formatNumber(totalQty);
return totalQty ? formatNumber(totalQty) : '-';
},
},
{
@@ -123,10 +125,10 @@ const DailyMarketingsTable = ({
cell: (props) => formatNumber(props.row.original.total_weight_kg),
footer: () => {
const totalWeightKg = isResponseSuccess(dailyMarketings)
? dailyMarketings.data.summary.total_weight_kg
? dailyMarketings?.total?.total_weight_kg
: 0;
return formatNumber(totalWeightKg);
return totalWeightKg ? formatNumber(totalWeightKg) : '-';
},
},
{
@@ -138,6 +140,13 @@ const DailyMarketingsTable = ({
accessorKey: 'hpp_price_per_kg',
header: 'HPP (Rp)',
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
footer: () => {
const totalHppPricePerKg = isResponseSuccess(dailyMarketings)
? dailyMarketings?.total?.total_hpp_price_per_kg
: 0;
return totalHppPricePerKg ? formatCurrency(totalHppPricePerKg) : '-';
},
},
{
accessorKey: 'sales_amount',
@@ -145,10 +154,10 @@ const DailyMarketingsTable = ({
cell: (props) => formatCurrency(props.row.original.sales_amount),
footer: () => {
const totalSalesAmount = isResponseSuccess(dailyMarketings)
? dailyMarketings.data.summary.total_sales_amount
? dailyMarketings?.total?.total_sales_amount
: 0;
return formatCurrency(totalSalesAmount);
return totalSalesAmount ? formatCurrency(totalSalesAmount) : '-';
},
},
];
@@ -167,7 +176,7 @@ const DailyMarketingsTable = ({
if (!open) {
setOpen(
isResponseSuccess(dailyMarketings)
? dailyMarketings.data.rows.length > 0
? dailyMarketings.data.length > 0
: false
);
}
@@ -215,9 +224,7 @@ const DailyMarketingsTable = ({
<Table<DailyMarketingRow>
data={
isResponseSuccess(dailyMarketings)
? dailyMarketings?.data.rows
: []
isResponseSuccess(dailyMarketings) ? dailyMarketings?.data : []
}
columns={dailyMarketingColumns}
pageSize={pageSize}
@@ -242,7 +249,7 @@ const DailyMarketingsTable = ({
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(dailyMarketings) &&
dailyMarketings?.data?.rows.length === 0,
dailyMarketings?.data?.length === 0,
}),
}}
/>
@@ -161,10 +161,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
{customerReport.customer.name}
</Text>
<Text style={pdfStyles.supplierInfo}>
{customerReport.customer_address || ''}
</Text>
<Text style={pdfStyles.supplierInfo}>
NPWP: {customerReport.customer_npwp || '-'}
{customerReport.customer.address || ''}
</Text>
{customerReport.summary && (
<Text style={pdfStyles.supplierInfo}>
@@ -266,7 +263,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.8 }]}>
<Text>{formatNumber(item.aging)} hari</Text>
<Text>
{item.aging_day ? formatNumber(item.aging_day) : '-'} hari
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.reference || '-'}</Text>
@@ -30,9 +30,11 @@ export const generateCustomerPaymentExcel = (
'Tanggal Realisasi': item.realization_date
? formatDate(item.realization_date, 'DD MMM YYYY')
: '',
Aging: formatNumber(item.aging || 0),
Aging: formatNumber(item.aging_day || 0),
Referensi: item.reference || '',
'Nomor Polisi': item.vehicle_plate || '',
'Nomor Polisi': Array.isArray(item.vehicle_plate)
? item.vehicle_plate.join(', ')
: '',
'Ekor/Qty': formatNumber(item.qty || 0),
'Berat (Kg)': formatNumber(item.weight || 0),
AVG: formatNumber(item.average_weight || 0),
@@ -279,10 +279,14 @@ const CustomerPaymentTab = () => {
{
id: 'aging',
header: 'Aging',
accessorKey: 'aging',
accessorKey: 'aging_day',
cell: (props) => {
const value = props.row.original.aging;
return <div className='text-center'>{formatNumber(value)} hari</div>;
const value = props.row.original.aging_day;
return (
<div className='text-center'>
{value ? formatNumber(value) : '-'} hari
</div>
);
},
},
{
@@ -662,7 +666,7 @@ const CustomerPaymentTab = () => {
<Card
key={customerReport.customer.id}
title={customerReport.customer.name}
subtitle={`NPWP: ${customerReport.customer_npwp || '-'} | ${customerReport.customer_address || ''}\nSaldo Piutang: ${formatCurrency(totalAccountsReceivable)}`}
subtitle={`${customerReport.customer.address || ''}\nSaldo Piutang: ${formatCurrency(totalAccountsReceivable)}`}
className={{ wrapper: 'w-full' }}
variant='bordered'
collapsible={true}
+6
View File
@@ -60,6 +60,12 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
// TODO: add permission
// permission: ['lti.daily_checklist.list'],
},
{
text: 'Konfigurasi',
link: '/daily-checklist/master-data/configuration',
// TODO: add permission
// permission: ['lti.daily_checklist.list'],
},
],
},
],
+1
View File
@@ -20,6 +20,7 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/daily-checklist/reports/': ['lti.dashboard.list'],
'/daily-checklist/master-data/employee/': ['lti.dashboard.list'],
'/daily-checklist/master-data/activity/': ['lti.dashboard.list'],
'/daily-checklist/master-data/configuration/': ['lti.dashboard.list'],
// Production
// Production - Project Flock
@@ -30,7 +30,7 @@ import { KandangApi } from '@/services/api/master-data';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import useSWR from 'swr';
import { BaseApiResponse } from '@/types/api/api-general';
import { BaseApiResponse, Document } from '@/types/api/api-general';
import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { PhaseApi } from '@/services/api/daily-checklist/phase';
@@ -39,6 +39,9 @@ import { Employee } from '@/types/api/daily-checklist/employee';
import { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity';
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link';
import { Icon } from '@iconify/react';
// Static categories
const CATEGORIES = [
@@ -148,6 +151,10 @@ export function DailyChecklistContent() {
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
// Format date for display
const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal';
@@ -340,6 +347,9 @@ export function DailyChecklistContent() {
return;
}
// set existing document
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
// Build assignments map
const assignmentMap: {
[taskId: string]: {
@@ -729,7 +739,11 @@ export function DailyChecklistContent() {
setLoading(true);
try {
const submitRes = await DailyChecklistApi.submit(dailyChecklistId);
const submitRes = await DailyChecklistApi.submit(
dailyChecklistId,
documents,
deletedDocumentIds
);
if (isResponseError(submitRes)) {
console.error('Error submitting:', submitRes.message);
@@ -750,6 +764,19 @@ export function DailyChecklistContent() {
const handleSaveDraft = async () => {
if (!dailyChecklistId) return;
const uploadImageRes = await DailyChecklistApi.uploadImage(
Number(dailyChecklistId),
'DRAFT',
documents,
deletedDocumentIds
);
if (isResponseError(uploadImageRes)) {
console.error('Error saving draft:', uploadImageRes.message);
toast.error('Gagal menyimpan draft');
return;
}
toast.success('Draft tersimpan otomatis');
};
@@ -1263,6 +1290,94 @@ export function DailyChecklistContent() {
</div>
)}
{dailyChecklistId &&
selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 && (
<>
{existingDocuments.length > 0 && (
<div className='mt-6'>
<h3 className='font-semibold text-gray-900 mb-2'>
Dokumen yang telah diupload
</h3>
{existingDocuments.map(
(existingDocument, existingDocumentIdx) => (
<div
key={existingDocumentIdx}
className='w-full flex flex-wrap justify-between'
>
<Link
href={existingDocument.url}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{existingDocument.name}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
<Button
type='button'
variant='ghost'
color='error'
onClick={() => {
setDeletedDocumentIds((prevIds) => [
...prevIds,
existingDocument.id,
]);
setExistingDocuments((prevExistingDocument) => {
const newExistingDocuments = [
...prevExistingDocument,
];
newExistingDocuments.splice(
existingDocumentIdx,
1
);
return newExistingDocuments;
});
}}
className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='fluent:delete-12-regular'
width={20}
height={20}
/>
</Button>
</div>
)
)}
</div>
)}
<DropFileInput
name='Dokumen'
label='Dokumen'
values={documents}
onChange={(files) => {
setDocuments(files);
}}
onDelete={(deletedFileIdx: number) => {
const newRequestDocuments = [...documents];
newRequestDocuments?.splice(deletedFileIdx, 1);
setDocuments(newRequestDocuments);
}}
className={{
wrapper: 'mt-6',
inputWrapper: 'flex items-center',
label: 'font-semibold text-gray-900',
}}
/>
</>
)}
{/* Action Buttons */}
{dailyChecklistId &&
selectedPhaseIds.length > 0 &&
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import {
Card,
CardContent,
@@ -15,7 +15,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge';
import {
Calendar as CalendarIcon,
@@ -35,53 +34,17 @@ import {
ResponsiveContainer,
Cell,
} from 'recharts';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import { toast } from 'sonner';
interface EmployeePerformance {
employee_id: string;
employee_name: string;
kandang_id: string;
kandang_name: string;
total_activities_in_category: number; // Total aktivitas di kategori
completed_activities: number; // Aktivitas yang sudah di-check
completion_rate: number;
last_activity_date: string | null;
color: string; // Color based on kandang
}
interface Kandang {
id: string;
name: string;
}
interface Category {
id: string;
name: string;
}
interface ChecklistKandang {
id: string;
date: string;
kandang_id: string;
category: string;
kandang: {
id: string;
name: string;
} | null;
}
interface AssignmentEmployee {
id: string;
task_id: string;
employee_id: string;
checked: boolean;
updated_at: string;
employee: {
id: string;
name: string;
} | null;
}
import useSWR from 'swr';
import { BaseApiResponse } from '@/types/api/api-general';
import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checklist';
import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { KandangApi } from '@/services/api/master-data';
import { useSelect } from '@/components/input/SelectInput';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate } from '@/lib/helper';
const KANDANG_COLORS = [
'#0069e0', // Blue (primary)
@@ -102,312 +65,65 @@ const CATEGORY_LABELS: { [key: string]: string } = {
};
export function Dashboard() {
const [loading, setLoading] = useState(false);
const [employeePerformance, setEmployeePerformance] = useState<
EmployeePerformance[]
>([]);
// Master data
const [kandangList, setKandangList] = useState<Kandang[]>([]);
const [categoryList, setCategoryList] = useState<Category[]>([]);
// Filters
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [kandangFilter, setKandangFilter] = useState('ALL');
const [categoryFilter, setCategoryFilter] = useState('ALL');
// Color mapping for kandang
const [kandangColorMap, setKandangColorMap] = useState<{
[key: string]: string;
}>({});
useEffect(() => {
fetchMasterData();
}, []);
useEffect(() => {
// Only fetch when date filters are set
if (dateFrom && dateTo) {
fetchEmployeePerformance();
} else {
setEmployeePerformance([]);
const {
data: summaryResponse,
isLoading: isLoadingSummary,
mutate: refreshSummary,
} = useSWR<
BaseApiResponse<DailyChecklistSummary | undefined>,
AxiosError<BaseApiResponse>,
SWRHttpKey
>(
dateFrom && dateTo
? `${DailyChecklistApi.basePath}/summary?date_from=${dateFrom}&date_to=${dateTo}&kandang_id=${kandangFilter === 'ALL' ? '' : kandangFilter}&category=${categoryFilter === 'ALL' ? '' : categoryFilter}`
: '',
httpClientFetcher,
{
keepPreviousData: true,
}
}, [dateFrom, dateTo, kandangFilter, categoryFilter]);
);
const fetchMasterData = async () => {
if (!isSupabaseConfigured()) return;
try {
// Fetch kandang
const { data: kandangData, error: kandangError } = await supabase
.from('kandang')
.select('id, name')
.order('name', { ascending: true });
if (kandangError) {
console.error('Error fetching kandang:', kandangError);
} else {
setKandangList(kandangData || []);
// Create color mapping
const colorMap: { [key: string]: string } = {};
(kandangData || []).forEach((k, index) => {
colorMap[k.id] = KANDANG_COLORS[index % KANDANG_COLORS.length];
});
setKandangColorMap(colorMap);
}
// Set categories from CATEGORY_LABELS (hardcoded list)
const categories: Category[] = Object.keys(CATEGORY_LABELS).map((id) => ({
id,
name: CATEGORY_LABELS[id],
}));
setCategoryList(categories);
} catch (error) {
console.error('Error fetching master data:', error);
}
};
const fetchEmployeePerformance = async () => {
if (!isSupabaseConfigured() || !dateFrom || !dateTo) {
return;
}
try {
setLoading(true);
// Step 1: Get all checklists in date range + filters
let checklistQuery = supabase
.from('daily_checklists')
.select(
`
id,
date,
kandang_id,
category,
kandang:kandang_id (
id,
name
)
`
)
.gte('date', dateFrom)
.lte('date', dateTo);
if (kandangFilter !== 'ALL') {
checklistQuery = checklistQuery.eq('kandang_id', kandangFilter);
}
if (categoryFilter !== 'ALL') {
checklistQuery = checklistQuery.eq('category', categoryFilter);
}
const { data: checklists, error: checklistError } = await checklistQuery;
if (checklistError) {
console.error('Error fetching checklists:', checklistError);
toast.error('Gagal memuat data checklist');
return;
}
if (!checklists || checklists.length === 0) {
setEmployeePerformance([]);
return;
}
const checklistsData = checklists as unknown as ChecklistKandang[];
// Step 2: Get all tasks from these checklists
const checklistIds = checklistsData.map((c) => c.id);
const { data: tasks, error: tasksError } = await supabase
.from('daily_checklist_activity_tasks')
.select('id, checklist_id')
.in('checklist_id', checklistIds);
if (tasksError) {
console.error('Error fetching tasks:', tasksError);
return;
}
if (!tasks || tasks.length === 0) {
setEmployeePerformance([]);
return;
}
const taskIds = tasks.map((t) => t.id);
// Step 3: Get all assignments for these tasks
const { data: assignments, error: assignmentsError } = await supabase
.from('daily_checklist_activity_task_assignments')
.select(
`
id,
task_id,
employee_id,
checked,
updated_at,
employee:employee_id (
id,
name
)
`
)
.in('task_id', taskIds);
if (assignmentsError) {
console.error('Error fetching assignments:', assignmentsError);
return;
}
if (!assignments || assignments.length === 0) {
setEmployeePerformance([]);
return;
}
const assignmentsData = assignments as unknown as AssignmentEmployee[];
// Step 4: Calculate total activities in selected category (if filtered)
let totalActivitiesInCategory = 0;
if (categoryFilter !== 'ALL') {
// Get total activities from master data for this category
const { data: phases } = await supabase
.from('phases')
.select('id')
.eq('category_id', categoryFilter);
if (phases && phases.length > 0) {
const phaseIds = phases.map((p) => p.id);
const { count } = await supabase
.from('activities')
.select('*', { count: 'exact', head: true })
.in('phase_id', phaseIds);
totalActivitiesInCategory = count || 0;
}
}
// Step 5: Group by employee and calculate performance
const employeeMap = new Map<
string,
{
employee_id: string;
employee_name: string;
kandang_id: string;
kandang_name: string;
completed_count: number;
total_count: number;
last_activity_date: string | null;
}
>();
assignmentsData.forEach((assignment) => {
const task = tasks.find((t) => t.id === assignment.task_id);
if (!task) return;
const checklist = checklistsData.find(
(c) => c.id === task.checklist_id
);
if (!checklist) return;
const employeeId = assignment.employee_id;
const employeeName = assignment.employee?.name || 'Unknown';
const kandangId = checklist.kandang_id;
const kandangName = checklist.kandang?.name || 'Unknown';
if (!employeeMap.has(employeeId)) {
employeeMap.set(employeeId, {
employee_id: employeeId,
employee_name: employeeName,
kandang_id: kandangId,
kandang_name: kandangName,
completed_count: 0,
total_count: 0,
last_activity_date: null,
});
}
const empData = employeeMap.get(employeeId)!;
empData.total_count += 1;
if (assignment.checked) {
empData.completed_count += 1;
}
// Update last activity date
if (assignment.updated_at) {
if (
!empData.last_activity_date ||
assignment.updated_at > empData.last_activity_date
) {
empData.last_activity_date = assignment.updated_at;
}
}
});
// Step 6: Convert to array and add calculated fields
const performanceData: EmployeePerformance[] = Array.from(
employeeMap.values()
).map((emp) => {
// Use total activities in category if category is selected, otherwise use employee's assigned count
const totalActivities =
categoryFilter !== 'ALL' && totalActivitiesInCategory > 0
? totalActivitiesInCategory
: emp.total_count;
return {
employee_id: emp.employee_id,
employee_name: emp.employee_name,
kandang_id: emp.kandang_id,
kandang_name: emp.kandang_name,
total_activities_in_category: totalActivities,
completed_activities: emp.completed_count,
completion_rate:
totalActivities > 0
? Math.round((emp.completed_count / totalActivities) * 100)
: 0,
last_activity_date: emp.last_activity_date,
color: kandangColorMap[emp.kandang_id] || '#0069e0',
};
});
// Sort by employee name
performanceData.sort((a, b) =>
a.employee_name.localeCompare(b.employee_name)
);
setEmployeePerformance(performanceData);
} catch (error) {
console.error('Error fetching employee performance:', error);
toast.error('Terjadi kesalahan saat memuat data');
} finally {
setLoading(false);
}
};
const formatDate = (dateString: string | null) => {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
});
};
const hasFilters = dateFrom && dateTo;
const kandangColorMap: { [key: string]: string } = {};
(kandangOptions || []).forEach((k, index) => {
kandangColorMap[k.value] = KANDANG_COLORS[index % KANDANG_COLORS.length];
});
// Prepare chart data
const chartData = employeePerformance.map((emp) => ({
const employeePerformance = isResponseSuccess(summaryResponse)
? summaryResponse.data?.tracking_abk.map((abk) => {
return {
...abk,
color: kandangColorMap[abk.kandang_id] || '#0069e0',
};
})
: [];
const chartData = employeePerformance?.map((emp) => ({
name: emp.employee_name,
completed: emp.completed_activities,
remaining: emp.total_activities_in_category - emp.completed_activities,
total: emp.total_activities_in_category,
completed: emp.activity_done,
remaining: emp.activity_left,
total: emp.total_activity,
color: emp.color,
kandang: emp.kandang_name,
}));
const hasFilters = dateFrom && dateTo;
if (summaryResponse && isResponseError(summaryResponse)) {
toast.error('Gagal memuat data: ' + summaryResponse.message);
}
return (
<div className='min-h-screen'>
<div className='p-6'>
@@ -457,9 +173,12 @@ export function Dashboard() {
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}>
{kandang.name}
{kandangOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
@@ -482,9 +201,9 @@ export function Dashboard() {
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Kategori</SelectItem>
{categoryList.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
{Object.keys(CATEGORY_LABELS).map((category) => (
<SelectItem key={category} value={category}>
{CATEGORY_LABELS[category]}
</SelectItem>
))}
</SelectContent>
@@ -523,11 +242,11 @@ export function Dashboard() {
melihat performance ABK.
</p>
</div>
) : loading ? (
) : isLoadingSummary ? (
<div className='text-center py-16 text-gray-500'>
Memuat data...
</div>
) : employeePerformance.length === 0 ? (
) : employeePerformance && employeePerformance.length === 0 ? (
<div className='flex flex-col items-center justify-center py-16 text-center'>
<Users className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
@@ -582,7 +301,7 @@ export function Dashboard() {
fill='#10B981'
radius={[0, 0, 0, 0]}
>
{chartData.map((entry, index) => (
{chartData?.map((entry, index) => (
<Cell
key={`cell-completed-${index}`}
fill={entry.color}
@@ -595,7 +314,7 @@ export function Dashboard() {
fill='#E5E7EB'
radius={[4, 4, 0, 0]}
>
{chartData.map((entry, index) => (
{chartData?.map((entry, index) => (
<Cell
key={`cell-remaining-${index}`}
fill={`${entry.color}33`}
@@ -610,102 +329,103 @@ export function Dashboard() {
</Card>
{/* Employee Tracking Table */}
{hasFilters && employeePerformance.length > 0 && (
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardHeader>
<CardTitle className='text-lg'>Tracking ABK</CardTitle>
<p className='text-sm text-gray-500 mt-1'>
Detail performance masing-masing ABK
</p>
</CardHeader>
<CardContent>
<div className='overflow-x-auto'>
<table className='w-full border border-gray-200 rounded-lg'>
<thead>
<tr className='bg-gray-50 border-b border-gray-200'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Nama ABK
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Total Aktivitas
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas Selesai
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas Tersisa
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Completion Rate
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Last Activity
</th>
</tr>
</thead>
<tbody>
{employeePerformance.map((emp, index) => (
<tr
key={emp.employee_id}
className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
}
>
<td className='py-3 px-4 text-sm text-gray-900 font-medium'>
{emp.employee_name}
</td>
<td className='py-3 px-4'>
<Badge
style={{
backgroundColor: `${emp.color}15`,
color: emp.color,
borderColor: `${emp.color}30`,
}}
className='border'
>
{emp.kandang_name}
</Badge>
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{emp.total_activities_in_category}
</td>
<td className='py-3 px-4 text-center text-sm font-semibold text-green-700'>
{emp.completed_activities}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-600'>
{emp.total_activities_in_category -
emp.completed_activities}
</td>
<td className='py-3 px-4 text-center'>
<div className='flex items-center justify-center gap-2'>
<div className='w-24 bg-gray-200 rounded-full h-2'>
<div
className='h-2 rounded-full transition-all'
style={{
width: `${emp.completion_rate}%`,
backgroundColor: emp.color,
}}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{emp.completion_rate}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDate(emp.last_activity_date)}
</td>
{hasFilters &&
employeePerformance &&
employeePerformance.length > 0 && (
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardHeader>
<CardTitle className='text-lg'>Tracking ABK</CardTitle>
<p className='text-sm text-gray-500 mt-1'>
Detail performance masing-masing ABK
</p>
</CardHeader>
<CardContent>
<div className='overflow-x-auto'>
<table className='w-full border border-gray-200 rounded-lg'>
<thead>
<tr className='bg-gray-50 border-b border-gray-200'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Nama ABK
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Total Aktivitas
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas Selesai
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas Tersisa
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Completion Rate
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Last Activity
</th>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</thead>
<tbody>
{employeePerformance?.map((emp, index) => (
<tr
key={emp.employee_id}
className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
}
>
<td className='py-3 px-4 text-sm text-gray-900 font-medium'>
{emp.employee_name}
</td>
<td className='py-3 px-4'>
<Badge
style={{
backgroundColor: `${emp.color}15`,
color: emp.color,
borderColor: `${emp.color}30`,
}}
className='border'
>
{emp.kandang_name}
</Badge>
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{emp.total_activity}
</td>
<td className='py-3 px-4 text-center text-sm font-semibold text-green-700'>
{emp.activity_done}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-600'>
{emp.activity_left}
</td>
<td className='py-3 px-4 text-center'>
<div className='flex items-center justify-center gap-2'>
<div className='w-24 bg-gray-200 rounded-full h-2'>
<div
className='h-2 rounded-full transition-all'
style={{
width: `${emp.completion_rate}%`,
backgroundColor: emp.color,
}}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{emp.completion_rate}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDate(emp.last_activity, 'DD MMM YYYY')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</div>
</div>
);
@@ -20,6 +20,9 @@ import { toast } from 'sonner';
import { useRouter, useSearchParams } from 'next/navigation';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError } from '@/lib/api-helper';
import Link from 'next/link';
import { Icon } from '@iconify/react';
import { Document } from '@/types/api/api-general';
interface ChecklistDetailRow {
checklist_id: string;
@@ -125,6 +128,7 @@ export function DetailDailyChecklistContent() {
const [employees, setEmployees] = useState<{ id: string; name: string }[]>(
[]
);
const [documents, setDocuments] = useState<Document[]>([]);
// Modals
const [showApproveModal, setShowApproveModal] = useState(false);
@@ -160,6 +164,8 @@ export function DetailDailyChecklistContent() {
const rawDetailChecklist = checklistDataRes?.data;
setDocuments(rawDetailChecklist?.document_urls || []);
const checklistData = {
id: rawDetailChecklist?.id,
date: rawDetailChecklist?.date,
@@ -842,6 +848,37 @@ export function DetailDailyChecklistContent() {
Tidak ada data aktivitas
</div>
)}
{documents.length > 0 && (
<div className='mt-6'>
<h3 className='font-semibold text-gray-900 mb-2'>
Dokumen yang telah diupload
</h3>
<ul className='list-disc pl-4'>
{documents.map((existingDocument, existingDocumentIdx) => (
<li key={existingDocumentIdx}>
<div className='w-full flex flex-wrap justify-between'>
<Link
href={existingDocument.url}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{existingDocument.name}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
</div>
</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
</div>
@@ -328,6 +328,7 @@ export function MasterAktivitasContent() {
return;
}
refreshPhases();
refreshPhaseActivities();
toast.success('Aktivitas berhasil ditambahkan');
} else {
@@ -349,6 +350,7 @@ export function MasterAktivitasContent() {
return;
}
refreshPhases();
refreshPhaseActivities();
toast.success('Aktivitas berhasil diubah');
}
@@ -387,6 +389,7 @@ export function MasterAktivitasContent() {
return;
}
refreshPhases();
refreshPhaseActivities();
toast.success('Aktivitas berhasil dihapus');
setShowActivityDeleteConfirm(false);
@@ -0,0 +1,564 @@
'use client';
import { useState } from 'react';
import { Plus, MoreVertical, Pencil, Trash2 } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/figma-make/components/base/alert-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner';
import useSWR from 'swr';
import Table from '@/components/Table';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn, formatDate } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { DailyChecklistConfiguration } from '@/types/api/daily-checklist/configuration';
import { DailyChecklistConfigurationApi } from '@/services/api/daily-checklist/configuration';
import { DatePicker } from '@/figma-make/components/base/date-picker';
export function MasterConfigurationContent() {
const {
state: tableFilterState,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
});
const {
data: dailyChecklistConfigurations,
isLoading: isLoadingDailyChecklistConfigurations,
mutate: refreshDailyChecklistConfigurations,
} = useSWR(
`${DailyChecklistConfigurationApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistConfigurationApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [configurationToDelete, setConfigurationToDelete] = useState<
number | null
>(null);
const [loading, setLoading] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [configurationForm, setConfigurationForm] = useState({
id: 0,
date: '',
percentage_threshold_bad: '',
percentage_threshold_enough: '',
});
const configurationColumns: ColumnDef<DailyChecklistConfiguration>[] = [
{
id: 'date',
header: 'Tanggal',
accessorKey: 'date',
enableSorting: false,
cell: ({ row }) => formatDate(row.original.date, 'DD MMM YYYY'),
},
{
id: 'percentage_threshold_bad',
header: 'Threshold Bad',
accessorKey: 'percentage_threshold_bad',
enableSorting: false,
cell: ({ row }) => `${row.original.percentage_threshold_bad}%`,
},
{
id: 'percentage_threshold_enough',
header: 'Threshold Enough',
accessorKey: 'percentage_threshold_enough',
enableSorting: false,
cell: ({ row }) => `${row.original.percentage_threshold_enough}%`,
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-8 w-8 p-0 hover:bg-gray-100'
>
<MoreVertical className='h-4 w-4 text-gray-600' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
<Pencil className='mr-2 h-4 w-4' />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(row.original.id)}
className='text-red-600'
>
<Trash2 className='mr-2 h-4 w-4' />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const handleAdd = () => {
setModalMode('create');
setConfigurationForm({
id: 0,
date: '',
percentage_threshold_bad: '',
percentage_threshold_enough: '',
});
setShowModal(true);
};
const handleEdit = (configuration: DailyChecklistConfiguration) => {
setModalMode('edit');
setConfigurationForm({
id: configuration.id,
date: configuration.date,
percentage_threshold_bad: String(configuration.percentage_threshold_bad),
percentage_threshold_enough: String(
configuration.percentage_threshold_enough
),
});
setShowModal(true);
};
const handleSave = async () => {
if (
!configurationForm.date.trim() ||
Number(configurationForm.percentage_threshold_bad) === 0 ||
Number(configurationForm.percentage_threshold_enough) === 0
) {
toast.error('Tanggal dan persentase harus diisi');
return;
}
setLoading(true);
try {
if (modalMode === 'create') {
const createConfigurationResponse =
await DailyChecklistConfigurationApi.create({
date: formatDate(configurationForm.date, 'YYYY-MM-DD'),
percentage_threshold_bad: Number(
configurationForm.percentage_threshold_bad
),
percentage_threshold_enough: Number(
configurationForm.percentage_threshold_enough
),
});
if (isResponseError(createConfigurationResponse)) {
console.error(
'Error creating configuration:',
createConfigurationResponse.message
);
toast.error('Gagal menambahkan konfigurasi');
return;
}
refreshDailyChecklistConfigurations();
toast.success('Konfigurasi berhasil ditambahkan');
} else {
const updateConfigurationResponse =
await DailyChecklistConfigurationApi.update(configurationForm.id, {
date: formatDate(configurationForm.date, 'YYYY-MM-DD'),
percentage_threshold_bad: Number(
configurationForm.percentage_threshold_bad
),
percentage_threshold_enough: Number(
configurationForm.percentage_threshold_enough
),
});
if (isResponseError(updateConfigurationResponse)) {
console.error(
'Error updating configuration:',
updateConfigurationResponse.message
);
toast.error('Gagal mengubah konfigurasi');
return;
}
refreshDailyChecklistConfigurations();
toast.success('Konfigurasi berhasil diubah');
}
setShowModal(false);
setConfigurationForm({
id: 0,
date: '',
percentage_threshold_bad: '',
percentage_threshold_enough: '',
});
} catch (error) {
console.error('Error saving configuration:', error);
toast.error('Terjadi kesalahan saat menyimpan konfigurasi');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (configurationId: number) => {
setConfigurationToDelete(configurationId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!configurationToDelete) return;
setLoading(true);
try {
const deleteConfigurationResponse =
await DailyChecklistConfigurationApi.delete(configurationToDelete);
if (isResponseError(deleteConfigurationResponse)) {
console.error(
'Error deleting configuration:',
deleteConfigurationResponse.message
);
toast.error('Gagal menghapus konfigurasi');
return;
}
refreshDailyChecklistConfigurations();
toast.success('Konfigurasi berhasil dihapus');
setShowDeleteConfirm(false);
setConfigurationToDelete(null);
} catch (error) {
console.error('Error deleting employee:', error);
toast.error('Terjadi kesalahan saat menghapus konfigurasi');
} finally {
setLoading(false);
}
};
const handleExport = (format: string) => {
toast.success(`Data berhasil diekspor ke ${format}`);
};
if (isLoadingDailyChecklistConfigurations && !dailyChecklistConfigurations) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Konfigurasi
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Konfigurasi</span>
</p>
</div>
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-12 text-center text-gray-500'>
Memuat data...
</CardContent>
</Card>
</div>
</div>
);
}
const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal';
const [year, month, day] = dateStr.split('-');
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return date.toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Konfigurasi
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Konfigurasi</span>
</p>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-0'>
{/* Single Toolbar Row */}
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
<div className='flex items-center gap-2 flex-wrap'>
<Button
onClick={handleAdd}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Plus className='w-4 h-4 mr-2' />
Tambah Konfigurasi
</Button>
</div>
</div>
{/* Table */}
<Table<DailyChecklistConfiguration>
data={
isResponseSuccess(dailyChecklistConfigurations)
? dailyChecklistConfigurations?.data
: []
}
columns={configurationColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(dailyChecklistConfigurations)
? dailyChecklistConfigurations?.meta?.page
: 0
}
totalItems={
isResponseSuccess(dailyChecklistConfigurations)
? dailyChecklistConfigurations?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingDailyChecklistConfigurations}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(dailyChecklistConfigurations) &&
dailyChecklistConfigurations?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
{/* Add/Edit Modal */}
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>
{modalMode === 'create'
? 'Tambah Konfigurasi'
: 'Edit Konfigurasi'}
</DialogTitle>
<DialogDescription>
{modalMode === 'create'
? 'Masukkan detail konfigurasi baru'
: 'Ubah detail konfigurasi'}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div>
<Label htmlFor='date'>
Tanggal Efektif <span className='text-red-500'>*</span>
</Label>
<div className='mt-1.5'>
<DatePicker
date={configurationForm.date}
onDateChange={(e) =>
setConfigurationForm({
...configurationForm,
date: e,
})
}
disabled={loading}
placeholder='Pilih tanggal'
formatDisplay={formatDateForDisplay}
/>
</div>
</div>
<div>
<Label>
Threshold <span className='text-red-500'>*</span>
</Label>
</div>
<div className='flex flex-row items-center justify-between gap-2 max-w-64'>
<Label htmlFor='thresholdBad'>
Kurang <span className='text-red-500'>*</span>
</Label>
<div className='flex flex-row items-center gap-1'>
<Input
id='thresholdBadGround'
value={0}
disabled
className='w-16'
/>
<span>{'<='}</span>
<Input
type='number'
id='percentageThresholdBad'
value={configurationForm.percentage_threshold_bad}
onChange={(e) =>
setConfigurationForm({
...configurationForm,
percentage_threshold_bad: e.target.value,
})
}
placeholder='Kurang'
className='w-20'
disabled={loading}
max={100}
/>
</div>
</div>
<div className='flex flex-row items-center justify-between gap-2 max-w-64'>
<Label htmlFor='thresholdEnough'>
Cukup <span className='text-red-500'>*</span>
</Label>
<div className='flex flex-row items-center gap-1'>
<Input
id='thresholdEnoughGround'
value={Number(configurationForm.percentage_threshold_bad) + 1}
disabled
className='w-16'
/>
<span>{'<='}</span>
<Input
type='number'
id='percentageThresholdEnough'
value={configurationForm.percentage_threshold_enough}
onChange={(e) =>
setConfigurationForm({
...configurationForm,
percentage_threshold_enough: e.target.value,
})
}
placeholder='Cukup'
className='w-20'
disabled={loading}
min={Number(configurationForm.percentage_threshold_bad) + 1}
max={100}
/>
</div>
</div>
<div className='flex flex-row items-center justify-between gap-2 max-w-64'>
<Label htmlFor='thresholdGood'>
Baik <span className='text-red-500'>*</span>
</Label>
<div className='flex flex-row items-center gap-1'>
<Input
id='thresholdGoodGround'
value={
Number(configurationForm.percentage_threshold_enough) + 1
}
disabled
className='w-16'
/>
<span>{'<='}</span>
<Input
type='number'
id='percentageThresholdGood'
value={100}
placeholder='Good'
className='w-20'
disabled
/>
</div>
</div>
</div>
<DialogFooter>
<Button
variant='outline'
onClick={() => setShowModal(false)}
disabled={loading}
>
Batal
</Button>
<Button
onClick={handleSave}
disabled={loading}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
{loading ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle>Hapus konfigurasi?</AlertDialogTitle>
<AlertDialogDescription>
Data konfigurasi akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={loading}
className='bg-red-600 hover:bg-red-700 text-white'
>
{loading ? 'Menghapus...' : 'Hapus'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -283,10 +283,6 @@ export function MasterEmployeeContent() {
}
};
const handleExport = (format: string) => {
toast.success(`Data berhasil diekspor ke ${format}`);
};
if (isLoadingEmployees && !employees) {
return (
<div className='min-h-screen'>
@@ -390,27 +386,6 @@ export function MasterEmployeeContent() {
{/* RIGHT: Export + Add */}
<div className='flex items-center gap-2 flex-wrap'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
className='border-gray-200 text-gray-700'
>
<Download className='w-4 h-4 mr-2' />
Export
<ChevronDown className='w-4 h-4 ml-2' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleExport('CSV')}>
Export CSV
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport('Excel')}>
Export Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
onClick={handleAdd}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
@@ -1,13 +1,9 @@
'use client';
import { useState, useEffect } from 'react';
import { Eye, Download, Search } from 'lucide-react';
import { useMemo } from 'react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge';
import { Input } from '@/figma-make/components/base/input';
import { Label } from '@/figma-make/components/base/label';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import {
Select,
SelectContent,
@@ -16,357 +12,291 @@ import {
SelectValue,
} from '@/figma-make/components/base/select';
import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
import { useRouter } from 'next/navigation';
import { useSelect } from '@/components/input/SelectInput';
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import useSWR from 'swr';
import { BaseApiResponse } from '@/types/api/api-general';
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { cn } from '@/lib/helper';
import { ColumnDef } from '@tanstack/react-table';
import { report } from 'process';
import { PhaseApi } from '@/services/api/daily-checklist/phase';
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import { Button } from '@/figma-make/components/base/button';
import { Download } from 'lucide-react';
interface SubmissionReportItem {
checklist_id: string;
date: string;
kandang_id: string;
kandang_name: string;
category: string;
status: string;
progress_percent: number;
total_phases: number;
total_activities: number;
total_employees: number;
updated_at: string;
}
interface Kandang {
id: string;
name: string;
}
interface ReportQueryResult {
id: string;
date: string;
kandang_id: string;
category: string;
status: string;
updated_at: string;
kandang: {
id: string;
name: string;
} | null;
}
const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' },
{ value: 'DRAFT', label: 'Draft' },
{ value: 'SUBMITTED', label: 'Submitted' },
{ value: 'APPROVED', label: 'Approved' },
{ value: 'REJECTED', label: 'Rejected' },
const MONTH_OPTIONS = [
{ value: '1', label: 'Januari' },
{ value: '2', label: 'Februari' },
{ value: '3', label: 'Maret' },
{ value: '4', label: 'April' },
{ value: '5', label: 'Mei' },
{ value: '6', label: 'Juni' },
{ value: '7', label: 'Juli' },
{ value: '8', label: 'Agustus' },
{ value: '9', label: 'September' },
{ value: '10', label: 'Oktober' },
{ value: '11', label: 'November' },
{ value: '12', label: 'Desember' },
];
const CATEGORY_LABELS: { [key: string]: string } = {
pullet_open: 'Pullet Open',
pullet_close: 'Pullet Close',
produksi_open: 'Produksi Open',
produksi_close: 'Produksi Close',
};
const YEAR_OPTIONS = [
{ value: '2027', label: '2027' },
{ value: '2026', label: '2026' },
{ value: '2025', label: '2025' },
{ value: '2024', label: '2024' },
{ value: '2023', label: '2023' },
{ value: '2022', label: '2022' },
{ value: '2021', label: '2021' },
{ value: '2020', label: '2020' },
];
// const CATEGORY_LABELS: { [key: string]: string } = {
// pullet_open: 'Pullet Open',
// pullet_close: 'Pullet Close',
// produksi_open: 'Produksi Open',
// produksi_close: 'Produksi Close',
// };
export function DailyChecklistReportsContent() {
const router = useRouter();
const [loading, setLoading] = useState(true);
// Report State
const [reportList, setReportList] = useState<SubmissionReportItem[]>([]);
const [filteredReportList, setFilteredReportList] = useState<
SubmissionReportItem[]
>([]);
const currentMonth = useMemo(() => new Date().getMonth() + 1, []);
const currentYear = useMemo(() => new Date().getFullYear(), []);
// Master data
const [kandangList, setKandangList] = useState<Kandang[]>([]);
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
bulan: currentMonth.toString(),
tahun: currentYear.toString(),
area_id: '',
location_id: '',
kandang_id: '',
employee_id: '',
phase_id: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
bulan: 'bulan',
tahun: 'tahun',
area_id: 'area_id',
location_id: 'location_id',
kandang_id: 'kandang_id',
employee_id: 'employee_id',
phase_id: 'phase_id',
},
});
// Filters
const [statusFilter, setStatusFilter] = useState('ALL');
const [kandangFilter, setKandangFilter] = useState('ALL');
const [searchText, setSearchText] = useState('');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
useEffect(() => {
fetchKandangList();
fetchReports();
}, []);
useEffect(() => {
applyFilters();
}, [reportList, statusFilter, kandangFilter, searchText, dateFrom, dateTo]);
const fetchKandangList = async () => {
if (!isSupabaseConfigured()) return;
try {
const { data, error } = await supabase
.from('kandang')
.select('id, name')
.order('name', { ascending: true });
if (error) {
console.error('Error fetching kandang:', error);
return;
}
setKandangList(data || []);
} catch (error) {
console.error('Error fetching kandang:', error);
const {
data: reportResponse,
isLoading: isLoadingReport,
mutate: refreshReport,
} = useSWR<
BaseApiResponse<DailyChecklistReport[] | undefined>,
AxiosError<BaseApiResponse>,
SWRHttpKey
>(
`${DailyChecklistApi.basePath}/report${getTableFilterQueryString()}`,
httpClientFetcher,
{
keepPreviousData: true,
}
};
);
const fetchReports = async () => {
if (!isSupabaseConfigured()) {
console.warn('Supabase not configured');
setLoading(false);
return;
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
AreaApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
try {
setLoading(true);
// Fetch checklists directly from daily_checklists table
const { data: checklists, error } = await supabase
.from('daily_checklists')
.select(
`
id,
date,
kandang_id,
category,
status,
updated_at,
kandang:kandang_id (
id,
name
)
`
)
.order('date', { ascending: false })
.order('updated_at', { ascending: false });
if (error) {
console.error('Error fetching reports:', error);
toast.error('Gagal memuat data reports');
return;
}
// Enrich data with calculations
const enrichedData = await Promise.all(
((checklists as unknown as ReportQueryResult[]) || [])
.filter((checklist) => checklist.id)
.map(async (checklist) => {
// Count phases
const { count: phaseCount } = await supabase
.from('daily_checklist_phases')
.select('*', { count: 'exact', head: true })
.eq('checklist_id', checklist.id);
// Count activities (tasks)
const { count: activityCount } = await supabase
.from('daily_checklist_activity_tasks')
.select('*', { count: 'exact', head: true })
.eq('checklist_id', checklist.id);
// Count unique employees
const { data: tasks } = await supabase
.from('daily_checklist_activity_tasks')
.select('id')
.eq('checklist_id', checklist.id);
const taskIds = (tasks || []).map((t) => t.id);
let uniqueEmployees = new Set<string>();
if (taskIds.length > 0) {
const { data: assignments } = await supabase
.from('daily_checklist_activity_task_assignments')
.select('employee_id')
.in('task_id', taskIds);
uniqueEmployees = new Set(
(assignments || []).map((a) => a.employee_id)
);
}
// ✅ Calculate progress based on phase coverage
const { count: totalPhasesInMaster } = await supabase
.from('phases')
.select('*', { count: 'exact', head: true })
.eq('category_id', checklist.category);
const { data: checklistTasks } = await supabase
.from('daily_checklist_activity_tasks')
.select('id, phase_id')
.eq('checklist_id', checklist.id);
const checklistTaskIds = (checklistTasks || []).map((t) => t.id);
const uniquePhasesWithChecked = new Set<string>();
if (checklistTaskIds.length > 0) {
const { data: checkedAssignments } = await supabase
.from('daily_checklist_activity_task_assignments')
.select('task_id')
.in('task_id', checklistTaskIds)
.eq('checked', true);
if (checkedAssignments && checkedAssignments.length > 0) {
const checkedTaskIds = new Set(
checkedAssignments.map((a) => a.task_id)
);
checklistTasks?.forEach((task) => {
if (checkedTaskIds.has(task.id)) {
uniquePhasesWithChecked.add(task.phase_id);
}
});
}
}
const phasesWithCheckedCount = uniquePhasesWithChecked.size;
const progressPercent =
totalPhasesInMaster && totalPhasesInMaster > 0
? Math.round(
(phasesWithCheckedCount / totalPhasesInMaster) * 100
)
: 0;
return {
checklist_id: checklist.id,
date: checklist.date,
kandang_id: checklist.kandang_id,
kandang_name: checklist.kandang?.name || '-',
category: checklist.category,
status: checklist.status,
progress_percent: progressPercent,
total_phases: phaseCount || 0,
total_activities: activityCount || 0,
total_employees: uniqueEmployees.size,
updated_at: checklist.updated_at,
};
})
);
setReportList(enrichedData);
} catch (error) {
console.error('Error fetching reports:', error);
toast.error('Terjadi kesalahan');
} finally {
setLoading(false);
}
};
const applyFilters = () => {
let filtered = [...reportList];
if (statusFilter && statusFilter !== 'ALL') {
filtered = filtered.filter((item) => item.status === statusFilter);
}
if (kandangFilter && kandangFilter !== 'ALL') {
filtered = filtered.filter((item) => item.kandang_id === kandangFilter);
}
if (searchText) {
filtered = filtered.filter(
(item) =>
item.kandang_name.toLowerCase().includes(searchText.toLowerCase()) ||
item.category.toLowerCase().includes(searchText.toLowerCase()) ||
(CATEGORY_LABELS[item.category] || '')
.toLowerCase()
.includes(searchText.toLowerCase())
);
}
if (dateFrom) {
filtered = filtered.filter(
(item) => new Date(item.date) >= new Date(dateFrom)
);
}
if (dateTo) {
filtered = filtered.filter(
(item) => new Date(item.date) <= new Date(dateTo)
);
}
setFilteredReportList(filtered);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'DRAFT':
return (
<Badge
variant='outline'
className='border-gray-300 text-gray-700 bg-white'
>
Draft
</Badge>
);
case 'SUBMITTED':
return (
<Badge
variant='outline'
className='border-orange-300 text-orange-700 bg-white'
>
Submitted
</Badge>
);
case 'APPROVED':
return (
<Badge
variant='outline'
className='border-green-300 text-green-700 bg-white'
>
Approved
</Badge>
);
case 'REJECTED':
return (
<Badge
variant='outline'
className='border-red-300 text-red-700 bg-white'
>
Rejected
</Badge>
);
default:
return (
<Badge
variant='outline'
className='border-gray-300 text-gray-700 bg-white'
>
{status}
</Badge>
);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
useSelect(LocationApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
area_id: tableFilterState.area_id,
});
};
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
area_id: tableFilterState.area_id,
location_id: tableFilterState.location_id,
});
};
const handleViewDetail = (checklistId: string) => {
// Navigate to detail page (same as List Daily Checklist)
router.push(`/list-daily-checklist/detail?checklistId=${checklistId}`);
};
const { options: phaseOptions, isLoadingOptions: isLoadingPhases } =
useSelect(PhaseApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '100',
});
const { options: employeeOptions, isLoadingOptions: isLoadingEmployees } =
useSelect(EmployeeApi.basePath, 'id', 'name', 'search', {
page: '1',
limit: '500',
kandang_id: tableFilterState.kandang_id,
});
const currentMonthMaxDay = new Date(
Number(tableFilterState.tahun),
Number(tableFilterState.bulan),
0
).getDate();
const reportDateColumns: ColumnDef<DailyChecklistReport>[] = [];
if (isResponseSuccess(reportResponse) && reportResponse.data) {
for (let dateNumber = 1; dateNumber <= currentMonthMaxDay; dateNumber++) {
reportDateColumns.push({
accessorKey: `daily_activities[${dateNumber}]`,
header: `${dateNumber}`,
enableSorting: false,
cell: ({ row }) => row.original.daily_activities[dateNumber],
});
}
}
const reportColumns: ColumnDef<DailyChecklistReport>[] = [
{
accessorKey: 'area',
header: 'Area',
enableSorting: false,
cell: ({ row }) => row.original.area.name,
},
{
accessorKey: 'farm',
header: 'Farm',
enableSorting: false,
cell: ({ row }) => row.original.farm.name,
},
{
accessorKey: 'kandang',
header: 'Kandang',
enableSorting: false,
cell: ({ row }) => row.original.kandang.name,
},
{
accessorKey: 'abk',
header: 'ABK',
enableSorting: false,
cell: ({ row }) => (
<span className='text-nowrap'>{row.original.abk.name}</span>
),
},
{
accessorKey: 'phase',
header: 'Phase',
enableSorting: false,
cell: ({ row }) => row.original.phase,
},
{
header: `Tanggal - ${MONTH_OPTIONS[Number(tableFilterState.bulan) - 1].label} - ${tableFilterState.tahun}`,
columns: reportDateColumns,
},
{
accessorKey: 'summary.total_checklist',
header: 'Total Checklist',
enableSorting: false,
},
{
accessorKey: 'summary.jumlah_hari_efektif',
header: 'Jumlah Hari Efektif',
enableSorting: false,
},
{
accessorKey: 'summary.abk_percentage',
header: 'ABK (%)',
enableSorting: false,
},
{
accessorKey: 'summary.kandang_percentage',
header: 'Kandang (%)',
enableSorting: false,
},
{
header: 'Kategori',
columns: [
{
accessorKey: 'summary.kategori.kurang',
header: 'Kurang',
enableSorting: false,
cell: ({ row }) => (
<span className='text-red-400'>
{row.original.summary.kategori.kurang}
</span>
),
},
{
accessorKey: 'summary.kategori.cukup',
header: 'Cukup',
enableSorting: false,
cell: ({ row }) => row.original.summary.kategori.cukup,
},
{
accessorKey: 'summary.kategori.baik',
header: 'Baik',
enableSorting: false,
cell: ({ row }) => (
<span className='text-green-400'>
{row.original.summary.kategori.baik}
</span>
),
},
],
},
];
const exportToCSV = () => {
toast.info('Export CSV akan segera tersedia');
DailyChecklistApi.exportDailyChecklistReportToExcel(
getTableFilterQueryString()
);
};
const monthChangeHandler = (value: string) => updateFilter('bulan', value);
const yearChangeHandler = (value: string) => updateFilter('tahun', value);
const areaChangeHandler = (value: string) => {
updateFilter('area_id', value === 'ALL' ? '' : value);
updateFilter('location_id', '');
updateFilter('kandang_id', '');
updateFilter('employee_id', '');
};
const locationChangeHandler = (value: string) => {
updateFilter('location_id', value === 'ALL' ? '' : value);
updateFilter('kandang_id', '');
updateFilter('employee_id', '');
};
const kandangChangeHandler = (value: string) => {
updateFilter('kandang_id', value === 'ALL' ? '' : value);
updateFilter('employee_id', '');
};
const phaseChangeHandler = (value: string) => {
updateFilter('phase_id', value);
};
const employeeChangeHandler = (value: string) => {
updateFilter('employee_id', value === 'ALL' ? '' : value);
};
return (
@@ -395,22 +325,100 @@ export function DailyChecklistReportsContent() {
{/* Filters Section */}
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6 pb-6 border-b border-gray-200'>
<div>
<Label>Periode Tanggal</Label>
<div className='mt-1.5'>
<DateRangePicker
dateFrom={dateFrom}
dateTo={dateTo}
onDateChange={(from, to) => {
setDateFrom(from);
setDateTo(to);
}}
/>
</div>
<Label htmlFor='bulan-filter-report'>Bulan</Label>
<Select
value={tableFilterState.bulan}
onValueChange={monthChangeHandler}
>
<SelectTrigger
id='bulan-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua Bulan' />
</SelectTrigger>
<SelectContent>
{MONTH_OPTIONS.map((bulan) => (
<SelectItem key={bulan.value} value={String(bulan.value)}>
{bulan.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='tahun-filter-report'>Tahun</Label>
<Select
value={tableFilterState.tahun}
onValueChange={yearChangeHandler}
>
<SelectTrigger
id='tahun-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua Tahun' />
</SelectTrigger>
<SelectContent>
{YEAR_OPTIONS.map((tahun) => (
<SelectItem key={tahun.value} value={String(tahun.value)}>
{tahun.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='area-filter-report'>Area</Label>
<Select
value={tableFilterState.area_id}
onValueChange={areaChangeHandler}
>
<SelectTrigger
id='area-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua Area' />
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Area</SelectItem>
{areaOptions.map((area) => (
<SelectItem key={area.value} value={String(area.value)}>
{area.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='location-filter-report'>Lokasi</Label>
<Select
value={tableFilterState.location_id}
onValueChange={locationChangeHandler}
>
<SelectTrigger
id='location-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua Lokasi' />
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Lokasi</SelectItem>
{locationOptions.map((location) => (
<SelectItem
key={location.value}
value={String(location.value)}
>
{location.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='kandang-filter-report'>Kandang</Label>
<Select value={kandangFilter} onValueChange={setKandangFilter}>
<Select
value={tableFilterState.kandang_id}
onValueChange={kandangChangeHandler}
>
<SelectTrigger
id='kandang-filter-report'
className='mt-1.5 border-gray-200'
@@ -419,168 +427,105 @@ export function DailyChecklistReportsContent() {
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangList.map((kandang) => (
<SelectItem key={kandang.id} value={kandang.id}>
{kandang.name}
{kandangOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='status-filter-report'>Status</Label>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<Label htmlFor='phase-filter-report'>Phase</Label>
<Select
value={tableFilterState.phase_id}
onValueChange={phaseChangeHandler}
>
<SelectTrigger
id='status-filter-report'
id='phase-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua Status' />
<SelectValue placeholder='Semua Phase' />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
<SelectItem value='ALL'>Semua Phase</SelectItem>
{phaseOptions.map((phase) => (
<SelectItem key={phase.value} value={String(phase.value)}>
{phase.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='search-text-report'>Cari</Label>
<div className='relative mt-1.5'>
<Input
id='search-text-report'
type='text'
placeholder='Kandang / Kategori...'
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className='border-gray-200 pl-9'
/>
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
</div>
<Label htmlFor='employee-filter-report'>ABK</Label>
<Select
value={tableFilterState.employee_id}
onValueChange={employeeChangeHandler}
>
<SelectTrigger
id='employee-filter-report'
className='mt-1.5 border-gray-200'
>
<SelectValue placeholder='Semua ABK' />
</SelectTrigger>
<SelectContent>
<SelectItem value='ALL'>Semua ABK</SelectItem>
{employeeOptions.map((employee) => (
<SelectItem
key={employee.value}
value={String(employee.value)}
>
{employee.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Reports Table */}
{loading ? (
<div className='text-center py-12 text-gray-500'>
Memuat data...
</div>
) : filteredReportList.length > 0 ? (
<div className='overflow-x-auto'>
<table className='w-full border border-gray-200 rounded-lg'>
<thead>
<tr className='bg-gray-50 border-b border-gray-200'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Tanggal
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kandang
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Kategori
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Status
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Phase
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aktivitas
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
ABK
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Progress
</th>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
Updated At
</th>
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
Aksi
</th>
</tr>
</thead>
<tbody>
{filteredReportList.map((item, index) => (
<tr
key={`${item.checklist_id}-${item.date}-${index}`}
className={
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
}
>
<td className='py-3 px-4 text-sm text-gray-900'>
{formatDate(item.date)}
</td>
<td className='py-3 px-4 text-sm text-gray-900'>
{item.kandang_name}
</td>
<td className='py-3 px-4 text-sm text-gray-900'>
{CATEGORY_LABELS[item.category] || item.category}
</td>
<td className='py-3 px-4'>
{getStatusBadge(item.status)}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{item.total_phases}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{item.total_activities}
</td>
<td className='py-3 px-4 text-center text-sm text-gray-900'>
{item.total_employees}
</td>
<td className='py-3 px-4 text-center'>
<div className='flex items-center justify-center gap-2'>
<div className='w-20 bg-gray-200 rounded-full h-2'>
<div
className='bg-[#0069e0] h-2 rounded-full transition-all'
style={{ width: `${item.progress_percent}%` }}
/>
</div>
<span className='text-sm text-gray-700 font-medium'>
{item.progress_percent}%
</span>
</div>
</td>
<td className='py-3 px-4 text-sm text-gray-600'>
{formatDateTime(item.updated_at)}
</td>
<td className='py-3 px-4'>
<div className='flex items-center justify-center'>
<Button
size='sm'
variant='outline'
onClick={() =>
handleViewDetail(item.checklist_id)
}
className='border-gray-200 text-gray-700 hover:bg-gray-50'
>
<Eye className='w-4 h-4 mr-1' />
Detail
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className='text-center py-12 text-gray-500'>
{searchText ||
dateFrom ||
dateTo ||
statusFilter !== 'ALL' ||
kandangFilter !== 'ALL'
? 'Tidak ada data yang sesuai dengan filter'
: 'Belum ada data checklist'}
</div>
)}
<Table<DailyChecklistReport>
data={
isResponseSuccess(reportResponse)
? reportResponse.data || []
: []
}
columns={reportColumns}
isLoading={isLoadingReport}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(reportResponse)
? reportResponse?.meta?.page
: 0
}
totalItems={
isResponseSuccess(reportResponse)
? reportResponse?.meta?.total_results
: 0
}
onPageChange={setPage}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(reportResponse) &&
reportResponse?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700 border-x border-base-content/10',
bodyColumnClassName:
'px-4 py-3 text-base-content border-x border-base-content/10',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
-6
View File
@@ -1,6 +0,0 @@
// TODO: delete this file later
/* AUTOGENERATED FILE - DO NOT EDIT CONTENTS */
export const projectId = 'xxx';
export const publicAnonKey = 'xxx';
-339
View File
@@ -1,339 +0,0 @@
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { projectId, publicAnonKey } from '@/figma-make/lib/info';
// ============================================
// 🔍 SUPABASE ENVIRONMENT DEBUG CHECK
// ============================================
/**
* Get environment variable from multiple sources
* Checks in order: __ENV__, window.__ENV__, process.env, import.meta.env
*/
function getEnv(key: string): string | undefined {
let value: string | undefined;
let source: string | undefined;
// Check globalThis.__ENV__
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((globalThis as any).__ENV__?.[key]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value = (globalThis as any).__ENV__[key];
source = 'globalThis.__ENV__';
}
// Check window.__ENV__
// eslint-disable-next-line @typescript-eslint/no-explicit-any
else if (typeof window !== 'undefined' && (window as any).__ENV__?.[key]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value = (window as any).__ENV__[key];
source = 'window.__ENV__';
}
// Check process.env
// eslint-disable-next-line @typescript-eslint/no-explicit-any
else if ((globalThis as any).process?.env?.[key]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value = (globalThis as any).process.env[key];
source = 'process.env';
}
// Check import.meta.env (if available)
else if (
typeof import.meta !== 'undefined' &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(import.meta as any)?.env?.[key]
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value = (import.meta as any).env[key];
source = 'import.meta.env';
}
if (value && source) {
console.log(`${key} loaded from: ${source}`);
}
return value;
}
// Try to read from environment variables first
let supabaseUrl = getEnv('VITE_SUPABASE_URL');
let supabaseAnonKey = getEnv('VITE_SUPABASE_ANON_KEY');
// Fallback to Figma Make autogenerated credentials
if (!supabaseUrl || !supabaseAnonKey) {
console.log(
'📋 Using Figma Make autogenerated Supabase credentials from /utils/supabase/info.tsx'
);
supabaseUrl = `https://${projectId}.supabase.co`;
supabaseAnonKey = publicAnonKey;
}
// Helper function to mask sensitive data
const maskString = (str: string | undefined): string => {
if (!str) return 'undefined';
if (str.length <= 20) return str.substring(0, 10) + '...';
return str.substring(0, 20) + '...' + `(${str.length - 20} chars masked)`;
};
// Debug logging
console.group('🔍 Supabase Environment Check');
console.log('projectId (from info.tsx):', projectId);
console.log('SUPABASE_URL present?', !!supabaseUrl);
console.log('SUPABASE_KEY present?', !!supabaseAnonKey);
console.log('SUPABASE_URL value:', maskString(supabaseUrl));
console.log('SUPABASE_KEY value:', maskString(supabaseAnonKey));
console.groupEnd();
// Check if Supabase is configured
export const isSupabaseConfigured = () => {
return !!(supabaseUrl && supabaseAnonKey);
};
// Create Supabase client or throw error
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let supabase: SupabaseClient<any>;
if (isSupabaseConfigured()) {
console.log('✅ Creating real Supabase client...');
supabase = createClient<Database>(supabaseUrl!, supabaseAnonKey!);
console.log('✅ Supabase client created successfully!');
} else {
const errorMessage = `
SUPABASE CONFIGURATION ERROR
Missing required environment variables:
- VITE_SUPABASE_URL: ${!!supabaseUrl ? '✅ Present' : '❌ Missing'}
- VITE_SUPABASE_ANON_KEY: ${!!supabaseAnonKey ? '✅ Present' : '❌ Missing'}
Please set Supabase environment variables in:
Figma Make Supabase integration settings
Deployment settings/environment configuration
The app checked the following sources:
- globalThis.__ENV__
- window.__ENV__
- process.env
- import.meta.env
None of these sources contained the required variables.
`.trim();
console.error(errorMessage);
throw new Error(errorMessage);
}
export { supabase };
// Database types
export interface Database {
public: {
Tables: {
kandang: {
Row: {
id: string;
name: string;
created_at?: string;
};
Insert: {
id?: string;
name: string;
created_at?: string;
};
Update: {
id?: string;
name?: string;
created_at?: string;
};
};
employees: {
Row: {
id: string;
name: string;
kandang_id: string;
is_active: boolean;
created_at?: string;
};
Insert: {
id?: string;
name: string;
kandang_id: string;
is_active?: boolean;
created_at?: string;
};
Update: {
id?: string;
name?: string;
kandang_id?: string;
is_active?: boolean;
created_at?: string;
};
};
phases: {
Row: {
id: string;
name: string;
created_at?: string;
updated_at?: string;
};
Insert: {
id?: string;
name: string;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
name?: string;
created_at?: string;
updated_at?: string;
};
};
phase_activities: {
Row: {
id: string;
phase_id: string;
name: string;
description?: string;
created_at?: string;
updated_at?: string;
};
Insert: {
id?: string;
phase_id: string;
name: string;
description?: string;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
phase_id?: string;
name?: string;
description?: string;
created_at?: string;
updated_at?: string;
};
};
checklists: {
Row: {
id: string;
name: string;
description?: string;
phase_id: string;
created_at?: string;
updated_at?: string;
};
Insert: {
id?: string;
name: string;
description?: string;
phase_id: string;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
name?: string;
description?: string;
phase_id?: string;
created_at?: string;
updated_at?: string;
};
};
daily_checklists: {
Row: {
id: string;
date: string;
kandang_id: string;
checklist_id: string;
category: string;
status: string;
name?: string;
total_score?: number;
document_path?: string;
reject_reason?: string;
created_by: string;
created_at?: string;
updated_at?: string;
};
Insert: {
id?: string;
date: string;
kandang_id: string;
checklist_id: string;
category: string;
status?: string;
name?: string;
total_score?: number;
document_path?: string;
reject_reason?: string;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
date?: string;
kandang_id?: string;
checklist_id?: string;
category?: string;
status?: string;
name?: string;
total_score?: number;
document_path?: string;
reject_reason?: string;
created_at?: string;
updated_at?: string;
};
};
daily_checklist_tasks: {
Row: {
id: string;
checklist_id: string;
activity_id: string;
notes?: string;
created_at?: string;
updated_at?: string;
};
Insert: {
id?: string;
checklist_id: string;
activity_id: string;
notes?: string;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
checklist_id?: string;
activity_id?: string;
notes?: string;
created_at?: string;
updated_at?: string;
};
};
task_assignees: {
Row: {
id: string;
task_id: string;
employee_id: string;
is_completed: boolean;
created_at?: string;
updated_at?: string;
};
Insert: {
id?: string;
task_id: string;
employee_id: string;
is_completed?: boolean;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
task_id?: string;
employee_id?: string;
is_completed?: boolean;
created_at?: string;
updated_at?: string;
};
};
};
};
}
@@ -0,0 +1,19 @@
import { BaseApiService } from '@/services/api/base';
import {
CreateDailyChecklistConfigurationPayload,
DailyChecklistConfiguration,
UpdateDailyChecklistConfigurationPayload,
} from '@/types/api/daily-checklist/configuration';
export class DailyChecklistConfigurationApiService extends BaseApiService<
DailyChecklistConfiguration,
CreateDailyChecklistConfigurationPayload,
UpdateDailyChecklistConfigurationPayload
> {
constructor(basePath: string = '/master-data/config-checklists') {
super(basePath);
}
}
export const DailyChecklistConfigurationApi =
new DailyChecklistConfigurationApiService('/master-data/config-checklists');
@@ -1,13 +1,18 @@
import axios from 'axios';
import * as XLSX from 'xlsx';
import { BaseApiService } from '@/services/api/base';
import { httpClient } from '@/services/http/client';
import { httpClient, httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import {
CreateDailyChecklistPayload,
DailyChecklist,
DailyChecklistReport,
DetailDailyChecklist,
} from '@/types/api/daily-checklist/daily-checklist';
import { isResponseError } from '@/lib/api-helper';
import { toast } from 'sonner';
import { formatDate } from '@/lib/helper';
export class DailyChecklistApiService extends BaseApiService<
DailyChecklist,
@@ -134,15 +139,26 @@ export class DailyChecklistApiService extends BaseApiService<
}
}
async submit(id: string) {
async submit(
id: string,
files: File[] = [],
deletedDocumentIds: number[] = []
) {
try {
const formData = new FormData();
formData.append('status', 'SUBMITTED');
formData.append('reject_reason', '');
files.forEach((file) => formData.append(`documents`, file));
formData.append('deleted_document_ids', deletedDocumentIds.join(','));
const submitPath = `${this.basePath}/${id}`;
const submitRes = await httpClient<BaseApiResponse>(submitPath, {
method: 'PATCH',
body: {
status: 'SUBMITTED',
reject_reason: '',
},
body: formData,
});
return submitRes;
@@ -156,13 +172,16 @@ export class DailyChecklistApiService extends BaseApiService<
async approve(id: string) {
try {
const formData = new FormData();
formData.append('status', 'APPROVED');
formData.append('reject_reason', '');
const approvePath = `${this.basePath}/${id}`;
const approveRes = await httpClient<BaseApiResponse>(approvePath, {
method: 'PATCH',
body: {
status: 'APPROVED',
reject_reason: '',
},
body: formData,
});
return approveRes;
@@ -176,13 +195,16 @@ export class DailyChecklistApiService extends BaseApiService<
async reject(id: string, rejectReason: string) {
try {
const formData = new FormData();
formData.append('status', 'REJECTED');
formData.append('reject_reason', rejectReason);
const rejectPath = `${this.basePath}/${id}`;
const rejectRes = await httpClient<BaseApiResponse>(rejectPath, {
method: 'PATCH',
body: {
status: 'REJECTED',
reject_reason: rejectReason,
},
body: formData,
});
return rejectRes;
@@ -193,6 +215,111 @@ export class DailyChecklistApiService extends BaseApiService<
return undefined;
}
}
async uploadImage(
id: number,
status: string,
files: File[],
deletedDocumentIds: number[] = []
) {
try {
const formData = new FormData();
formData.append('status', status);
files.forEach((file) => formData.append(`documents`, file));
formData.append('deleted_document_ids', deletedDocumentIds.join(','));
const uploadImagePath = `${this.basePath}/${id}`;
const uploadImageRes = await httpClient<BaseApiResponse>(
uploadImagePath,
{
method: 'PATCH',
body: formData,
}
);
return uploadImageRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse>(error)) {
return error.response?.data;
}
return undefined;
}
}
async exportDailyChecklistReportToExcel(initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
params.set('limit', '2000');
const queryString = `?${params.toString()}`;
try {
const dailyMarketingsReport = await httpClientFetcher<
BaseApiResponse<DailyChecklistReport[]>
>(`${this.basePath}/report${queryString}`);
if (isResponseError(dailyMarketingsReport)) {
toast.error('Gagal melakukan export daily checklist! Coba lagi.');
return;
}
const currentMonthMaxDay = new Date(
Number(params.get('tahun')),
Number(params.get('bulan')),
0
).getDate();
const rows = dailyMarketingsReport.data;
const formattedRows = [];
for (let i = 0; i < rows.length; i++) {
const formattedData: Record<string, string | number> = {
Area: rows[i].area.name,
Farm: rows[i].farm.name,
Kandang: rows[i].kandang.name,
ABK: rows[i].abk.name,
Phase: rows[i].phase,
};
// Add day
for (let j = 1; j <= currentMonthMaxDay; j++) {
formattedData[`Day ${j}`] = rows[i].daily_activities[`${j}`];
}
// add summary
formattedData['Total Checklist'] = rows[i].summary.total_checklist;
formattedData['Jumlah Hari Efektif'] =
rows[i].summary.jumlah_hari_efektif;
formattedData['ABK %'] = rows[i].summary.abk_percentage;
formattedData['Kandang %'] = rows[i].summary.kandang_percentage;
formattedData['Kategori Kurang'] = rows[i].summary.kategori.kurang;
formattedData['Kategori Cukup'] = rows[i].summary.kategori.cukup;
formattedData['Kategori Baik'] = rows[i].summary.kategori.baik;
formattedRows.push(formattedData);
}
const ws = XLSX.utils.json_to_sheet(formattedRows);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(
wb,
ws,
`Daily Checklist ${params.get('tahun')}-${params.get('bulan')?.slice(0, 3)}`
);
// triggers download in browser
XLSX.writeFile(
wb,
`laporan-daily-checklist-${params.get('tahun')}-${params.get('bulan')}.xlsx`
);
} catch (error) {
toast.error('Gagal melakukan export daily checklist! Coba lagi.');
}
}
}
export const DailyChecklistApi = new DailyChecklistApiService(
+5 -1
View File
@@ -63,4 +63,8 @@ export class FinanceApiService extends BaseApiService<
}
}
export const FinanceApi = new FinanceApiService('reports');
// export const FinanceApi = new FinanceApiService('reports');
export const FinanceApi = new FinanceApiService(
'http://localhost:4010/api/reports/finance'
);
+15 -11
View File
@@ -2,11 +2,14 @@ import * as XLSX from 'xlsx';
import toast from 'react-hot-toast';
import { BaseApiService } from '@/services/api/base';
import { httpClient, httpClientFetcher } from '@/services/http/client';
import { httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general';
import { DailyMarketingReport } from '@/types/api/report/marketing';
import {
DailyMarketingReport,
DailyMarketingReportResponse,
} from '@/types/api/report/marketing';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate, sleep } from '@/lib/helper';
import { formatDate } from '@/lib/helper';
export class MarketingReportApiService extends BaseApiService<
DailyMarketingReport,
@@ -19,10 +22,8 @@ export class MarketingReportApiService extends BaseApiService<
async getAllDailyMarketingFetcher(
endpoint: string
): Promise<BaseApiResponse<DailyMarketingReport>> {
return await httpClientFetcher<BaseApiResponse<DailyMarketingReport>>(
endpoint
);
): Promise<DailyMarketingReportResponse> {
return await httpClientFetcher<DailyMarketingReportResponse>(endpoint);
}
async exportDailyMarketingToExcel(initialQueryString: string) {
@@ -42,16 +43,19 @@ export class MarketingReportApiService extends BaseApiService<
return;
}
const rows = dailyMarketingsReport.data.rows;
const rows = dailyMarketingsReport.data;
const formattedRows = [];
for (let i = 0; i < rows.length; i++) {
formattedRows.push({
...rows[i],
created_user: rows[i].created_user.name,
created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'),
updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'),
// created_user: rows[i].created_user.name,
// created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'),
// updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'),
so_date: formatDate(rows[i].so_date, 'YYYY-MM-DD'),
realization_date: formatDate(rows[i].realization_date, 'YYYY-MM-DD'),
sales: rows[i].sales.name,
warehouse: rows[i].warehouse.name,
customer: rows[i].customer.name,
product: rows[i].product.name,
+7
View File
@@ -116,3 +116,10 @@ export type BaseGroupedApproval = {
export type Approvals = BaseApiResponse<BaseApproval>;
export type GroupedApprovals = BaseApiResponse<BaseGroupedApproval[]>;
export type Document = {
id: number;
name: string;
size: number;
url: string;
};
+22
View File
@@ -0,0 +1,22 @@
import { BaseMetadata } from '@/types/api/api-general';
export type BaseConfiguration = {
id: number;
date: string;
percentage_threshold_bad: number;
percentage_threshold_enough: number;
};
export type DailyChecklistConfiguration = BaseMetadata & BaseConfiguration;
export type CreateDailyChecklistConfigurationPayload = {
date: string;
percentage_threshold_bad: number;
percentage_threshold_enough: number;
};
export type UpdateDailyChecklistConfigurationPayload = {
date: string;
percentage_threshold_bad: number;
percentage_threshold_enough: number;
};
+51 -1
View File
@@ -1,7 +1,10 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseMetadata, Document } from '@/types/api/api-general';
import { BaseKandang } from '@/types/api/master-data/kandang';
import { Phase } from '@/types/api/daily-checklist/phase';
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
import { BaseArea } from '@/types/api/master-data/area';
import { BaseLocation } from '@/types/api/master-data/location';
import { BaseEmployee } from '@/types/api/master-data/employee';
export type BaseDailyChecklist = {
id: number;
@@ -46,6 +49,7 @@ export type DetailDailyChecklist = BaseDailyChecklist & {
id: number;
name: string;
}[];
document_urls: Document[];
};
export type CreateDailyChecklistPayload = {
@@ -54,3 +58,49 @@ export type CreateDailyChecklistPayload = {
category: string;
status: string;
};
export type PerformanceOverviewItem = {
employee_id: number;
employee_name: string;
total_activity: number;
activity_done: number;
activity_left: number;
kandang: Pick<BaseKandang, 'id' | 'name'>;
};
export type TrackingAbkItem = {
employee_id: number;
employee_name: string;
kandang_id: number;
kandang_name: string;
total_activity: number;
activity_done: number;
activity_left: number;
completion_rate: number;
last_activity: string;
};
export type DailyChecklistSummary = {
performance_overview: PerformanceOverviewItem[];
tracking_abk: TrackingAbkItem[];
};
export type DailyChecklistReport = {
area: Pick<BaseArea, 'id' | 'name'>;
farm: Pick<BaseLocation, 'id' | 'name'>;
kandang: Pick<BaseKandang, 'id' | 'name'>;
abk: Pick<BaseEmployee, 'id' | 'name'>;
phase: string;
daily_activities: Record<string, number>;
summary: {
total_checklist: number;
jumlah_hari_efektif: number;
abk_percentage: number;
kandang_percentage: number;
kategori: {
kurang: number;
cukup: number;
baik: number;
};
};
};
+4 -9
View File
@@ -1,15 +1,13 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseCustomer } from '@/types/api/master-data/customer';
import { BaseProduct } from '@/types/api/master-data/product';
import { BaseMetadata } from '@/types/api/api-general';
export type CustomerPaymentRow = {
no: number;
id: number;
do_date: string;
payment_date: string;
realization_date: string;
aging: number;
aging_day: number | null;
reference: string;
vehicle_plate: string;
vehicle_plate: string[];
qty: number;
weight: number;
average_weight: number;
@@ -23,7 +21,6 @@ export type CustomerPaymentRow = {
notes: string;
pickup_info: string;
sales_marketing: string;
product?: BaseProduct;
};
export type CustomerPaymentSummary = {
@@ -40,8 +37,6 @@ export type CustomerPaymentSummary = {
export type CustomerPaymentReport = BaseMetadata & {
customer: BaseCustomer;
customer_npwp: string;
customer_address: string;
rows: CustomerPaymentRow[];
summary: CustomerPaymentSummary;
};
+11 -9
View File
@@ -1,4 +1,4 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseApiResponse, BaseMetadata } from '@/types/api/api-general';
import { BaseCustomer, Customer } from '@/types/api/master-data/customer';
import {
BaseWarehouseArea,
@@ -9,16 +9,17 @@ import {
import { Location } from '@/types/api/master-data/location';
import { Area } from '@/types/api/master-data/area';
import { BaseProduct } from '@/types/api/master-data/product';
import { BaseUser } from '@/types/api/user';
export type BaseDailyMarketingRow = {
no: number;
so_date: string; // e.g. "01-Dec-2025"
do_date: string; // e.g. "08-Dec-2025"
id: number;
so_date: string;
realization_date: string;
aging_days: number;
warehouse: BaseWarehouseArea | BaseWarehouseLocation | BaseWarehouseKandang;
customer: BaseCustomer;
sales: string;
sales: BaseUser;
product: BaseProduct;
do_number: string;
@@ -43,12 +44,13 @@ export interface SalesSummary {
total_weight_kg: number;
total_sales_amount: number;
total_hpp_amount: number;
total_hpp_price_per_kg: number;
}
export type DailyMarketingReport = {
rows: DailyMarketingRow[];
summary: SalesSummary;
};
export type DailyMarketingReport = DailyMarketingRow[];
export type DailyMarketingReportResponse =
BaseApiResponse<DailyMarketingReport> & { total: SalesSummary };
export type MarketingReportFilters = {
area_id?: number;