mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu
This commit is contained in:
Generated
+17
-17
@@ -26,9 +26,9 @@
|
|||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.1.0",
|
"react": "^19.1.2",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "^19.1.2",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hook-form": "^7.70.0",
|
"react-hook-form": "^7.70.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
@@ -8174,12 +8174,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jspdf-autotable": {
|
"node_modules/jspdf-autotable": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz",
|
||||||
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
|
"integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"jspdf": "^2 || ^3"
|
"jspdf": "^2 || ^3 || ^4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsx-ast-utils": {
|
"node_modules/jsx-ast-utils": {
|
||||||
@@ -9376,9 +9376,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -9407,16 +9407,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.1.0"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dropzone": {
|
"node_modules/react-dropzone": {
|
||||||
@@ -9916,9 +9916,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.26.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
|
|||||||
+2
-2
@@ -29,9 +29,9 @@
|
|||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.1.0",
|
"react": "^19.1.2",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "^19.1.2",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hook-form": "^7.70.0",
|
"react-hook-form": "^7.70.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function MultiSelect({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen} modal>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -115,8 +115,8 @@ export function MultiSelect({
|
|||||||
onValueChange={onSearchChange}
|
onValueChange={onSearchChange}
|
||||||
/>
|
/>
|
||||||
<CommandEmpty>No item found.</CommandEmpty>
|
<CommandEmpty>No item found.</CommandEmpty>
|
||||||
<CommandList>
|
<CommandList className='max-h-[300px] overflow-y-auto'>
|
||||||
<CommandGroup className='max-h-64 overflow-auto'>
|
<CommandGroup className='overflow-visible'>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot='select-content'
|
data-slot='select-content'
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[300px] min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||||
position === 'popper' &&
|
position === 'popper' &&
|
||||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Card, CardContent } from '@/figma-make/components/base/card';
|
|||||||
import { Button } from '@/figma-make/components/base/button';
|
import { Button } from '@/figma-make/components/base/button';
|
||||||
import { Label } from '@/figma-make/components/base/label';
|
import { Label } from '@/figma-make/components/base/label';
|
||||||
import { Input } from '@/figma-make/components/base/input';
|
import { Input } from '@/figma-make/components/base/input';
|
||||||
import { Textarea } from '@/figma-make/components/base/textarea';
|
|
||||||
import { Badge } from '@/figma-make/components/base/badge';
|
import { Badge } from '@/figma-make/components/base/badge';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -26,7 +25,20 @@ import {
|
|||||||
} from '@/figma-make/components/base/dialog';
|
} from '@/figma-make/components/base/dialog';
|
||||||
import { DatePicker } from '@/figma-make/components/base/date-picker';
|
import { DatePicker } from '@/figma-make/components/base/date-picker';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||||
|
import { PhaseApi } from '@/services/api/daily-checklist/phase';
|
||||||
|
import { EmployeeApi } from '@/services/api/daily-checklist/employee';
|
||||||
|
import { Employee } from '@/types/api/daily-checklist/employee';
|
||||||
|
import { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity';
|
||||||
|
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
|
||||||
|
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
|
||||||
|
|
||||||
// Static categories
|
// Static categories
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
@@ -38,59 +50,68 @@ const CATEGORIES = [
|
|||||||
|
|
||||||
const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam'];
|
const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam'];
|
||||||
const TIME_TYPE_LABELS: { [key: string]: string } = {
|
const TIME_TYPE_LABELS: { [key: string]: string } = {
|
||||||
umum: 'Umum',
|
Umum: 'Umum',
|
||||||
pagi: 'Pagi',
|
Pagi: 'Pagi',
|
||||||
siang: 'Siang',
|
Siang: 'Siang',
|
||||||
sore: 'Sore',
|
Sore: 'Sore',
|
||||||
malam: 'Malam',
|
Malam: 'Malam',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Kandang {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Phase {
|
interface Phase {
|
||||||
id: string;
|
id: string;
|
||||||
name: 'string';
|
name: 'string';
|
||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Activity {
|
|
||||||
id: string;
|
|
||||||
phase_id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
time_type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Employee {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
kandang_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskAssignment {
|
|
||||||
task_id: string;
|
|
||||||
employee_id: string;
|
|
||||||
checked: boolean;
|
|
||||||
note: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DailyChecklistContent() {
|
export function DailyChecklistContent() {
|
||||||
|
const [kandangId, setKandangId] = useState('');
|
||||||
|
|
||||||
|
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
||||||
|
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: phases,
|
||||||
|
isLoading: isLoadingPhases,
|
||||||
|
mutate: refreshPhases,
|
||||||
|
} = useSWR<
|
||||||
|
BaseApiResponse<Phase[] | undefined>,
|
||||||
|
AxiosError<BaseApiResponse>,
|
||||||
|
SWRHttpKey
|
||||||
|
>(`${PhaseApi.basePath}?page=1&limit=100`, httpClientFetcher, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: employeesRes,
|
||||||
|
isLoading: isLoadingEmployees,
|
||||||
|
mutate: refreshEmployees,
|
||||||
|
} = useSWR(
|
||||||
|
`${EmployeeApi.basePath}?page=1&limit=500&kandang_id=${kandangId}&is_active=true`,
|
||||||
|
EmployeeApi.getAllFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const allPhases = isResponseSuccess(phases) ? phases.data || [] : [];
|
||||||
|
const employees = isResponseSuccess(employeesRes)
|
||||||
|
? employeesRes.data || []
|
||||||
|
: [];
|
||||||
|
|
||||||
const [date, setDate] = useState(() => {
|
const [date, setDate] = useState(() => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
return today.toISOString().split('T')[0];
|
return today.toISOString().split('T')[0];
|
||||||
});
|
});
|
||||||
const [kandangId, setKandangId] = useState('');
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState('');
|
const [selectedCategory, setSelectedCategory] = useState('');
|
||||||
const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]);
|
const [selectedPhaseIds, setSelectedPhaseIds] = useState<string[]>([]);
|
||||||
const [checklistName, setChecklistName] = useState('');
|
|
||||||
|
|
||||||
const [kandangList, setKandangList] = useState<Kandang[]>([]);
|
const [selectedEmployees, setSelectedEmployees] = useState<
|
||||||
const [allPhases, setAllPhases] = useState<Phase[]>([]);
|
{ id: number; name: string }[]
|
||||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
>([]);
|
||||||
const [selectedEmployees, setSelectedEmployees] = useState<Employee[]>([]);
|
|
||||||
|
|
||||||
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
|
const [dailyChecklistId, setDailyChecklistId] = useState<string | null>(null);
|
||||||
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
|
const [checklistStatus, setChecklistStatus] = useState<string>('DRAFT');
|
||||||
@@ -98,7 +119,7 @@ export function DailyChecklistContent() {
|
|||||||
|
|
||||||
// Activities grouped by phase
|
// Activities grouped by phase
|
||||||
const [activitiesByPhase, setActivitiesByPhase] = useState<{
|
const [activitiesByPhase, setActivitiesByPhase] = useState<{
|
||||||
[phaseId: string]: Activity[];
|
[phaseId: string]: PhaseActivity[];
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
// Task IDs mapped by phase_activity_id for quick lookup
|
// Task IDs mapped by phase_activity_id for quick lookup
|
||||||
@@ -116,7 +137,7 @@ export function DailyChecklistContent() {
|
|||||||
const [showAbkModal, setShowAbkModal] = useState(false);
|
const [showAbkModal, setShowAbkModal] = useState(false);
|
||||||
const [showPhaseModal, setShowPhaseModal] = useState(false);
|
const [showPhaseModal, setShowPhaseModal] = useState(false);
|
||||||
const [tempSelectedEmployees, setTempSelectedEmployees] = useState<
|
const [tempSelectedEmployees, setTempSelectedEmployees] = useState<
|
||||||
Employee[]
|
{ id: number; name: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
const [tempSelectedPhaseIds, setTempSelectedPhaseIds] = useState<string[]>(
|
const [tempSelectedPhaseIds, setTempSelectedPhaseIds] = useState<string[]>(
|
||||||
[]
|
[]
|
||||||
@@ -126,7 +147,6 @@ export function DailyChecklistContent() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatDateForDisplay = (dateStr: string) => {
|
const formatDateForDisplay = (dateStr: string) => {
|
||||||
@@ -143,23 +163,13 @@ export function DailyChecklistContent() {
|
|||||||
|
|
||||||
// Fetch master data on mount
|
// Fetch master data on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSupabaseConfigured()) {
|
|
||||||
console.warn(
|
|
||||||
'Supabase not configured. Please add environment variables.'
|
|
||||||
);
|
|
||||||
setInitialLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchKandang();
|
|
||||||
fetchAllPhases();
|
|
||||||
setInitialLoading(false);
|
setInitialLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Check for existing checklist when unique key changes
|
// Check for existing checklist when unique key changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAndLoadChecklist = async () => {
|
const checkAndLoadChecklist = async () => {
|
||||||
if (!date || !kandangId || !selectedCategory || !isSupabaseConfigured()) {
|
if (!date || !kandangId || !selectedCategory) {
|
||||||
setDailyChecklistId(null);
|
setDailyChecklistId(null);
|
||||||
setChecklistStatus('DRAFT');
|
setChecklistStatus('DRAFT');
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
@@ -171,55 +181,47 @@ export function DailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// UPSERT to get or create checklist (UNIQUE KEY: date, kandang_id, category)
|
const checklist = await DailyChecklistApi.create({
|
||||||
const { data: checklist, error } = await supabase
|
|
||||||
.from('daily_checklists')
|
|
||||||
.upsert(
|
|
||||||
{
|
|
||||||
date,
|
date,
|
||||||
kandang_id: kandangId,
|
kandang_id: Number(kandangId),
|
||||||
category: selectedCategory,
|
category: selectedCategory,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
updated_at: new Date().toISOString(),
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
onConflict: 'date,kandang_id,category',
|
|
||||||
ignoreDuplicates: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(checklist)) {
|
||||||
console.error('Error upserting checklist:', error);
|
console.error('Error upserting checklist:', checklist.message);
|
||||||
toast.error('Gagal memuat checklist');
|
toast.error('Gagal memuat checklist');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDailyChecklistId(checklist.id);
|
setDailyChecklistId(String(checklist?.data?.id));
|
||||||
setChecklistStatus(checklist.status);
|
setChecklistStatus(String(checklist?.data?.status));
|
||||||
|
|
||||||
// Load existing phases for this checklist
|
const existingPhases = await DailyChecklistApi.getOneDailyChecklist(
|
||||||
const { data: existingPhases, error: phaseError } = await supabase
|
String(checklist?.data.id)
|
||||||
.from('daily_checklist_phases')
|
);
|
||||||
.select('phase_id')
|
|
||||||
.eq('checklist_id', checklist.id); // ✅ Uses checklist_id
|
|
||||||
|
|
||||||
if (phaseError) {
|
if (isResponseError(existingPhases)) {
|
||||||
console.error('Error loading phases:', phaseError);
|
console.error('Error loading phases:', existingPhases.message);
|
||||||
} else if (existingPhases && existingPhases.length > 0) {
|
} else if (
|
||||||
|
existingPhases &&
|
||||||
|
existingPhases.data &&
|
||||||
|
existingPhases.data.phases.length > 0
|
||||||
|
) {
|
||||||
// Existing checklist - EDIT MODE
|
// Existing checklist - EDIT MODE
|
||||||
setIsEditMode(true);
|
setIsEditMode(true);
|
||||||
const phaseIds = existingPhases.map((p) => p.phase_id);
|
const phaseIds = existingPhases.data.phases.map((p) =>
|
||||||
|
String(p.phase_id)
|
||||||
|
);
|
||||||
setSelectedPhaseIds(phaseIds);
|
setSelectedPhaseIds(phaseIds);
|
||||||
|
|
||||||
if (checklist.status === 'DRAFT') {
|
if (checklist?.data?.status === 'DRAFT') {
|
||||||
toast.info('Checklist ditemukan - Mode Edit (Draft)', {
|
toast.info('Checklist ditemukan - Mode Edit (Draft)', {
|
||||||
description:
|
description:
|
||||||
'Anda dapat menambah atau mengubah data checklist ini',
|
'Anda dapat menambah atau mengubah data checklist ini',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.warning(`Checklist sudah ${checklist.status}`, {
|
toast.warning(`Checklist sudah ${checklist?.data?.status}`, {
|
||||||
description: 'Checklist tidak dapat diedit',
|
description: 'Checklist tidak dapat diedit',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -239,32 +241,27 @@ export function DailyChecklistContent() {
|
|||||||
// Load activities and tasks when phases change
|
// Load activities and tasks when phases change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadActivitiesAndTasks = async () => {
|
const loadActivitiesAndTasks = async () => {
|
||||||
if (
|
if (!dailyChecklistId || selectedPhaseIds.length === 0) {
|
||||||
!dailyChecklistId ||
|
|
||||||
selectedPhaseIds.length === 0 ||
|
|
||||||
!isSupabaseConfigured()
|
|
||||||
) {
|
|
||||||
setActivitiesByPhase({});
|
setActivitiesByPhase({});
|
||||||
setTaskIdsByPhaseActivityId({});
|
setTaskIdsByPhaseActivityId({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch activities for selected phases
|
const activitiesRes = await PhaseActivityApi.getAll({
|
||||||
const { data: activities, error: actError } = await supabase
|
phase_ids: selectedPhaseIds.join(','),
|
||||||
.from('phase_activities')
|
});
|
||||||
.select('id, phase_id, name, description, time_type')
|
|
||||||
.in('phase_id', selectedPhaseIds)
|
|
||||||
.order('id', { ascending: true }); // ✅ Urutan berdasarkan ID (yang paling awal diinput di atas)
|
|
||||||
|
|
||||||
if (actError) {
|
if (isResponseError(activitiesRes)) {
|
||||||
console.error('Error loading activities:', actError);
|
console.error('Error loading activities:', activitiesRes.message);
|
||||||
toast.error('Gagal memuat aktivitas');
|
toast.error('Gagal memuat aktivitas');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activities = activitiesRes?.data || [];
|
||||||
|
|
||||||
// Group activities by phase
|
// Group activities by phase
|
||||||
const grouped: { [phaseId: string]: Activity[] } = {};
|
const grouped: { [phaseId: string]: PhaseActivity[] } = {};
|
||||||
(activities || []).forEach((act) => {
|
(activities || []).forEach((act) => {
|
||||||
if (!grouped[act.phase_id]) {
|
if (!grouped[act.phase_id]) {
|
||||||
grouped[act.phase_id] = [];
|
grouped[act.phase_id] = [];
|
||||||
@@ -283,28 +280,30 @@ export function DailyChecklistContent() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (taskUpserts.length > 0) {
|
if (taskUpserts.length > 0) {
|
||||||
const { data: tasks, error: taskError } = await supabase
|
const existingDailyChecklist =
|
||||||
.from('daily_checklist_activity_tasks')
|
await DailyChecklistApi.getOneDailyChecklist(
|
||||||
.upsert(taskUpserts, {
|
String(dailyChecklistId)
|
||||||
onConflict: 'checklist_id,phase_activity_id',
|
);
|
||||||
ignoreDuplicates: false,
|
|
||||||
})
|
|
||||||
.select('id, phase_activity_id');
|
|
||||||
|
|
||||||
if (taskError) {
|
if (isResponseError(existingDailyChecklist)) {
|
||||||
console.error('Error upserting tasks:', taskError);
|
console.error(
|
||||||
|
'Error loading assignments:',
|
||||||
|
existingDailyChecklist.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build task ID lookup
|
// Build task ID lookup
|
||||||
const taskMap: { [phaseActivityId: string]: string } = {};
|
const taskMap: { [phaseActivityId: string]: string } = {};
|
||||||
(tasks || []).forEach((task) => {
|
(existingDailyChecklist?.data?.tasks || []).forEach((task) => {
|
||||||
taskMap[task.phase_activity_id] = task.id;
|
taskMap[String(task.phase_activity_id)] = String(task.id);
|
||||||
});
|
});
|
||||||
setTaskIdsByPhaseActivityId(taskMap);
|
setTaskIdsByPhaseActivityId(taskMap);
|
||||||
|
|
||||||
// Load existing assignments for these tasks
|
// Load existing assignments for these tasks
|
||||||
await loadAssignments(tasks.map((t) => t.id));
|
await loadAssignments(
|
||||||
|
existingDailyChecklist?.data?.tasks?.map((t) => String(t.id)) || []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading activities and tasks:', error);
|
console.error('Error loading activities and tasks:', error);
|
||||||
@@ -316,29 +315,28 @@ export function DailyChecklistContent() {
|
|||||||
|
|
||||||
// Load employees when kandang changes
|
// Load employees when kandang changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (kandangId && isSupabaseConfigured()) {
|
if (kandangId) {
|
||||||
fetchEmployees(kandangId);
|
|
||||||
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
|
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
|
||||||
setSelectedEmployees([]);
|
setSelectedEmployees([]);
|
||||||
setAssignments({});
|
setAssignments({});
|
||||||
} else {
|
} else {
|
||||||
setEmployees([]);
|
|
||||||
setSelectedEmployees([]);
|
setSelectedEmployees([]);
|
||||||
setAssignments({});
|
setAssignments({});
|
||||||
}
|
}
|
||||||
}, [kandangId]);
|
}, [kandangId]);
|
||||||
|
|
||||||
const loadAssignments = async (taskIds: string[]) => {
|
const loadAssignments = async (taskIds: string[]) => {
|
||||||
if (taskIds.length === 0 || !isSupabaseConfigured()) return;
|
if (taskIds.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const existingDailyChecklist =
|
||||||
.from('daily_checklist_activity_task_assignments')
|
await DailyChecklistApi.getOneDailyChecklist(String(dailyChecklistId));
|
||||||
.select('task_id, employee_id, checked, note')
|
|
||||||
.in('task_id', taskIds);
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(existingDailyChecklist)) {
|
||||||
console.error('Error loading assignments:', error);
|
console.error(
|
||||||
|
'Error loading assignments:',
|
||||||
|
existingDailyChecklist.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,30 +347,43 @@ export function DailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
(data || []).forEach((assignment) => {
|
(existingDailyChecklist?.data.tasks || []).forEach(
|
||||||
if (!assignmentMap[assignment.task_id]) {
|
(dailyChecklistTask) => {
|
||||||
assignmentMap[assignment.task_id] = {};
|
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||||
|
assignmentMap[dailyChecklistTask.id] = {};
|
||||||
}
|
}
|
||||||
assignmentMap[assignment.task_id][assignment.employee_id] = {
|
|
||||||
|
dailyChecklistTask.assignments.forEach((assignment) => {
|
||||||
|
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||||
|
assignmentMap[dailyChecklistTask.id] = {};
|
||||||
|
}
|
||||||
|
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
|
||||||
checked: assignment.checked,
|
checked: assignment.checked,
|
||||||
note: assignment.note || '',
|
note: assignment.note || '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setAssignments(assignmentMap);
|
setAssignments(assignmentMap);
|
||||||
|
|
||||||
// Load employees from assignments
|
// Load employees from assignments
|
||||||
const employeeIds = Array.from(
|
const employeeIds = Array.from(
|
||||||
new Set((data || []).map((a) => a.employee_id))
|
new Set(
|
||||||
|
(existingDailyChecklist?.data.assigned_employees || []).map(
|
||||||
|
(a) => a.id
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
if (employeeIds.length > 0) {
|
|
||||||
const { data: empData, error: empError } = await supabase
|
|
||||||
.from('employees')
|
|
||||||
.select('id, name, kandang_id')
|
|
||||||
.in('id', employeeIds);
|
|
||||||
|
|
||||||
if (!empError && empData) {
|
if (employeeIds.length > 0) {
|
||||||
setSelectedEmployees(empData);
|
const existingDailyChecklist =
|
||||||
|
await DailyChecklistApi.getOneDailyChecklist(
|
||||||
|
String(dailyChecklistId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(existingDailyChecklist)) {
|
||||||
|
setSelectedEmployees(existingDailyChecklist.data.assigned_employees);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -380,68 +391,6 @@ export function DailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchKandang = 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 fetchAllPhases = async () => {
|
|
||||||
if (!isSupabaseConfigured()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('phases')
|
|
||||||
.select('id, name, category')
|
|
||||||
.order('id', { ascending: true }); // ✅ Urutan berdasarkan ID (yang paling awal diinput di atas)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching phases:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAllPhases(data || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching phases:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchEmployees = async (kandangId: string) => {
|
|
||||||
if (!isSupabaseConfigured()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('employees')
|
|
||||||
.select('id, name, kandang_id')
|
|
||||||
.eq('kandang_id', kandangId)
|
|
||||||
.eq('is_active', true)
|
|
||||||
.order('name', { ascending: true });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching employees:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEmployees(data || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching employees:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Phase selection modal
|
// Phase selection modal
|
||||||
const handleAddPhase = () => {
|
const handleAddPhase = () => {
|
||||||
if (!selectedCategory) {
|
if (!selectedCategory) {
|
||||||
@@ -464,31 +413,25 @@ export function DailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyPhaseSelection = async () => {
|
const applyPhaseSelection = async () => {
|
||||||
if (!dailyChecklistId || !isSupabaseConfigured()) {
|
if (!dailyChecklistId) {
|
||||||
toast.error('Checklist belum tersedia');
|
toast.error('Checklist belum tersedia');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete existing phase links
|
|
||||||
await supabase
|
|
||||||
.from('daily_checklist_phases')
|
|
||||||
.delete()
|
|
||||||
.eq('checklist_id', dailyChecklistId);
|
|
||||||
|
|
||||||
// Insert new phase links
|
// Insert new phase links
|
||||||
if (tempSelectedPhaseIds.length > 0) {
|
if (tempSelectedPhaseIds.length > 0) {
|
||||||
const phaseLinks = tempSelectedPhaseIds.map((phaseId) => ({
|
const setDailyChecklistPhaseRes =
|
||||||
checklist_id: dailyChecklistId,
|
await DailyChecklistApi.setDailyChecklistPhase(
|
||||||
phase_id: phaseId,
|
dailyChecklistId,
|
||||||
}));
|
tempSelectedPhaseIds
|
||||||
|
);
|
||||||
|
|
||||||
const { error } = await supabase
|
if (isResponseError(setDailyChecklistPhaseRes)) {
|
||||||
.from('daily_checklist_phases')
|
console.error(
|
||||||
.insert(phaseLinks);
|
'Error saving phases:',
|
||||||
|
setDailyChecklistPhaseRes.message
|
||||||
if (error) {
|
);
|
||||||
console.error('Error saving phases:', error);
|
|
||||||
toast.error('Gagal menyimpan fase');
|
toast.error('Gagal menyimpan fase');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -545,18 +488,23 @@ export function DailyChecklistContent() {
|
|||||||
(emp) => !tempSelectedEmployees.find((temp) => temp.id === emp.id)
|
(emp) => !tempSelectedEmployees.find((temp) => temp.id === emp.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (removedEmployees.length > 0 && isSupabaseConfigured()) {
|
if (removedEmployees.length > 0) {
|
||||||
const taskIds = Object.values(taskIdsByPhaseActivityId);
|
removedEmployees.forEach(async (removedEmp) => {
|
||||||
if (taskIds.length > 0) {
|
const removeEmployeeAssignmentRes =
|
||||||
await supabase
|
await DailyChecklistApi.removeEmployeeAssignment(
|
||||||
.from('daily_checklist_activity_task_assignments')
|
dailyChecklistId,
|
||||||
.delete()
|
String(removedEmp.id)
|
||||||
.in('task_id', taskIds)
|
|
||||||
.in(
|
|
||||||
'employee_id',
|
|
||||||
removedEmployees.map((e) => e.id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isResponseError(removeEmployeeAssignmentRes)) {
|
||||||
|
console.error(
|
||||||
|
'Error removing employee assignment:',
|
||||||
|
removeEmployeeAssignmentRes.message
|
||||||
|
);
|
||||||
|
toast.error('Gagal menghapus tugas');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Remove from state
|
// Remove from state
|
||||||
const newAssignments = { ...assignments };
|
const newAssignments = { ...assignments };
|
||||||
@@ -573,7 +521,7 @@ export function DailyChecklistContent() {
|
|||||||
(temp) => !selectedEmployees.find((emp) => emp.id === temp.id)
|
(temp) => !selectedEmployees.find((emp) => emp.id === temp.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addedEmployees.length > 0 && isSupabaseConfigured()) {
|
if (addedEmployees.length > 0) {
|
||||||
const taskIds = Object.values(taskIdsByPhaseActivityId);
|
const taskIds = Object.values(taskIdsByPhaseActivityId);
|
||||||
const newAssignments: {
|
const newAssignments: {
|
||||||
task_id: string;
|
task_id: string;
|
||||||
@@ -586,20 +534,23 @@ export function DailyChecklistContent() {
|
|||||||
addedEmployees.forEach((emp) => {
|
addedEmployees.forEach((emp) => {
|
||||||
newAssignments.push({
|
newAssignments.push({
|
||||||
task_id: taskId,
|
task_id: taskId,
|
||||||
employee_id: emp.id,
|
employee_id: String(emp.id),
|
||||||
checked: false,
|
checked: false,
|
||||||
note: null,
|
note: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newAssignments.length > 0) {
|
const assignEmployeeRes =
|
||||||
await supabase
|
await DailyChecklistApi.setDailyChecklistEmployees(
|
||||||
.from('daily_checklist_activity_task_assignments')
|
dailyChecklistId,
|
||||||
.upsert(newAssignments, {
|
addedEmployees.map((emp) => String(emp.id))
|
||||||
onConflict: 'task_id,employee_id',
|
);
|
||||||
ignoreDuplicates: false,
|
|
||||||
});
|
if (isResponseError(assignEmployeeRes)) {
|
||||||
|
console.error('Error assigning employees:', assignEmployeeRes.message);
|
||||||
|
toast.error('Gagal mengassign ABK: ' + assignEmployeeRes.message);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,16 +561,15 @@ export function DailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAbk = async (employeeId: string) => {
|
const handleRemoveAbk = async (employeeId: string) => {
|
||||||
if (!isSupabaseConfigured()) return;
|
const deleteEmployeeRes = await DailyChecklistApi.removeEmployeeAssignment(
|
||||||
|
String(dailyChecklistId),
|
||||||
|
String(employeeId)
|
||||||
|
);
|
||||||
|
|
||||||
// Delete assignments for this employee
|
if (isResponseError(deleteEmployeeRes)) {
|
||||||
const taskIds = Object.values(taskIdsByPhaseActivityId);
|
console.error('Error deleting employee:', deleteEmployeeRes.message);
|
||||||
if (taskIds.length > 0) {
|
toast.error('Gagal menghapus ABK: ' + deleteEmployeeRes.message);
|
||||||
await supabase
|
return;
|
||||||
.from('daily_checklist_activity_task_assignments')
|
|
||||||
.delete()
|
|
||||||
.in('task_id', taskIds)
|
|
||||||
.eq('employee_id', employeeId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from state
|
// Remove from state
|
||||||
@@ -629,7 +579,9 @@ export function DailyChecklistContent() {
|
|||||||
});
|
});
|
||||||
setAssignments(newAssignments);
|
setAssignments(newAssignments);
|
||||||
|
|
||||||
setSelectedEmployees(selectedEmployees.filter((e) => e.id !== employeeId));
|
setSelectedEmployees(
|
||||||
|
selectedEmployees.filter((e) => String(e.id) !== employeeId)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckboxChange = async (
|
const handleCheckboxChange = async (
|
||||||
@@ -645,7 +597,6 @@ export function DailyChecklistContent() {
|
|||||||
checked,
|
checked,
|
||||||
taskId,
|
taskId,
|
||||||
hasTaskId: !!taskId,
|
hasTaskId: !!taskId,
|
||||||
isSupabaseConfigured: isSupabaseConfigured(),
|
|
||||||
checklistStatus,
|
checklistStatus,
|
||||||
isEditable,
|
isEditable,
|
||||||
});
|
});
|
||||||
@@ -657,12 +608,6 @@ export function DailyChecklistContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSupabaseConfigured()) {
|
|
||||||
console.error('[CHECKBOX] Supabase not configured');
|
|
||||||
toast.error('Database tidak terkonfigurasi');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isEditable) {
|
if (!isEditable) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[CHECKBOX] Checklist is not editable, status:',
|
'[CHECKBOX] Checklist is not editable, status:',
|
||||||
@@ -692,24 +637,23 @@ export function DailyChecklistContent() {
|
|||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
const payload = {
|
const payload = {
|
||||||
task_id: taskId,
|
task_id: Number(taskId),
|
||||||
employee_id: employeeId,
|
employee_id: Number(employeeId),
|
||||||
checked,
|
checked,
|
||||||
note: assignments[taskId]?.[employeeId]?.note || null,
|
note: assignments[taskId]?.[employeeId]?.note || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[CHECKBOX] Saving to database:', payload);
|
console.log('[CHECKBOX] Saving to database:', payload);
|
||||||
|
|
||||||
const { error } = await supabase
|
const checkOrUncheckAssignmentRes =
|
||||||
.from('daily_checklist_activity_task_assignments')
|
await DailyChecklistApi.checkOrUncheckAssignment(payload);
|
||||||
.upsert(payload, {
|
|
||||||
onConflict: 'task_id,employee_id',
|
|
||||||
ignoreDuplicates: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(checkOrUncheckAssignmentRes)) {
|
||||||
console.error('[CHECKBOX] Database error:', error);
|
console.error(
|
||||||
toast.error('Gagal menyimpan: ' + error.message);
|
'[CHECKBOX] Database error:',
|
||||||
|
checkOrUncheckAssignmentRes.message
|
||||||
|
);
|
||||||
|
toast.error('Gagal menyimpan: ' + checkOrUncheckAssignmentRes.message);
|
||||||
|
|
||||||
// Revert state on error
|
// Revert state on error
|
||||||
setAssignments((prev) => ({
|
setAssignments((prev) => ({
|
||||||
@@ -734,26 +678,25 @@ export function DailyChecklistContent() {
|
|||||||
note: string
|
note: string
|
||||||
) => {
|
) => {
|
||||||
const taskId = taskIdsByPhaseActivityId[activityId];
|
const taskId = taskIdsByPhaseActivityId[activityId];
|
||||||
if (!taskId || !isSupabaseConfigured()) return;
|
if (!taskId) return;
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
const { error } = await supabase
|
const payload = {
|
||||||
.from('daily_checklist_activity_task_assignments')
|
task_id: Number(taskId),
|
||||||
.upsert(
|
employee_id: Number(employeeId),
|
||||||
{
|
|
||||||
task_id: taskId,
|
|
||||||
employee_id: employeeId,
|
|
||||||
checked: assignments[taskId]?.[employeeId]?.checked || false,
|
checked: assignments[taskId]?.[employeeId]?.checked || false,
|
||||||
note: note || null,
|
note: note || null,
|
||||||
},
|
};
|
||||||
{
|
|
||||||
onConflict: 'task_id,employee_id',
|
|
||||||
ignoreDuplicates: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
const checkOrUncheckAssignmentRes =
|
||||||
console.error('Error updating note:', error);
|
await DailyChecklistApi.checkOrUncheckAssignment(payload);
|
||||||
|
|
||||||
|
if (isResponseError(checkOrUncheckAssignmentRes)) {
|
||||||
|
console.error(
|
||||||
|
'[CHECKBOX] Database error:',
|
||||||
|
checkOrUncheckAssignmentRes.message
|
||||||
|
);
|
||||||
|
toast.error('Gagal menyimpan: ' + checkOrUncheckAssignmentRes.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,7 +714,7 @@ export function DailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!dailyChecklistId || !isSupabaseConfigured()) return;
|
if (!dailyChecklistId) return;
|
||||||
|
|
||||||
if (selectedEmployees.length === 0) {
|
if (selectedEmployees.length === 0) {
|
||||||
toast.error('Pilih minimal 1 ABK');
|
toast.error('Pilih minimal 1 ABK');
|
||||||
@@ -786,17 +729,10 @@ export function DailyChecklistContent() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update status to SUBMITTED
|
const submitRes = await DailyChecklistApi.submit(dailyChecklistId);
|
||||||
const { error } = await supabase
|
|
||||||
.from('daily_checklists')
|
|
||||||
.update({
|
|
||||||
status: 'SUBMITTED',
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', dailyChecklistId);
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(submitRes)) {
|
||||||
console.error('Error submitting:', error);
|
console.error('Error submitting:', submitRes.message);
|
||||||
toast.error('Gagal submit checklist');
|
toast.error('Gagal submit checklist');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -841,13 +777,13 @@ export function DailyChecklistContent() {
|
|||||||
[phaseId: string]: {
|
[phaseId: string]: {
|
||||||
phase: Phase;
|
phase: Phase;
|
||||||
timeGroups: {
|
timeGroups: {
|
||||||
[timeType: string]: Activity[];
|
[timeType: string]: PhaseActivity[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const selectedPhasesData = allPhases.filter((p) =>
|
const selectedPhasesData = allPhases.filter((p) =>
|
||||||
selectedPhaseIds.includes(p.id)
|
selectedPhaseIds.includes(String(p.id))
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedPhasesData.forEach((phase) => {
|
selectedPhasesData.forEach((phase) => {
|
||||||
@@ -900,7 +836,7 @@ export function DailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen bg-[#F9FAFB]'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div className='mb-6'>
|
<div className='mb-6'>
|
||||||
@@ -967,9 +903,12 @@ export function DailyChecklistContent() {
|
|||||||
<SelectValue placeholder='Pilih kandang' />
|
<SelectValue placeholder='Pilih kandang' />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{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>
|
||||||
@@ -1022,7 +961,7 @@ export function DailyChecklistContent() {
|
|||||||
{selectedPhaseIds.length > 0 ? (
|
{selectedPhaseIds.length > 0 ? (
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{allPhases
|
{allPhases
|
||||||
.filter((p) => selectedPhaseIds.includes(p.id))
|
.filter((p) => selectedPhaseIds.includes(String(p.id)))
|
||||||
.map((phase) => (
|
.map((phase) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={phase.id}
|
key={phase.id}
|
||||||
@@ -1069,7 +1008,7 @@ export function DailyChecklistContent() {
|
|||||||
{emp.name}
|
{emp.name}
|
||||||
{isEditable && (
|
{isEditable && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveAbk(emp.id)}
|
onClick={() => handleRemoveAbk(String(emp.id))}
|
||||||
className='ml-2 hover:text-gray-900'
|
className='ml-2 hover:text-gray-900'
|
||||||
>
|
>
|
||||||
<X className='w-3 h-3' />
|
<X className='w-3 h-3' />
|
||||||
@@ -1226,8 +1165,8 @@ export function DailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleCheckboxChange(
|
handleCheckboxChange(
|
||||||
activity.id,
|
String(activity.id),
|
||||||
emp.id,
|
String(emp.id),
|
||||||
e.target.checked
|
e.target.checked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1237,7 +1176,11 @@ export function DailyChecklistContent() {
|
|||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
<td className='py-3 px-4'>
|
<td className='py-3 px-4'>
|
||||||
<Textarea
|
<DebouncedTextArea
|
||||||
|
delay={500}
|
||||||
|
name='notes'
|
||||||
|
rows={1}
|
||||||
|
placeholder='Catatan (opsional)'
|
||||||
value={
|
value={
|
||||||
taskId && selectedEmployees.length > 0
|
taskId && selectedEmployees.length > 0
|
||||||
? assignments[taskId]?.[
|
? assignments[taskId]?.[
|
||||||
@@ -1248,16 +1191,13 @@ export function DailyChecklistContent() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (selectedEmployees.length > 0) {
|
if (selectedEmployees.length > 0) {
|
||||||
handleNoteChange(
|
handleNoteChange(
|
||||||
activity.id,
|
String(activity.id),
|
||||||
selectedEmployees[0].id,
|
String(selectedEmployees[0].id),
|
||||||
e.target.value
|
e.target.value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder='Catatan (opsional)'
|
|
||||||
disabled={!isEditable}
|
disabled={!isEditable}
|
||||||
className='text-sm min-h-[36px] resize-none'
|
|
||||||
rows={1}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1401,7 +1341,9 @@ export function DailyChecklistContent() {
|
|||||||
{filteredPhases.length > 0 ? (
|
{filteredPhases.length > 0 ? (
|
||||||
<div className='space-y-1.5'>
|
<div className='space-y-1.5'>
|
||||||
{filteredPhases.map((phase) => {
|
{filteredPhases.map((phase) => {
|
||||||
const isChecked = tempSelectedPhaseIds.includes(phase.id);
|
const isChecked = tempSelectedPhaseIds.includes(
|
||||||
|
String(phase.id)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -1415,7 +1357,7 @@ export function DailyChecklistContent() {
|
|||||||
<input
|
<input
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
onChange={() => toggleTempPhase(phase.id)}
|
onChange={() => toggleTempPhase(String(phase.id))}
|
||||||
className='checkbox-clean mt-0.5'
|
className='checkbox-clean mt-0.5'
|
||||||
/>
|
/>
|
||||||
<div className='flex-1 min-w-0'>
|
<div className='flex-1 min-w-0'>
|
||||||
@@ -1507,9 +1449,22 @@ export function DailyChecklistContent() {
|
|||||||
const isChecked = tempSelectedEmployees.find(
|
const isChecked = tempSelectedEmployees.find(
|
||||||
(e) => e.id === emp.id
|
(e) => e.id === emp.id
|
||||||
);
|
);
|
||||||
const kandang = kandangList.find(
|
// const kandang = kandangOptions.find((k) => {
|
||||||
(k) => k.id === emp.kandang_id
|
// const formattedKandangIds = emp.kandangs.map((empKandang) =>
|
||||||
);
|
// String(empKandang.id)
|
||||||
|
// );
|
||||||
|
// return formattedKandangIds.includes(String(k.value));
|
||||||
|
// });
|
||||||
|
|
||||||
|
const kandang = emp.kandangs
|
||||||
|
.map((empKandang) => {
|
||||||
|
if (String(empKandang.id) === kandangId) {
|
||||||
|
return `<b>${empKandang.name}</b>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return empKandang.name;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -1532,7 +1487,9 @@ export function DailyChecklistContent() {
|
|||||||
</p>
|
</p>
|
||||||
{kandang && (
|
{kandang && (
|
||||||
<p className='text-xs text-gray-500 mt-0.5'>
|
<p className='text-xs text-gray-500 mt-0.5'>
|
||||||
{kandang.name}
|
<span
|
||||||
|
dangerouslySetInnerHTML={{ __html: kandang }}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+261
-437
@@ -1,11 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Eye, CheckCircle, XCircle, Search, Trash2 } from 'lucide-react';
|
import { Eye, CheckCircle, XCircle, Search, Trash2 } 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 { 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 { Textarea } from '@/figma-make/components/base/textarea';
|
import { Textarea } from '@/figma-make/components/base/textarea';
|
||||||
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
|
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
|
||||||
@@ -25,40 +24,24 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/figma-make/components/base/dialog';
|
} from '@/figma-make/components/base/dialog';
|
||||||
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 useSWR from 'swr';
|
||||||
interface ChecklistItem {
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
checklist_id: string;
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
date: string;
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
kandang_name: string;
|
import Table from '@/components/Table';
|
||||||
kandang_id: string; // ✅ Add kandang_id
|
import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
|
||||||
category: string;
|
import { cn } from '@/lib/helper';
|
||||||
status: string;
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
progress_percent: number;
|
import { useSelect } from '@/components/input/SelectInput';
|
||||||
total_phases: number;
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
total_activities: number;
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Kandang {
|
interface Kandang {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChecklistQueryResult {
|
|
||||||
id: string;
|
|
||||||
date: string;
|
|
||||||
kandang_id: string;
|
|
||||||
category: string;
|
|
||||||
status: string;
|
|
||||||
updated_at: string;
|
|
||||||
kandang: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: 'ALL', label: 'Semua Status' },
|
{ value: 'ALL', label: 'Semua Status' },
|
||||||
{ value: 'DRAFT', label: 'Draft' },
|
{ value: 'DRAFT', label: 'Draft' },
|
||||||
@@ -76,244 +59,80 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
|
|
||||||
export function ListDailyChecklistContent() {
|
export function ListDailyChecklistContent() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [checklistList, setChecklistList] = useState<ChecklistItem[]>([]);
|
|
||||||
const [filteredList, setFilteredList] = useState<ChecklistItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// Master data
|
const {
|
||||||
const [kandangList, setKandangList] = useState<Kandang[]>([]);
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
date_from: '',
|
||||||
|
date_to: '',
|
||||||
|
search: '',
|
||||||
|
kandang_id: '',
|
||||||
|
status: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
search: 'search',
|
||||||
|
kandang_id: 'kandang_id',
|
||||||
|
status: 'status',
|
||||||
|
date_from: 'date_from',
|
||||||
|
date_to: 'date_to',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Filters
|
const {
|
||||||
const [statusFilter, setStatusFilter] = useState('ALL');
|
data: checklistListRes,
|
||||||
const [kandangFilter, setKandangFilter] = useState('ALL');
|
isLoading: isLoadingChecklistList,
|
||||||
const [searchText, setSearchText] = useState('');
|
mutate: refreshChecklistList,
|
||||||
const [dateFrom, setDateFrom] = useState('');
|
} = useSWR(
|
||||||
const [dateTo, setDateTo] = useState('');
|
`${DailyChecklistApi.basePath}${getTableFilterQueryString()}`,
|
||||||
|
DailyChecklistApi.getAllFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
||||||
|
useSelect(KandangApi.basePath, 'id', 'name', 'search', {
|
||||||
|
page: '1',
|
||||||
|
limit: '100',
|
||||||
|
});
|
||||||
|
|
||||||
|
const checklistList = isResponseSuccess(checklistListRes)
|
||||||
|
? checklistListRes.data || []
|
||||||
|
: [];
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [showApproveModal, setShowApproveModal] = useState(false);
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<ChecklistItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<DailyChecklist | null>(null);
|
||||||
const [rejectReason, setRejectReason] = useState('');
|
const [rejectReason, setRejectReason] = useState('');
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleDetail = (item: DailyChecklist) => {
|
||||||
fetchKandangList();
|
|
||||||
fetchChecklistList();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
applyFilters();
|
|
||||||
}, [
|
|
||||||
checklistList,
|
|
||||||
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 fetchChecklistList = async () => {
|
|
||||||
if (!isSupabaseConfigured()) {
|
|
||||||
console.warn('Supabase not configured');
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// ✅ Fetch checklists with joins to get complete data
|
|
||||||
const { data: checklists, error } = await supabase
|
|
||||||
.from('daily_checklists')
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
id,
|
|
||||||
date,
|
|
||||||
kandang_id,
|
|
||||||
category,
|
|
||||||
status,
|
|
||||||
updated_at,
|
|
||||||
kandang:kandang_id (
|
|
||||||
id,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.order('date', { ascending: false })
|
|
||||||
.order('updated_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error fetching checklist list:', error);
|
|
||||||
toast.error('Gagal memuat data checklist');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ For each checklist, fetch phases, activities, and assignments count
|
|
||||||
const enrichedData: ChecklistItem[] = await Promise.all(
|
|
||||||
((checklists as unknown as ChecklistQueryResult[]) || [])
|
|
||||||
.filter((checklist) => checklist.id) // ✅ Skip checklists with null 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);
|
|
||||||
|
|
||||||
// ✅ NEW LOGIC: Calculate progress based on phase coverage
|
|
||||||
// Step 1: Get total phases in master data for this category
|
|
||||||
const { count: totalPhasesInMaster } = await supabase
|
|
||||||
.from('phases')
|
|
||||||
.select('*', { count: 'exact', head: true })
|
|
||||||
.eq('category_id', checklist.category);
|
|
||||||
|
|
||||||
// Step 2: Get phases that have at least 1 CHECKED assignment
|
|
||||||
// First, get all tasks for this checklist
|
|
||||||
const { data: tasks } = await supabase
|
|
||||||
.from('daily_checklist_activity_tasks')
|
|
||||||
.select('id, phase_id')
|
|
||||||
.eq('checklist_id', checklist.id);
|
|
||||||
|
|
||||||
const taskIds = (tasks || []).map((t) => t.id);
|
|
||||||
const uniquePhasesWithChecked = new Set<string>();
|
|
||||||
|
|
||||||
if (taskIds.length > 0) {
|
|
||||||
// Get assignments that are CHECKED
|
|
||||||
const { data: checkedAssignments } = await supabase
|
|
||||||
.from('daily_checklist_activity_task_assignments')
|
|
||||||
.select('task_id')
|
|
||||||
.in('task_id', taskIds)
|
|
||||||
.eq('checked', true); // ✅ Only get checked assignments
|
|
||||||
|
|
||||||
if (checkedAssignments && checkedAssignments.length > 0) {
|
|
||||||
// Map task_ids back to phase_ids
|
|
||||||
const checkedTaskIds = new Set(
|
|
||||||
checkedAssignments.map((a) => a.task_id)
|
|
||||||
);
|
|
||||||
tasks?.forEach((task) => {
|
|
||||||
if (checkedTaskIds.has(task.id)) {
|
|
||||||
uniquePhasesWithChecked.add(task.phase_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const phasesWithCheckedCount = uniquePhasesWithChecked.size;
|
|
||||||
|
|
||||||
// Step 3: Calculate progress
|
|
||||||
const progressPercent =
|
|
||||||
totalPhasesInMaster && totalPhasesInMaster > 0
|
|
||||||
? Math.round(
|
|
||||||
(phasesWithCheckedCount / totalPhasesInMaster) * 100
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
checklist_id: checklist.id,
|
|
||||||
date: checklist.date,
|
|
||||||
kandang_name: checklist.kandang?.name || '-',
|
|
||||||
kandang_id: checklist.kandang_id,
|
|
||||||
category: checklist.category,
|
|
||||||
status: checklist.status,
|
|
||||||
progress_percent: progressPercent,
|
|
||||||
total_phases: phaseCount || 0,
|
|
||||||
total_activities: activityCount || 0,
|
|
||||||
updated_at: checklist.updated_at,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setChecklistList(enrichedData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching checklist list:', error);
|
|
||||||
toast.error('Terjadi kesalahan');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyFilters = () => {
|
|
||||||
let filtered = [...checklistList];
|
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
if (statusFilter && statusFilter !== 'ALL') {
|
|
||||||
filtered = filtered.filter((item) => item.status === statusFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Filter by kandang - use kandang_id directly from item
|
|
||||||
if (kandangFilter && kandangFilter !== 'ALL') {
|
|
||||||
filtered = filtered.filter((item) => item.kandang_id === kandangFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by search text (kandang_name or category)
|
|
||||||
if (searchText) {
|
|
||||||
const searchLower = searchText.toLowerCase();
|
|
||||||
filtered = filtered.filter(
|
|
||||||
(item) =>
|
|
||||||
item.kandang_name.toLowerCase().includes(searchLower) ||
|
|
||||||
item.category.toLowerCase().includes(searchLower) ||
|
|
||||||
(CATEGORY_LABELS[item.category] || '')
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchLower)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by date range
|
|
||||||
if (dateFrom) {
|
|
||||||
filtered = filtered.filter((item) => item.date >= dateFrom);
|
|
||||||
}
|
|
||||||
if (dateTo) {
|
|
||||||
filtered = filtered.filter((item) => item.date <= dateTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredList(filtered);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDetail = (item: ChecklistItem) => {
|
|
||||||
router.push(
|
router.push(
|
||||||
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.checklist_id}`
|
`/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = (item: ChecklistItem) => {
|
const handleApprove = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setShowApproveModal(true);
|
setShowApproveModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReject = (item: ChecklistItem) => {
|
const handleReject = (item: DailyChecklist) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
setShowRejectModal(true);
|
setShowRejectModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (item: ChecklistItem) => {
|
const handleDelete = (item: DailyChecklist) => {
|
||||||
// ✅ VALIDATION: Only DRAFT can be deleted
|
// ✅ VALIDATION: Only DRAFT can be deleted
|
||||||
if (item.status !== 'DRAFT') {
|
if (item.status !== 'DRAFT') {
|
||||||
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
toast.error('Hanya checklist dengan status DRAFT yang bisa dihapus', {
|
||||||
@@ -327,29 +146,24 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprove = async () => {
|
const confirmApprove = async () => {
|
||||||
if (!selectedItem || !isSupabaseConfigured()) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
|
|
||||||
const { error } = await supabase
|
const approveRes = await DailyChecklistApi.approve(
|
||||||
.from('daily_checklists')
|
String(selectedItem.id)
|
||||||
.update({
|
);
|
||||||
status: 'APPROVED',
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', selectedItem.checklist_id);
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(approveRes)) {
|
||||||
console.error('Error approving checklist:', error);
|
toast.error('Gagal approve checklist: ' + approveRes.message);
|
||||||
toast.error('Gagal approve checklist');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshChecklistList();
|
||||||
toast.success('Checklist berhasil di-approve');
|
toast.success('Checklist berhasil di-approve');
|
||||||
setShowApproveModal(false);
|
setShowApproveModal(false);
|
||||||
setSelectedItem(null);
|
setSelectedItem(null);
|
||||||
await fetchChecklistList();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error approving checklist:', error);
|
console.error('Error approving checklist:', error);
|
||||||
toast.error('Terjadi kesalahan');
|
toast.error('Terjadi kesalahan');
|
||||||
@@ -359,7 +173,7 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmReject = async () => {
|
const confirmReject = async () => {
|
||||||
if (!selectedItem || !isSupabaseConfigured()) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
if (!rejectReason.trim()) {
|
if (!rejectReason.trim()) {
|
||||||
toast.error('Alasan reject harus diisi');
|
toast.error('Alasan reject harus diisi');
|
||||||
@@ -369,26 +183,21 @@ export function ListDailyChecklistContent() {
|
|||||||
try {
|
try {
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
|
|
||||||
const { error } = await supabase
|
const rejectRes = await DailyChecklistApi.reject(
|
||||||
.from('daily_checklists')
|
String(selectedItem.id),
|
||||||
.update({
|
rejectReason
|
||||||
status: 'REJECTED',
|
);
|
||||||
reject_reason: rejectReason,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', selectedItem.checklist_id);
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(rejectRes)) {
|
||||||
console.error('Error rejecting checklist:', error);
|
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
||||||
toast.error('Gagal reject checklist');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshChecklistList();
|
||||||
toast.success('Checklist berhasil di-reject');
|
toast.success('Checklist berhasil di-reject');
|
||||||
setShowRejectModal(false);
|
setShowRejectModal(false);
|
||||||
setSelectedItem(null);
|
setSelectedItem(null);
|
||||||
setRejectReason('');
|
setRejectReason('');
|
||||||
await fetchChecklistList();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rejecting checklist:', error);
|
console.error('Error rejecting checklist:', error);
|
||||||
toast.error('Terjadi kesalahan');
|
toast.error('Terjadi kesalahan');
|
||||||
@@ -398,26 +207,22 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
if (!selectedItem || !isSupabaseConfigured()) return;
|
if (!selectedItem) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
|
|
||||||
const { error } = await supabase
|
const deleteRes = await DailyChecklistApi.delete(selectedItem.id);
|
||||||
.from('daily_checklists')
|
|
||||||
.delete()
|
|
||||||
.eq('id', selectedItem.checklist_id);
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(deleteRes)) {
|
||||||
console.error('Error deleting checklist:', error);
|
toast.error('Gagal hapus checklist: ' + deleteRes.message);
|
||||||
toast.error('Gagal hapus checklist');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshChecklistList();
|
||||||
toast.success('Checklist berhasil dihapus');
|
toast.success('Checklist berhasil dihapus');
|
||||||
setShowDeleteModal(false);
|
setShowDeleteModal(false);
|
||||||
setSelectedItem(null);
|
setSelectedItem(null);
|
||||||
await fetchChecklistList();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting checklist:', error);
|
console.error('Error deleting checklist:', error);
|
||||||
toast.error('Terjadi kesalahan');
|
toast.error('Terjadi kesalahan');
|
||||||
@@ -496,6 +301,117 @@ export function ListDailyChecklistContent() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checklistListColumns: ColumnDef<DailyChecklist>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: 'Tanggal',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => formatDate(row.original.date),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'kandang',
|
||||||
|
header: 'Kandang',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => row.original.kandang.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'category',
|
||||||
|
header: 'Kategori',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
CATEGORY_LABELS[row.original.category] || row.original.category,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => getStatusBadge(row.original.status),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'total_phase',
|
||||||
|
header: 'Total Phase',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'total_activity',
|
||||||
|
header: 'Total Aktivitas',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'progress',
|
||||||
|
header: 'Progress',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className='flex items-center justify-center gap-2'>
|
||||||
|
<div className='w-24 bg-gray-200 rounded-full h-2'>
|
||||||
|
<div
|
||||||
|
className='bg-[#0069e0] h-2 rounded-full transition-all'
|
||||||
|
style={{ width: `${row.original.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className='text-sm text-gray-700 font-medium'>
|
||||||
|
{row.original.progress}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'updated_at',
|
||||||
|
header: 'Update At',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => formatDateTime(row.original.updated_at),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'action',
|
||||||
|
header: 'Aksi',
|
||||||
|
accessorKey: 'action',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className='flex items-center justify-center gap-2'>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => handleDetail(row.original)}
|
||||||
|
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
||||||
|
>
|
||||||
|
<Eye className='w-4 h-4 mr-1' />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
{row.original.status === 'SUBMITTED' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
onClick={() => handleApprove(row.original)}
|
||||||
|
className='bg-green-600 hover:bg-green-700 text-white'
|
||||||
|
>
|
||||||
|
<CheckCircle className='w-4 h-4 mr-1' />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='destructive'
|
||||||
|
onClick={() => handleReject(row.original)}
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
<XCircle className='w-4 h-4 mr-1' />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='destructive'
|
||||||
|
onClick={() => handleDelete(row.original)}
|
||||||
|
className='bg-red-600 hover:bg-red-700 text-white'
|
||||||
|
>
|
||||||
|
<Trash2 className='w-4 h-4 mr-1' />
|
||||||
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen'>
|
<div className='min-h-screen'>
|
||||||
<div className='p-6'>
|
<div className='p-6'>
|
||||||
@@ -518,11 +434,11 @@ export function ListDailyChecklistContent() {
|
|||||||
<Label>Periode Tanggal</Label>
|
<Label>Periode Tanggal</Label>
|
||||||
<div className='mt-1.5'>
|
<div className='mt-1.5'>
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
dateFrom={dateFrom}
|
dateFrom={tableFilterState.date_from}
|
||||||
dateTo={dateTo}
|
dateTo={tableFilterState.date_to}
|
||||||
onDateChange={(from, to) => {
|
onDateChange={(from, to) => {
|
||||||
setDateFrom(from);
|
updateFilter('date_from', from);
|
||||||
setDateTo(to);
|
updateFilter('date_to', to);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -532,8 +448,10 @@ export function ListDailyChecklistContent() {
|
|||||||
<Label htmlFor='kandang-filter'>Kandang</Label>
|
<Label htmlFor='kandang-filter'>Kandang</Label>
|
||||||
<div className='mt-1.5'>
|
<div className='mt-1.5'>
|
||||||
<Select
|
<Select
|
||||||
value={kandangFilter}
|
value={tableFilterState.kandang_id}
|
||||||
onValueChange={setKandangFilter}
|
onValueChange={(value) => {
|
||||||
|
updateFilter('kandang_id', value === 'ALL' ? '' : value);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id='kandang-filter'
|
id='kandang-filter'
|
||||||
@@ -543,9 +461,12 @@ export function ListDailyChecklistContent() {
|
|||||||
</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>
|
||||||
@@ -556,7 +477,12 @@ export function ListDailyChecklistContent() {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor='status-filter'>Status</Label>
|
<Label htmlFor='status-filter'>Status</Label>
|
||||||
<div className='mt-1.5'>
|
<div className='mt-1.5'>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<Select
|
||||||
|
value={tableFilterState.status}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
updateFilter('status', value === 'ALL' ? '' : value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id='status-filter'
|
id='status-filter'
|
||||||
className='border-gray-200'
|
className='border-gray-200'
|
||||||
@@ -577,159 +503,57 @@ export function ListDailyChecklistContent() {
|
|||||||
<div>
|
<div>
|
||||||
<Label htmlFor='search-text'>Cari</Label>
|
<Label htmlFor='search-text'>Cari</Label>
|
||||||
<div className='relative mt-1.5'>
|
<div className='relative mt-1.5'>
|
||||||
<Input
|
<DebouncedTextInput
|
||||||
id='search-text'
|
name='search'
|
||||||
type='text'
|
placeholder='Kandang / Kategori'
|
||||||
placeholder='Kandang / Kategori...'
|
value={tableFilterState.search}
|
||||||
value={searchText}
|
onChange={(e) => updateFilter('search', e.target.value)}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
className={{
|
||||||
className='border-gray-200 pl-9'
|
wrapper: 'w-full border-gray-200',
|
||||||
|
inputWrapper: 'px-3 py-2 h-fit rounded-md',
|
||||||
|
input: 'text-sm',
|
||||||
|
}}
|
||||||
|
startAdornment={
|
||||||
|
<Search className='text-gray-400 w-4 h-4' />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Search className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400' />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Section */}
|
{/* Table Section */}
|
||||||
{loading ? (
|
<Table<DailyChecklist>
|
||||||
<div className='text-center py-12 text-gray-500'>
|
data={checklistList}
|
||||||
Memuat data...
|
columns={checklistListColumns}
|
||||||
</div>
|
pageSize={tableFilterState.pageSize}
|
||||||
) : filteredList.length > 0 ? (
|
onPageSizeChange={setPageSize}
|
||||||
<div className='overflow-x-auto'>
|
rowOptions={[10, 20, 50, 100]}
|
||||||
<table className='w-full border border-gray-200 rounded-lg'>
|
page={
|
||||||
<thead>
|
isResponseSuccess(checklistListRes)
|
||||||
<tr className='bg-gray-50 border-b border-gray-200'>
|
? checklistListRes?.meta?.page
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
: 0
|
||||||
Tanggal
|
|
||||||
</th>
|
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Kandang
|
|
||||||
</th>
|
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Kategori
|
|
||||||
</th>
|
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Total Phase
|
|
||||||
</th>
|
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Total Aktivitas
|
|
||||||
</th>
|
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Progress
|
|
||||||
</th>
|
|
||||||
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Updated At
|
|
||||||
</th>
|
|
||||||
<th className='text-center py-3 px-4 text-sm font-semibold text-gray-700'>
|
|
||||||
Aksi
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredList.map((item, index) => (
|
|
||||||
<tr
|
|
||||||
key={`${item.checklist_id}-${index}`}
|
|
||||||
className={
|
|
||||||
index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'
|
|
||||||
}
|
}
|
||||||
>
|
totalItems={
|
||||||
<td className='py-3 px-4 text-sm text-gray-900'>
|
isResponseSuccess(checklistListRes)
|
||||||
{formatDate(item.date)}
|
? checklistListRes?.meta?.total_results
|
||||||
</td>
|
: 0
|
||||||
<td className='py-3 px-4 text-sm text-gray-900'>
|
}
|
||||||
{item.kandang_name}
|
onPageChange={setPage}
|
||||||
</td>
|
isLoading={isLoadingChecklistList}
|
||||||
<td className='py-3 px-4 text-sm text-gray-900'>
|
className={{
|
||||||
{CATEGORY_LABELS[item.category] || item.category}
|
containerClassName: cn({
|
||||||
</td>
|
'w-full mb-20':
|
||||||
<td className='py-3 px-4'>
|
isResponseSuccess(checklistListRes) &&
|
||||||
{getStatusBadge(item.status)}
|
checklistListRes?.data?.length === 0,
|
||||||
</td>
|
}),
|
||||||
<td className='py-3 px-4 text-center text-sm text-gray-900'>
|
tableWrapperClassName:
|
||||||
{item.total_phases}
|
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
|
||||||
</td>
|
headerRowClassName: 'bg-gray-50/50',
|
||||||
<td className='py-3 px-4 text-center text-sm text-gray-900'>
|
headerColumnClassName:
|
||||||
{item.total_activities}
|
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
|
||||||
</td>
|
paginationClassName: 'px-4',
|
||||||
<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='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 gap-2'>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => handleDetail(item)}
|
|
||||||
className='border-gray-200 text-gray-700 hover:bg-gray-50'
|
|
||||||
>
|
|
||||||
<Eye className='w-4 h-4 mr-1' />
|
|
||||||
Detail
|
|
||||||
</Button>
|
|
||||||
{item.status === 'SUBMITTED' && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
onClick={() => handleApprove(item)}
|
|
||||||
className='bg-green-600 hover:bg-green-700 text-white'
|
|
||||||
>
|
|
||||||
<CheckCircle className='w-4 h-4 mr-1' />
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='destructive'
|
|
||||||
onClick={() => handleReject(item)}
|
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
|
||||||
>
|
|
||||||
<XCircle className='w-4 h-4 mr-1' />
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='destructive'
|
|
||||||
onClick={() => handleDelete(item)}
|
|
||||||
className='bg-red-600 hover:bg-red-700 text-white'
|
|
||||||
>
|
|
||||||
<Trash2 className='w-4 h-4 mr-1' />
|
|
||||||
Hapus
|
|
||||||
</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>
|
||||||
@@ -755,7 +579,7 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='flex justify-between text-sm'>
|
<div className='flex justify-between text-sm'>
|
||||||
<span className='text-gray-600'>Kandang:</span>
|
<span className='text-gray-600'>Kandang:</span>
|
||||||
<span className='font-medium text-gray-900'>
|
<span className='font-medium text-gray-900'>
|
||||||
{selectedItem.kandang_name}
|
{selectedItem.kandang.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between text-sm'>
|
<div className='flex justify-between text-sm'>
|
||||||
@@ -768,7 +592,7 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='flex justify-between text-sm'>
|
<div className='flex justify-between text-sm'>
|
||||||
<span className='text-gray-600'>Progress:</span>
|
<span className='text-gray-600'>Progress:</span>
|
||||||
<span className='font-medium text-gray-900'>
|
<span className='font-medium text-gray-900'>
|
||||||
{selectedItem.progress_percent}%
|
{selectedItem.progress}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -815,7 +639,7 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='flex justify-between text-sm'>
|
<div className='flex justify-between text-sm'>
|
||||||
<span className='text-gray-600'>Kandang:</span>
|
<span className='text-gray-600'>Kandang:</span>
|
||||||
<span className='font-medium text-gray-900'>
|
<span className='font-medium text-gray-900'>
|
||||||
{selectedItem.kandang_name}
|
{selectedItem.kandang.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between text-sm'>
|
<div className='flex justify-between text-sm'>
|
||||||
@@ -888,7 +712,7 @@ export function ListDailyChecklistContent() {
|
|||||||
<div className='flex justify-between text-sm'>
|
<div className='flex justify-between text-sm'>
|
||||||
<span className='text-gray-600'>Kandang:</span>
|
<span className='text-gray-600'>Kandang:</span>
|
||||||
<span className='font-medium text-gray-900'>
|
<span className='font-medium text-gray-900'>
|
||||||
{selectedItem.kandang_name}
|
{selectedItem.kandang.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between text-sm'>
|
<div className='flex justify-between text-sm'>
|
||||||
|
|||||||
+67
-134
@@ -17,8 +17,9 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/figma-make/components/base/dialog';
|
} from '@/figma-make/components/base/dialog';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
|
||||||
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
|
|
||||||
interface ChecklistDetailRow {
|
interface ChecklistDetailRow {
|
||||||
checklist_id: string;
|
checklist_id: string;
|
||||||
@@ -85,23 +86,6 @@ interface ChecklistData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskQueryResult {
|
|
||||||
id: number;
|
|
||||||
phase_id: string;
|
|
||||||
phase_activity_id: string;
|
|
||||||
time_type: string;
|
|
||||||
notes: string | null;
|
|
||||||
phases: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
} | null;
|
|
||||||
phase_activities: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssignmentQueryResult {
|
interface AssignmentQueryResult {
|
||||||
task_id: number;
|
task_id: number;
|
||||||
employee_id: string;
|
employee_id: string;
|
||||||
@@ -120,13 +104,13 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TIME_TYPE_ORDER = ['umum', 'pagi', 'siang', 'sore', 'malam'];
|
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
||||||
const TIME_TYPE_LABELS: { [key: string]: string } = {
|
const TIME_TYPE_LABELS: { [key: string]: string } = {
|
||||||
umum: 'Umum',
|
Umum: 'Umum',
|
||||||
pagi: 'Pagi',
|
Pagi: 'Pagi',
|
||||||
siang: 'Siang',
|
Siang: 'Siang',
|
||||||
sore: 'Sore',
|
Sore: 'Sore',
|
||||||
malam: 'Malam',
|
Malam: 'Malam',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DetailDailyChecklistContent() {
|
export function DetailDailyChecklistContent() {
|
||||||
@@ -155,8 +139,8 @@ export function DetailDailyChecklistContent() {
|
|||||||
}, [checklistId]);
|
}, [checklistId]);
|
||||||
|
|
||||||
const fetchChecklistDetail = async () => {
|
const fetchChecklistDetail = async () => {
|
||||||
if (!isSupabaseConfigured() || !checklistId) {
|
if (!checklistId) {
|
||||||
console.warn('Supabase not configured or checklistId missing');
|
console.warn('checklistId missing');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -164,69 +148,34 @@ export function DetailDailyChecklistContent() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// ✅ Fetch checklist header with kandang name
|
const checklistDataRes =
|
||||||
const { data: checklistData, error: checklistError } = await supabase
|
await DailyChecklistApi.getOneDailyChecklist(checklistId);
|
||||||
.from('daily_checklists')
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
id,
|
|
||||||
date,
|
|
||||||
kandang_id,
|
|
||||||
category,
|
|
||||||
status,
|
|
||||||
reject_reason,
|
|
||||||
kandang:kandang_id (
|
|
||||||
id,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.eq('id', checklistId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (checklistError || !checklistData) {
|
if (isResponseError(checklistDataRes)) {
|
||||||
console.error('Error fetching checklist:', checklistError);
|
console.error('Error fetching checklist:', checklistDataRes.message);
|
||||||
toast.error('Data checklist tidak ditemukan');
|
toast.error('Data checklist tidak ditemukan');
|
||||||
router.push('/daily-checklist/list-daily-checklist');
|
router.push('/daily-checklist/list-daily-checklist');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Fetch all tasks with their phase_activity details
|
const rawDetailChecklist = checklistDataRes?.data;
|
||||||
const { data: tasks, error: tasksError } = await supabase
|
|
||||||
.from('daily_checklist_activity_tasks')
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
id,
|
|
||||||
phase_id,
|
|
||||||
phase_activity_id,
|
|
||||||
time_type,
|
|
||||||
notes,
|
|
||||||
phases:phase_id (
|
|
||||||
id,
|
|
||||||
name
|
|
||||||
),
|
|
||||||
phase_activities:phase_activity_id (
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.eq('checklist_id', checklistId);
|
|
||||||
|
|
||||||
if (tasksError) {
|
const checklistData = {
|
||||||
console.error('Error fetching tasks:', tasksError);
|
id: rawDetailChecklist?.id,
|
||||||
toast.error('Gagal memuat detail checklist');
|
date: rawDetailChecklist?.date,
|
||||||
router.push('/daily-checklist/list-daily-checklist');
|
kandang_id: rawDetailChecklist?.kandang.id,
|
||||||
return;
|
category: rawDetailChecklist?.category,
|
||||||
}
|
status: rawDetailChecklist?.status,
|
||||||
|
reject_reason: rawDetailChecklist?.reject_reason,
|
||||||
|
kandang: rawDetailChecklist?.kandang,
|
||||||
|
};
|
||||||
|
|
||||||
const castedTasks = (tasks as unknown as TaskQueryResult[]) || [];
|
const tasks = rawDetailChecklist?.tasks;
|
||||||
|
|
||||||
const castedChecklistData =
|
const castedChecklistData =
|
||||||
checklistData as unknown as ChecklistData | null;
|
checklistData as unknown as ChecklistData | null;
|
||||||
|
|
||||||
if (!castedTasks || castedTasks.length === 0) {
|
if (!tasks || tasks.length === 0) {
|
||||||
toast.info('Checklist belum memiliki aktivitas');
|
toast.info('Checklist belum memiliki aktivitas');
|
||||||
setHeader({
|
setHeader({
|
||||||
date: castedChecklistData?.date || '-',
|
date: castedChecklistData?.date || '-',
|
||||||
@@ -242,36 +191,32 @@ export function DetailDailyChecklistContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Fetch all assignments for these tasks
|
const assignments: {
|
||||||
const taskIds = castedTasks.map((t) => t.id);
|
task_id: number;
|
||||||
const { data: assignments, error: assignmentsError } = await supabase
|
checked: boolean;
|
||||||
.from('daily_checklist_activity_task_assignments')
|
note: string | null;
|
||||||
.select(
|
employee: {
|
||||||
`
|
id: number;
|
||||||
task_id,
|
name: string;
|
||||||
employee_id,
|
};
|
||||||
checked,
|
}[] = [];
|
||||||
note,
|
|
||||||
employees:employee_id (
|
|
||||||
id,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.in('task_id', taskIds);
|
|
||||||
|
|
||||||
if (assignmentsError) {
|
tasks.forEach((task) => {
|
||||||
console.error('Error fetching assignments:', assignmentsError);
|
task.assignments.forEach((assignment) => {
|
||||||
}
|
assignments.push({
|
||||||
|
task_id: task.id,
|
||||||
const castedAssignments =
|
checked: assignment.checked,
|
||||||
(assignments as unknown as AssignmentQueryResult[]) || [];
|
note: assignment.note,
|
||||||
|
employee: assignment.employee,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ✅ Build detail rows from tasks and assignments
|
// ✅ Build detail rows from tasks and assignments
|
||||||
const detailRows: ChecklistDetailRow[] = [];
|
const detailRows: ChecklistDetailRow[] = [];
|
||||||
|
|
||||||
castedTasks.forEach((task) => {
|
tasks.forEach((task) => {
|
||||||
const taskAssignments = (castedAssignments || []).filter(
|
const taskAssignments = assignments.filter(
|
||||||
(a) => a.task_id === task.id
|
(a) => a.task_id === task.id
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -283,14 +228,14 @@ export function DetailDailyChecklistContent() {
|
|||||||
category: castedChecklistData?.category || '-',
|
category: castedChecklistData?.category || '-',
|
||||||
status: castedChecklistData?.status || '-',
|
status: castedChecklistData?.status || '-',
|
||||||
reject_reason: castedChecklistData?.reject_reason || '-',
|
reject_reason: castedChecklistData?.reject_reason || '-',
|
||||||
phase_id: task.phase_id,
|
phase_id: String(task.phase_id),
|
||||||
phase_name: task.phases?.name || '-',
|
phase_name: task.phase?.name || '-',
|
||||||
activity_id: task.phase_activity_id,
|
activity_id: String(task.phase_activity_id),
|
||||||
activity_name: task.phase_activities?.name || '-',
|
activity_name: task.phase_activity?.name || '-',
|
||||||
activity_description: task.phase_activities?.description || null,
|
activity_description: task.phase_activity?.description || null,
|
||||||
time_type: task.time_type,
|
time_type: task.time_type,
|
||||||
employee_id: assignment.employee_id,
|
employee_id: String(assignment.employee.id),
|
||||||
employee_name: assignment.employees?.name || '-',
|
employee_name: assignment.employee?.name || '-',
|
||||||
checked: assignment.checked,
|
checked: assignment.checked,
|
||||||
note: assignment.note,
|
note: assignment.note,
|
||||||
});
|
});
|
||||||
@@ -306,8 +251,8 @@ export function DetailDailyChecklistContent() {
|
|||||||
status: castedChecklistData?.status || '-',
|
status: castedChecklistData?.status || '-',
|
||||||
reject_reason: castedChecklistData?.reject_reason || '-',
|
reject_reason: castedChecklistData?.reject_reason || '-',
|
||||||
progress_percent: 0,
|
progress_percent: 0,
|
||||||
total_phases: new Set(castedTasks.map((t) => t.phase_id)).size,
|
total_phases: new Set(tasks.map((t) => t.phase_id)).size,
|
||||||
total_activities: castedTasks.length,
|
total_activities: tasks.length,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -471,22 +416,15 @@ export function DetailDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmApprove = async () => {
|
const confirmApprove = async () => {
|
||||||
if (!checklistId || !isSupabaseConfigured()) return;
|
if (!checklistId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
|
|
||||||
const { error } = await supabase
|
const approveRes = await DailyChecklistApi.approve(String(checklistId));
|
||||||
.from('daily_checklists')
|
|
||||||
.update({
|
|
||||||
status: 'APPROVED',
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', checklistId);
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(approveRes)) {
|
||||||
console.error('Error approving checklist:', error);
|
toast.error('Gagal approve checklist: ' + approveRes.message);
|
||||||
toast.error('Gagal approve checklist');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +440,7 @@ export function DetailDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmReject = async () => {
|
const confirmReject = async () => {
|
||||||
if (!checklistId || !isSupabaseConfigured()) return;
|
if (!checklistId) return;
|
||||||
|
|
||||||
if (!rejectReason.trim()) {
|
if (!rejectReason.trim()) {
|
||||||
toast.error('Alasan reject harus diisi');
|
toast.error('Alasan reject harus diisi');
|
||||||
@@ -512,18 +450,13 @@ export function DetailDailyChecklistContent() {
|
|||||||
try {
|
try {
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
|
|
||||||
const { error } = await supabase
|
const rejectRes = await DailyChecklistApi.reject(
|
||||||
.from('daily_checklists')
|
String(checklistId),
|
||||||
.update({
|
rejectReason
|
||||||
status: 'REJECTED',
|
);
|
||||||
reject_reason: rejectReason,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq('id', checklistId);
|
|
||||||
|
|
||||||
if (error) {
|
if (isResponseError(rejectRes)) {
|
||||||
console.error('Error rejecting checklist:', error);
|
toast.error('Gagal reject checklist: ' + rejectRes.message);
|
||||||
toast.error('Gagal reject checklist');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
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 { Phase } from '@/types/api/daily-checklist/phase';
|
||||||
|
|
||||||
// Static categories - tidak bisa CRUD
|
// Static categories - tidak bisa CRUD
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
@@ -64,13 +65,6 @@ const TIME_TYPES = [
|
|||||||
{ value: 'Malam', label: 'Malam' },
|
{ value: 'Malam', label: 'Malam' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Phase {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
category: string;
|
|
||||||
activityCount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MasterAktivitasContent() {
|
export function MasterAktivitasContent() {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
||||||
const [selectedPhase, setSelectedPhase] = useState<Phase | null>(null);
|
const [selectedPhase, setSelectedPhase] = useState<Phase | null>(null);
|
||||||
@@ -103,7 +97,7 @@ export function MasterAktivitasContent() {
|
|||||||
SWRHttpKey
|
SWRHttpKey
|
||||||
>(
|
>(
|
||||||
selectedPhase?.id
|
selectedPhase?.id
|
||||||
? `${PhaseActivityApi.basePath}?page=1&limit=100&phase_id=${selectedPhase.id}`
|
? `${PhaseActivityApi.basePath}?page=1&limit=100&phase_ids=${selectedPhase.id}`
|
||||||
: '',
|
: '',
|
||||||
httpClientFetcher,
|
httpClientFetcher,
|
||||||
{
|
{
|
||||||
@@ -171,7 +165,7 @@ export function MasterAktivitasContent() {
|
|||||||
const handleEditPhase = (phase: Phase) => {
|
const handleEditPhase = (phase: Phase) => {
|
||||||
setPhaseModalMode('edit');
|
setPhaseModalMode('edit');
|
||||||
setPhaseForm({
|
setPhaseForm({
|
||||||
id: phase.id,
|
id: String(phase.id),
|
||||||
name: phase.name,
|
name: phase.name,
|
||||||
});
|
});
|
||||||
setShowPhaseModal(true);
|
setShowPhaseModal(true);
|
||||||
@@ -265,7 +259,7 @@ export function MasterAktivitasContent() {
|
|||||||
setPhaseToDelete(null);
|
setPhaseToDelete(null);
|
||||||
|
|
||||||
// Clear selection if deleted phase was selected
|
// Clear selection if deleted phase was selected
|
||||||
if (selectedPhase?.id === phaseToDelete) {
|
if (selectedPhase?.id === Number(phaseToDelete)) {
|
||||||
setSelectedPhase(null);
|
setSelectedPhase(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -543,7 +537,7 @@ export function MasterAktivitasContent() {
|
|||||||
{phase.name}
|
{phase.name}
|
||||||
</p>
|
</p>
|
||||||
<p className='text-xs text-gray-500 mt-1'>
|
<p className='text-xs text-gray-500 mt-1'>
|
||||||
{phase.activityCount || 0} aktivitas
|
{phase.activity_count || 0} aktivitas
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -567,7 +561,9 @@ export function MasterAktivitasContent() {
|
|||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleDeletePhaseClick(phase.id)}
|
onClick={() =>
|
||||||
|
handleDeletePhaseClick(String(phase.id))
|
||||||
|
}
|
||||||
className='text-red-600'
|
className='text-red-600'
|
||||||
>
|
>
|
||||||
<Trash2 className='mr-2 h-4 w-4' />
|
<Trash2 className='mr-2 h-4 w-4' />
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { BaseApiService } from '@/services/api/base';
|
||||||
|
import { httpClient } from '@/services/http/client';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import {
|
||||||
|
CreateDailyChecklistPayload,
|
||||||
|
DailyChecklist,
|
||||||
|
DetailDailyChecklist,
|
||||||
|
} from '@/types/api/daily-checklist/daily-checklist';
|
||||||
|
|
||||||
|
export class DailyChecklistApiService extends BaseApiService<
|
||||||
|
DailyChecklist,
|
||||||
|
CreateDailyChecklistPayload,
|
||||||
|
unknown
|
||||||
|
> {
|
||||||
|
constructor(basePath: string = '/daily-checklists') {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOneDailyChecklist(id: string) {
|
||||||
|
try {
|
||||||
|
const getOneDailyChecklistPath = `${this.basePath}/relation/${id}`;
|
||||||
|
const getOneDailyChecklistRes = await httpClient<
|
||||||
|
BaseApiResponse<DetailDailyChecklist>
|
||||||
|
>(getOneDailyChecklistPath);
|
||||||
|
|
||||||
|
return getOneDailyChecklistRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<DetailDailyChecklist>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDailyChecklistPhase(id: string, phaseIds: string[]) {
|
||||||
|
try {
|
||||||
|
const setDailyChecklistPhasePath = `${this.basePath}/phase/${id}`;
|
||||||
|
const setDailyChecklistPhaseRes = await httpClient<BaseApiResponse>(
|
||||||
|
setDailyChecklistPhasePath,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: { phase_ids: phaseIds.join(',') },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return setDailyChecklistPhaseRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeEmployeeAssignment(id: string, employeeId: string) {
|
||||||
|
try {
|
||||||
|
const removeEmployeeAssignmentPath = `${this.basePath}/${id}/assignments/${employeeId}`;
|
||||||
|
const removeEmployeeAssignmentRes = await httpClient<BaseApiResponse>(
|
||||||
|
removeEmployeeAssignmentPath,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return removeEmployeeAssignmentRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDailyChecklistEmployees(checklistId: string, employeeIds: string[]) {
|
||||||
|
try {
|
||||||
|
const setDailyChecklistPhasePath = `${this.basePath}/assignment/${checklistId}`;
|
||||||
|
const setDailyChecklistPhaseRes = await httpClient<BaseApiResponse>(
|
||||||
|
setDailyChecklistPhasePath,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: { employee_ids: employeeIds.join(',') },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return setDailyChecklistPhaseRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTasksByChecklistId(id: string) {
|
||||||
|
try {
|
||||||
|
const getTasksByChecklistIdPath = `${this.basePath}/tasks?checklist_id=${id}&page=1&limit=100`;
|
||||||
|
const getTasksByChecklistIdRes = await httpClient<BaseApiResponse>(
|
||||||
|
getTasksByChecklistIdPath
|
||||||
|
);
|
||||||
|
|
||||||
|
return getTasksByChecklistIdRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkOrUncheckAssignment(payload: {
|
||||||
|
task_id: number;
|
||||||
|
employee_id: number;
|
||||||
|
checked: boolean;
|
||||||
|
note: string | null;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const checkOrUncheckAssignmentPath = `${this.basePath}/assignment`;
|
||||||
|
const checkOrUncheckAssignmentRes = await httpClient<BaseApiResponse>(
|
||||||
|
checkOrUncheckAssignmentPath,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return checkOrUncheckAssignmentRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(id: string) {
|
||||||
|
try {
|
||||||
|
const submitPath = `${this.basePath}/${id}`;
|
||||||
|
const submitRes = await httpClient<BaseApiResponse>(submitPath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
status: 'SUBMITTED',
|
||||||
|
reject_reason: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return submitRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async approve(id: string) {
|
||||||
|
try {
|
||||||
|
const approvePath = `${this.basePath}/${id}`;
|
||||||
|
const approveRes = await httpClient<BaseApiResponse>(approvePath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
status: 'APPROVED',
|
||||||
|
reject_reason: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return approveRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reject(id: string, rejectReason: string) {
|
||||||
|
try {
|
||||||
|
const rejectPath = `${this.basePath}/${id}`;
|
||||||
|
const rejectRes = await httpClient<BaseApiResponse>(rejectPath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
status: 'REJECTED',
|
||||||
|
reject_reason: rejectReason,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return rejectRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DailyChecklistApi = new DailyChecklistApiService(
|
||||||
|
'/daily-checklists'
|
||||||
|
);
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
import { BaseKandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { Phase } from '@/types/api/daily-checklist/phase';
|
||||||
|
import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity';
|
||||||
|
|
||||||
|
export type BaseDailyChecklist = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
category: string;
|
||||||
|
date: string;
|
||||||
|
kandang: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
|
||||||
|
total_phase: number;
|
||||||
|
total_activity: number;
|
||||||
|
progress: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DailyChecklist = BaseMetadata & BaseDailyChecklist;
|
||||||
|
|
||||||
|
export type DetailDailyChecklist = BaseDailyChecklist & {
|
||||||
|
reject_reason: string | null;
|
||||||
|
phases: {
|
||||||
|
id: number;
|
||||||
|
phase_id: number;
|
||||||
|
phase: Phase;
|
||||||
|
}[];
|
||||||
|
tasks: {
|
||||||
|
id: number;
|
||||||
|
checklist_id: number;
|
||||||
|
phase_id: number;
|
||||||
|
phase_activity_id: number;
|
||||||
|
time_type: string;
|
||||||
|
notes: string | null;
|
||||||
|
phase: Phase;
|
||||||
|
phase_activity: PhaseActivity;
|
||||||
|
assignments: {
|
||||||
|
employee: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
checked: boolean;
|
||||||
|
note: string | null;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
assigned_employees: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateDailyChecklistPayload = {
|
||||||
|
date: string;
|
||||||
|
kandang_id: number;
|
||||||
|
category: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
+1
@@ -5,6 +5,7 @@ export type BasePhase = {
|
|||||||
name: string;
|
name: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
category: string;
|
category: string;
|
||||||
|
activity_count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Phase = BaseMetadata & BasePhase;
|
export type Phase = BaseMetadata & BasePhase;
|
||||||
|
|||||||
Reference in New Issue
Block a user