mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 14:55:44 +00:00
Merge branch 'feat/FE/daily-checklist' into 'development'
[FEAT/FE] Daily Checklist See merge request mbugroup/lti-web-client!160
This commit is contained in:
Generated
+2
-126
@@ -9,7 +9,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.1",
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
"@supabase/supabase-js": "^2.89.0",
|
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
@@ -3951,86 +3950,6 @@
|
|||||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -4471,6 +4390,7 @@
|
|||||||
"version": "20.19.23",
|
"version": "20.19.23",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
|
||||||
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
|
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -4488,12 +4408,6 @@
|
|||||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/raf": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
@@ -4542,15 +4456,6 @@
|
|||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.46.2",
|
"version": "8.46.2",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||||
@@ -7511,15 +7416,6 @@
|
|||||||
"integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==",
|
"integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -10717,6 +10613,7 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unicode-properties": {
|
"node_modules/unicode-properties": {
|
||||||
@@ -11048,27 +10945,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/xlsx": {
|
||||||
"version": "0.20.3",
|
"version": "0.20.3",
|
||||||
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.1",
|
"@react-pdf/renderer": "^4.3.1",
|
||||||
"@supabase/supabase-js": "^2.89.0",
|
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"axios": "^1.12.2",
|
"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;
|
||||||
@@ -60,6 +60,12 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
// TODO: add permission
|
// TODO: add permission
|
||||||
// permission: ['lti.daily_checklist.list'],
|
// permission: ['lti.daily_checklist.list'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Konfigurasi',
|
||||||
|
link: '/daily-checklist/master-data/configuration',
|
||||||
|
// TODO: add permission
|
||||||
|
// permission: ['lti.daily_checklist.list'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/daily-checklist/reports/': ['lti.dashboard.list'],
|
'/daily-checklist/reports/': ['lti.dashboard.list'],
|
||||||
'/daily-checklist/master-data/employee/': ['lti.dashboard.list'],
|
'/daily-checklist/master-data/employee/': ['lti.dashboard.list'],
|
||||||
'/daily-checklist/master-data/activity/': ['lti.dashboard.list'],
|
'/daily-checklist/master-data/activity/': ['lti.dashboard.list'],
|
||||||
|
'/daily-checklist/master-data/configuration/': ['lti.dashboard.list'],
|
||||||
|
|
||||||
// Production
|
// Production
|
||||||
// Production - Project Flock
|
// Production - Project Flock
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { KandangApi } from '@/services/api/master-data';
|
|||||||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse, Document } from '@/types/api/api-general';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||||
import { PhaseApi } from '@/services/api/daily-checklist/phase';
|
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 { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity';
|
||||||
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
|
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
|
||||||
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
|
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
|
||||||
|
import DropFileInput from '@/components/input/DropFileInput';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
// Static categories
|
// Static categories
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
@@ -148,6 +151,10 @@ export function DailyChecklistContent() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
|
|
||||||
|
const [existingDocuments, setExistingDocuments] = useState<Document[]>([]);
|
||||||
|
const [documents, setDocuments] = useState<File[]>([]);
|
||||||
|
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatDateForDisplay = (dateStr: string) => {
|
const formatDateForDisplay = (dateStr: string) => {
|
||||||
if (!dateStr) return 'Pilih tanggal';
|
if (!dateStr) return 'Pilih tanggal';
|
||||||
@@ -340,6 +347,9 @@ export function DailyChecklistContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set existing document
|
||||||
|
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
|
||||||
|
|
||||||
// Build assignments map
|
// Build assignments map
|
||||||
const assignmentMap: {
|
const assignmentMap: {
|
||||||
[taskId: string]: {
|
[taskId: string]: {
|
||||||
@@ -729,7 +739,11 @@ export function DailyChecklistContent() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const submitRes = await DailyChecklistApi.submit(dailyChecklistId);
|
const submitRes = await DailyChecklistApi.submit(
|
||||||
|
dailyChecklistId,
|
||||||
|
documents,
|
||||||
|
deletedDocumentIds
|
||||||
|
);
|
||||||
|
|
||||||
if (isResponseError(submitRes)) {
|
if (isResponseError(submitRes)) {
|
||||||
console.error('Error submitting:', submitRes.message);
|
console.error('Error submitting:', submitRes.message);
|
||||||
@@ -750,6 +764,19 @@ export function DailyChecklistContent() {
|
|||||||
const handleSaveDraft = async () => {
|
const handleSaveDraft = async () => {
|
||||||
if (!dailyChecklistId) return;
|
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');
|
toast.success('Draft tersimpan otomatis');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1263,6 +1290,94 @@ export function DailyChecklistContent() {
|
|||||||
</div>
|
</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 */}
|
{/* Action Buttons */}
|
||||||
{dailyChecklistId &&
|
{dailyChecklistId &&
|
||||||
selectedPhaseIds.length > 0 &&
|
selectedPhaseIds.length > 0 &&
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/figma-make/components/base/select';
|
} from '@/figma-make/components/base/select';
|
||||||
import { Input } from '@/figma-make/components/base/input';
|
|
||||||
import { Badge } from '@/figma-make/components/base/badge';
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
import {
|
import {
|
||||||
Calendar as CalendarIcon,
|
Calendar as CalendarIcon,
|
||||||
@@ -35,53 +34,17 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
Cell,
|
Cell,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import useSWR from 'swr';
|
||||||
interface EmployeePerformance {
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
employee_id: string;
|
import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checklist';
|
||||||
employee_name: string;
|
import { AxiosError } from 'axios';
|
||||||
kandang_id: string;
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||||
kandang_name: string;
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
total_activities_in_category: number; // Total aktivitas di kategori
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
completed_activities: number; // Aktivitas yang sudah di-check
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
completion_rate: number;
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
last_activity_date: string | null;
|
import { formatDate } from '@/lib/helper';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KANDANG_COLORS = [
|
const KANDANG_COLORS = [
|
||||||
'#0069e0', // Blue (primary)
|
'#0069e0', // Blue (primary)
|
||||||
@@ -102,312 +65,65 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function Dashboard() {
|
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
|
// Filters
|
||||||
const [dateFrom, setDateFrom] = useState('');
|
const [dateFrom, setDateFrom] = useState('');
|
||||||
const [dateTo, setDateTo] = useState('');
|
const [dateTo, setDateTo] = useState('');
|
||||||
const [kandangFilter, setKandangFilter] = useState('ALL');
|
const [kandangFilter, setKandangFilter] = useState('ALL');
|
||||||
const [categoryFilter, setCategoryFilter] = useState('ALL');
|
const [categoryFilter, setCategoryFilter] = useState('ALL');
|
||||||
|
|
||||||
// Color mapping for kandang
|
const {
|
||||||
const [kandangColorMap, setKandangColorMap] = useState<{
|
data: summaryResponse,
|
||||||
[key: string]: string;
|
isLoading: isLoadingSummary,
|
||||||
}>({});
|
mutate: refreshSummary,
|
||||||
|
} = useSWR<
|
||||||
useEffect(() => {
|
BaseApiResponse<DailyChecklistSummary | undefined>,
|
||||||
fetchMasterData();
|
AxiosError<BaseApiResponse>,
|
||||||
}, []);
|
SWRHttpKey
|
||||||
|
>(
|
||||||
useEffect(() => {
|
dateFrom && dateTo
|
||||||
// Only fetch when date filters are set
|
? `${DailyChecklistApi.basePath}/summary?date_from=${dateFrom}&date_to=${dateTo}&kandang_id=${kandangFilter === 'ALL' ? '' : kandangFilter}&category=${categoryFilter === 'ALL' ? '' : categoryFilter}`
|
||||||
if (dateFrom && dateTo) {
|
: '',
|
||||||
fetchEmployeePerformance();
|
httpClientFetcher,
|
||||||
} else {
|
{
|
||||||
setEmployeePerformance([]);
|
keepPreviousData: true,
|
||||||
}
|
}
|
||||||
}, [dateFrom, dateTo, kandangFilter, categoryFilter]);
|
);
|
||||||
|
|
||||||
const fetchMasterData = async () => {
|
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
||||||
if (!isSupabaseConfigured()) return;
|
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
|
||||||
|
page: '1',
|
||||||
try {
|
limit: '100',
|
||||||
// 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 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 employeePerformance = isResponseSuccess(summaryResponse)
|
||||||
const chartData = employeePerformance.map((emp) => ({
|
? summaryResponse.data?.tracking_abk.map((abk) => {
|
||||||
|
return {
|
||||||
|
...abk,
|
||||||
|
color: kandangColorMap[abk.kandang_id] || '#0069e0',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const chartData = employeePerformance?.map((emp) => ({
|
||||||
name: emp.employee_name,
|
name: emp.employee_name,
|
||||||
completed: emp.completed_activities,
|
completed: emp.activity_done,
|
||||||
remaining: emp.total_activities_in_category - emp.completed_activities,
|
remaining: emp.activity_left,
|
||||||
total: emp.total_activities_in_category,
|
total: emp.total_activity,
|
||||||
color: emp.color,
|
color: emp.color,
|
||||||
kandang: emp.kandang_name,
|
kandang: emp.kandang_name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const hasFilters = dateFrom && dateTo;
|
||||||
|
|
||||||
|
if (summaryResponse && isResponseError(summaryResponse)) {
|
||||||
|
toast.error('Gagal memuat data: ' + summaryResponse.message);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
@@ -457,9 +173,12 @@ export function Dashboard() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||||
{kandangList.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem key={kandang.id} value={kandang.id}>
|
<SelectItem
|
||||||
{kandang.name}
|
key={kandang.value}
|
||||||
|
value={String(kandang.value)}
|
||||||
|
>
|
||||||
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -482,9 +201,9 @@ export function Dashboard() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='ALL'>Semua Kategori</SelectItem>
|
<SelectItem value='ALL'>Semua Kategori</SelectItem>
|
||||||
{categoryList.map((category) => (
|
{Object.keys(CATEGORY_LABELS).map((category) => (
|
||||||
<SelectItem key={category.id} value={category.id}>
|
<SelectItem key={category} value={category}>
|
||||||
{category.name}
|
{CATEGORY_LABELS[category]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -523,11 +242,11 @@ export function Dashboard() {
|
|||||||
melihat performance ABK.
|
melihat performance ABK.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : loading ? (
|
) : isLoadingSummary ? (
|
||||||
<div className='text-center py-16 text-gray-500'>
|
<div className='text-center py-16 text-gray-500'>
|
||||||
Memuat data...
|
Memuat data...
|
||||||
</div>
|
</div>
|
||||||
) : employeePerformance.length === 0 ? (
|
) : employeePerformance && employeePerformance.length === 0 ? (
|
||||||
<div className='flex flex-col items-center justify-center py-16 text-center'>
|
<div className='flex flex-col items-center justify-center py-16 text-center'>
|
||||||
<Users className='w-16 h-16 text-gray-300 mb-4' />
|
<Users className='w-16 h-16 text-gray-300 mb-4' />
|
||||||
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
|
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
|
||||||
@@ -582,7 +301,7 @@ export function Dashboard() {
|
|||||||
fill='#10B981'
|
fill='#10B981'
|
||||||
radius={[0, 0, 0, 0]}
|
radius={[0, 0, 0, 0]}
|
||||||
>
|
>
|
||||||
{chartData.map((entry, index) => (
|
{chartData?.map((entry, index) => (
|
||||||
<Cell
|
<Cell
|
||||||
key={`cell-completed-${index}`}
|
key={`cell-completed-${index}`}
|
||||||
fill={entry.color}
|
fill={entry.color}
|
||||||
@@ -595,7 +314,7 @@ export function Dashboard() {
|
|||||||
fill='#E5E7EB'
|
fill='#E5E7EB'
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
>
|
>
|
||||||
{chartData.map((entry, index) => (
|
{chartData?.map((entry, index) => (
|
||||||
<Cell
|
<Cell
|
||||||
key={`cell-remaining-${index}`}
|
key={`cell-remaining-${index}`}
|
||||||
fill={`${entry.color}33`}
|
fill={`${entry.color}33`}
|
||||||
@@ -610,102 +329,103 @@ export function Dashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Employee Tracking Table */}
|
{/* Employee Tracking Table */}
|
||||||
{hasFilters && employeePerformance.length > 0 && (
|
{hasFilters &&
|
||||||
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
employeePerformance &&
|
||||||
<CardHeader>
|
employeePerformance.length > 0 && (
|
||||||
<CardTitle className='text-lg'>Tracking ABK</CardTitle>
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
||||||
<p className='text-sm text-gray-500 mt-1'>
|
<CardHeader>
|
||||||
Detail performance masing-masing ABK
|
<CardTitle className='text-lg'>Tracking ABK</CardTitle>
|
||||||
</p>
|
<p className='text-sm text-gray-500 mt-1'>
|
||||||
</CardHeader>
|
Detail performance masing-masing ABK
|
||||||
<CardContent>
|
</p>
|
||||||
<div className='overflow-x-auto'>
|
</CardHeader>
|
||||||
<table className='w-full border border-gray-200 rounded-lg'>
|
<CardContent>
|
||||||
<thead>
|
<div className='overflow-x-auto'>
|
||||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
<table className='w-full border border-gray-200 rounded-lg'>
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
<thead>
|
||||||
Nama ABK
|
<tr className='bg-gray-50 border-b border-gray-200'>
|
||||||
</th>
|
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
Nama ABK
|
||||||
Kandang
|
</th>
|
||||||
</th>
|
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
Kandang
|
||||||
Total Aktivitas
|
</th>
|
||||||
</th>
|
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
Total Aktivitas
|
||||||
Aktivitas Selesai
|
</th>
|
||||||
</th>
|
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
Aktivitas Selesai
|
||||||
Aktivitas Tersisa
|
</th>
|
||||||
</th>
|
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
Aktivitas Tersisa
|
||||||
Completion Rate
|
</th>
|
||||||
</th>
|
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
Completion Rate
|
||||||
Last Activity
|
</th>
|
||||||
</th>
|
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
||||||
</tr>
|
Last Activity
|
||||||
</thead>
|
</th>
|
||||||
<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>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{employeePerformance?.map((emp, index) => (
|
||||||
</div>
|
<tr
|
||||||
</CardContent>
|
key={emp.employee_id}
|
||||||
</Card>
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+37
@@ -20,6 +20,9 @@ import { toast } from 'sonner';
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
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 {
|
interface ChecklistDetailRow {
|
||||||
checklist_id: string;
|
checklist_id: string;
|
||||||
@@ -125,6 +128,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
const [employees, setEmployees] = useState<{ id: string; name: string }[]>(
|
const [employees, setEmployees] = useState<{ id: string; name: string }[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
const [documents, setDocuments] = useState<Document[]>([]);
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
@@ -160,6 +164,8 @@ export function DetailDailyChecklistContent() {
|
|||||||
|
|
||||||
const rawDetailChecklist = checklistDataRes?.data;
|
const rawDetailChecklist = checklistDataRes?.data;
|
||||||
|
|
||||||
|
setDocuments(rawDetailChecklist?.document_urls || []);
|
||||||
|
|
||||||
const checklistData = {
|
const checklistData = {
|
||||||
id: rawDetailChecklist?.id,
|
id: rawDetailChecklist?.id,
|
||||||
date: rawDetailChecklist?.date,
|
date: rawDetailChecklist?.date,
|
||||||
@@ -842,6 +848,37 @@ export function DetailDailyChecklistContent() {
|
|||||||
Tidak ada data aktivitas
|
Tidak ada data aktivitas
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ export function MasterAktivitasContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshPhases();
|
||||||
refreshPhaseActivities();
|
refreshPhaseActivities();
|
||||||
toast.success('Aktivitas berhasil ditambahkan');
|
toast.success('Aktivitas berhasil ditambahkan');
|
||||||
} else {
|
} else {
|
||||||
@@ -349,6 +350,7 @@ export function MasterAktivitasContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshPhases();
|
||||||
refreshPhaseActivities();
|
refreshPhaseActivities();
|
||||||
toast.success('Aktivitas berhasil diubah');
|
toast.success('Aktivitas berhasil diubah');
|
||||||
}
|
}
|
||||||
@@ -387,6 +389,7 @@ export function MasterAktivitasContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshPhases();
|
||||||
refreshPhaseActivities();
|
refreshPhaseActivities();
|
||||||
toast.success('Aktivitas berhasil dihapus');
|
toast.success('Aktivitas berhasil dihapus');
|
||||||
setShowActivityDeleteConfirm(false);
|
setShowActivityDeleteConfirm(false);
|
||||||
|
|||||||
+564
@@ -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) {
|
if (isLoadingEmployees && !employees) {
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
@@ -390,27 +386,6 @@ export function MasterEmployeeContent() {
|
|||||||
|
|
||||||
{/* RIGHT: Export + Add */}
|
{/* RIGHT: Export + Add */}
|
||||||
<div className='flex items-center gap-2 flex-wrap'>
|
<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
|
<Button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Eye, Download, Search } from 'lucide-react';
|
|
||||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
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 { Badge } from '@/figma-make/components/base/badge';
|
||||||
import { Input } from '@/figma-make/components/base/input';
|
|
||||||
import { Label } from '@/figma-make/components/base/label';
|
import { Label } from '@/figma-make/components/base/label';
|
||||||
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -16,357 +12,291 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/figma-make/components/base/select';
|
} from '@/figma-make/components/base/select';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
|
|
||||||
import { useRouter } from 'next/navigation';
|
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 {
|
const MONTH_OPTIONS = [
|
||||||
checklist_id: string;
|
{ value: '1', label: 'Januari' },
|
||||||
date: string;
|
{ value: '2', label: 'Februari' },
|
||||||
kandang_id: string;
|
{ value: '3', label: 'Maret' },
|
||||||
kandang_name: string;
|
{ value: '4', label: 'April' },
|
||||||
category: string;
|
{ value: '5', label: 'Mei' },
|
||||||
status: string;
|
{ value: '6', label: 'Juni' },
|
||||||
progress_percent: number;
|
{ value: '7', label: 'Juli' },
|
||||||
total_phases: number;
|
{ value: '8', label: 'Agustus' },
|
||||||
total_activities: number;
|
{ value: '9', label: 'September' },
|
||||||
total_employees: number;
|
{ value: '10', label: 'Oktober' },
|
||||||
updated_at: string;
|
{ value: '11', label: 'November' },
|
||||||
}
|
{ value: '12', label: 'Desember' },
|
||||||
|
|
||||||
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 CATEGORY_LABELS: { [key: string]: string } = {
|
const YEAR_OPTIONS = [
|
||||||
pullet_open: 'Pullet Open',
|
{ value: '2027', label: '2027' },
|
||||||
pullet_close: 'Pullet Close',
|
{ value: '2026', label: '2026' },
|
||||||
produksi_open: 'Produksi Open',
|
{ value: '2025', label: '2025' },
|
||||||
produksi_close: 'Produksi Close',
|
{ 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() {
|
export function DailyChecklistReportsContent() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// Report State
|
const currentMonth = useMemo(() => new Date().getMonth() + 1, []);
|
||||||
const [reportList, setReportList] = useState<SubmissionReportItem[]>([]);
|
const currentYear = useMemo(() => new Date().getFullYear(), []);
|
||||||
const [filteredReportList, setFilteredReportList] = useState<
|
|
||||||
SubmissionReportItem[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
// Master data
|
const {
|
||||||
const [kandangList, setKandangList] = useState<Kandang[]>([]);
|
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 {
|
||||||
const [statusFilter, setStatusFilter] = useState('ALL');
|
data: reportResponse,
|
||||||
const [kandangFilter, setKandangFilter] = useState('ALL');
|
isLoading: isLoadingReport,
|
||||||
const [searchText, setSearchText] = useState('');
|
mutate: refreshReport,
|
||||||
const [dateFrom, setDateFrom] = useState('');
|
} = useSWR<
|
||||||
const [dateTo, setDateTo] = useState('');
|
BaseApiResponse<DailyChecklistReport[] | undefined>,
|
||||||
|
AxiosError<BaseApiResponse>,
|
||||||
useEffect(() => {
|
SWRHttpKey
|
||||||
fetchKandangList();
|
>(
|
||||||
fetchReports();
|
`${DailyChecklistApi.basePath}/report${getTableFilterQueryString()}`,
|
||||||
}, []);
|
httpClientFetcher,
|
||||||
|
{
|
||||||
useEffect(() => {
|
keepPreviousData: true,
|
||||||
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 fetchReports = async () => {
|
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||||
if (!isSupabaseConfigured()) {
|
AreaApi.basePath,
|
||||||
console.warn('Supabase not configured');
|
'id',
|
||||||
setLoading(false);
|
'name',
|
||||||
return;
|
'search',
|
||||||
|
{
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
|
||||||
setLoading(true);
|
useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
|
page: '1',
|
||||||
// Fetch checklists directly from daily_checklists table
|
limit: '100',
|
||||||
const { data: checklists, error } = await supabase
|
area_id: tableFilterState.area_id,
|
||||||
.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 formatDateTime = (dateString: string) => {
|
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
||||||
const date = new Date(dateString);
|
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
|
||||||
return date.toLocaleString('id-ID', {
|
page: '1',
|
||||||
day: '2-digit',
|
limit: '100',
|
||||||
month: 'short',
|
area_id: tableFilterState.area_id,
|
||||||
year: 'numeric',
|
location_id: tableFilterState.location_id,
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDetail = (checklistId: string) => {
|
const { options: phaseOptions, isLoadingOptions: isLoadingPhases } =
|
||||||
// Navigate to detail page (same as List Daily Checklist)
|
useSelect(PhaseApi.basePath, 'id', 'name', 'search', {
|
||||||
router.push(`/list-daily-checklist/detail?checklistId=${checklistId}`);
|
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 = () => {
|
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 (
|
return (
|
||||||
@@ -395,22 +325,100 @@ export function DailyChecklistReportsContent() {
|
|||||||
{/* Filters Section */}
|
{/* 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 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>
|
<div>
|
||||||
<Label>Periode Tanggal</Label>
|
<Label htmlFor='bulan-filter-report'>Bulan</Label>
|
||||||
<div className='mt-1.5'>
|
<Select
|
||||||
<DateRangePicker
|
value={tableFilterState.bulan}
|
||||||
dateFrom={dateFrom}
|
onValueChange={monthChangeHandler}
|
||||||
dateTo={dateTo}
|
>
|
||||||
onDateChange={(from, to) => {
|
<SelectTrigger
|
||||||
setDateFrom(from);
|
id='bulan-filter-report'
|
||||||
setDateTo(to);
|
className='mt-1.5 border-gray-200'
|
||||||
}}
|
>
|
||||||
/>
|
<SelectValue placeholder='Semua Bulan' />
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor='kandang-filter-report'>Kandang</Label>
|
<Label htmlFor='kandang-filter-report'>Kandang</Label>
|
||||||
<Select value={kandangFilter} onValueChange={setKandangFilter}>
|
<Select
|
||||||
|
value={tableFilterState.kandang_id}
|
||||||
|
onValueChange={kandangChangeHandler}
|
||||||
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id='kandang-filter-report'
|
id='kandang-filter-report'
|
||||||
className='mt-1.5 border-gray-200'
|
className='mt-1.5 border-gray-200'
|
||||||
@@ -419,168 +427,105 @@ export function DailyChecklistReportsContent() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
<SelectItem value='ALL'>Semua Kandang</SelectItem>
|
||||||
{kandangList.map((kandang) => (
|
{kandangOptions.map((kandang) => (
|
||||||
<SelectItem key={kandang.id} value={kandang.id}>
|
<SelectItem
|
||||||
{kandang.name}
|
key={kandang.value}
|
||||||
|
value={String(kandang.value)}
|
||||||
|
>
|
||||||
|
{kandang.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor='status-filter-report'>Status</Label>
|
<Label htmlFor='phase-filter-report'>Phase</Label>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<Select
|
||||||
|
value={tableFilterState.phase_id}
|
||||||
|
onValueChange={phaseChangeHandler}
|
||||||
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id='status-filter-report'
|
id='phase-filter-report'
|
||||||
className='mt-1.5 border-gray-200'
|
className='mt-1.5 border-gray-200'
|
||||||
>
|
>
|
||||||
<SelectValue placeholder='Semua Status' />
|
<SelectValue placeholder='Semua Phase' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{STATUS_OPTIONS.map((option) => (
|
<SelectItem value='ALL'>Semua Phase</SelectItem>
|
||||||
<SelectItem key={option.value} value={option.value}>
|
{phaseOptions.map((phase) => (
|
||||||
{option.label}
|
<SelectItem key={phase.value} value={String(phase.value)}>
|
||||||
|
{phase.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor='search-text-report'>Cari</Label>
|
<Label htmlFor='employee-filter-report'>ABK</Label>
|
||||||
<div className='relative mt-1.5'>
|
<Select
|
||||||
<Input
|
value={tableFilterState.employee_id}
|
||||||
id='search-text-report'
|
onValueChange={employeeChangeHandler}
|
||||||
type='text'
|
>
|
||||||
placeholder='Kandang / Kategori...'
|
<SelectTrigger
|
||||||
value={searchText}
|
id='employee-filter-report'
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
className='mt-1.5 border-gray-200'
|
||||||
className='border-gray-200 pl-9'
|
>
|
||||||
/>
|
<SelectValue placeholder='Semua ABK' />
|
||||||
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
|
</SelectTrigger>
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reports Table */}
|
{/* Reports Table */}
|
||||||
{loading ? (
|
<Table<DailyChecklistReport>
|
||||||
<div className='text-center py-12 text-gray-500'>
|
data={
|
||||||
Memuat data...
|
isResponseSuccess(reportResponse)
|
||||||
</div>
|
? reportResponse.data || []
|
||||||
) : filteredReportList.length > 0 ? (
|
: []
|
||||||
<div className='overflow-x-auto'>
|
}
|
||||||
<table className='w-full border border-gray-200 rounded-lg'>
|
columns={reportColumns}
|
||||||
<thead>
|
isLoading={isLoadingReport}
|
||||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
pageSize={tableFilterState.pageSize}
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
onPageSizeChange={setPageSize}
|
||||||
Tanggal
|
rowOptions={[10, 20, 50, 100]}
|
||||||
</th>
|
page={
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
isResponseSuccess(reportResponse)
|
||||||
Kandang
|
? reportResponse?.meta?.page
|
||||||
</th>
|
: 0
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
}
|
||||||
Kategori
|
totalItems={
|
||||||
</th>
|
isResponseSuccess(reportResponse)
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
? reportResponse?.meta?.total_results
|
||||||
Status
|
: 0
|
||||||
</th>
|
}
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
onPageChange={setPage}
|
||||||
Phase
|
className={{
|
||||||
</th>
|
containerClassName: cn({
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
'w-full mb-20':
|
||||||
Aktivitas
|
isResponseSuccess(reportResponse) &&
|
||||||
</th>
|
reportResponse?.data?.length === 0,
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
}),
|
||||||
ABK
|
tableWrapperClassName:
|
||||||
</th>
|
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
headerRowClassName: 'bg-gray-50/50',
|
||||||
Progress
|
headerColumnClassName:
|
||||||
</th>
|
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700 border-x border-base-content/10',
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
bodyColumnClassName:
|
||||||
Updated At
|
'px-4 py-3 text-base-content border-x border-base-content/10',
|
||||||
</th>
|
paginationClassName: 'px-4',
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// TODO: delete this file later
|
|
||||||
|
|
||||||
/* AUTOGENERATED FILE - DO NOT EDIT CONTENTS */
|
|
||||||
|
|
||||||
export const projectId = 'xxx';
|
|
||||||
export const publicAnonKey = 'xxx';
|
|
||||||
@@ -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 axios from 'axios';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
import { BaseApiService } from '@/services/api/base';
|
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 { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
CreateDailyChecklistPayload,
|
CreateDailyChecklistPayload,
|
||||||
DailyChecklist,
|
DailyChecklist,
|
||||||
|
DailyChecklistReport,
|
||||||
DetailDailyChecklist,
|
DetailDailyChecklist,
|
||||||
} from '@/types/api/daily-checklist/daily-checklist';
|
} 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<
|
export class DailyChecklistApiService extends BaseApiService<
|
||||||
DailyChecklist,
|
DailyChecklist,
|
||||||
@@ -134,15 +139,26 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit(id: string) {
|
async submit(
|
||||||
|
id: string,
|
||||||
|
files: File[] = [],
|
||||||
|
deletedDocumentIds: number[] = []
|
||||||
|
) {
|
||||||
try {
|
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 submitPath = `${this.basePath}/${id}`;
|
||||||
const submitRes = await httpClient<BaseApiResponse>(submitPath, {
|
const submitRes = await httpClient<BaseApiResponse>(submitPath, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: formData,
|
||||||
status: 'SUBMITTED',
|
|
||||||
reject_reason: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return submitRes;
|
return submitRes;
|
||||||
@@ -156,13 +172,16 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
|
|
||||||
async approve(id: string) {
|
async approve(id: string) {
|
||||||
try {
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('status', 'APPROVED');
|
||||||
|
|
||||||
|
formData.append('reject_reason', '');
|
||||||
|
|
||||||
const approvePath = `${this.basePath}/${id}`;
|
const approvePath = `${this.basePath}/${id}`;
|
||||||
const approveRes = await httpClient<BaseApiResponse>(approvePath, {
|
const approveRes = await httpClient<BaseApiResponse>(approvePath, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: formData,
|
||||||
status: 'APPROVED',
|
|
||||||
reject_reason: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return approveRes;
|
return approveRes;
|
||||||
@@ -176,13 +195,16 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
|
|
||||||
async reject(id: string, rejectReason: string) {
|
async reject(id: string, rejectReason: string) {
|
||||||
try {
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('status', 'REJECTED');
|
||||||
|
|
||||||
|
formData.append('reject_reason', rejectReason);
|
||||||
|
|
||||||
const rejectPath = `${this.basePath}/${id}`;
|
const rejectPath = `${this.basePath}/${id}`;
|
||||||
const rejectRes = await httpClient<BaseApiResponse>(rejectPath, {
|
const rejectRes = await httpClient<BaseApiResponse>(rejectPath, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: formData,
|
||||||
status: 'REJECTED',
|
|
||||||
reject_reason: rejectReason,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return rejectRes;
|
return rejectRes;
|
||||||
@@ -193,6 +215,111 @@ export class DailyChecklistApiService extends BaseApiService<
|
|||||||
return undefined;
|
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(
|
export const DailyChecklistApi = new DailyChecklistApiService(
|
||||||
|
|||||||
Vendored
+7
@@ -116,3 +116,10 @@ export type BaseGroupedApproval = {
|
|||||||
export type Approvals = BaseApiResponse<BaseApproval>;
|
export type Approvals = BaseApiResponse<BaseApproval>;
|
||||||
|
|
||||||
export type GroupedApprovals = BaseApiResponse<BaseGroupedApproval[]>;
|
export type GroupedApprovals = BaseApiResponse<BaseGroupedApproval[]>;
|
||||||
|
|
||||||
|
export type Document = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
@@ -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 { BaseKandang } from '@/types/api/master-data/kandang';
|
||||||
import { Phase } from '@/types/api/daily-checklist/phase';
|
import { Phase } from '@/types/api/daily-checklist/phase';
|
||||||
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
|
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 = {
|
export type BaseDailyChecklist = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -46,6 +49,7 @@ export type DetailDailyChecklist = BaseDailyChecklist & {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
}[];
|
||||||
|
document_urls: Document[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateDailyChecklistPayload = {
|
export type CreateDailyChecklistPayload = {
|
||||||
@@ -54,3 +58,49 @@ export type CreateDailyChecklistPayload = {
|
|||||||
category: string;
|
category: string;
|
||||||
status: 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user