mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-22 22:35:45 +00:00
917 lines
32 KiB
TypeScript
917 lines
32 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
|
|
import { Card, CardContent } from '@/figma-make/components/base/card';
|
|
import { Button } from '@/figma-make/components/base/button';
|
|
import { Label } from '@/figma-make/components/base/label';
|
|
import { Input } from '@/figma-make/components/base/input';
|
|
import { Textarea } from '@/figma-make/components/base/textarea';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/figma-make/components/base/select';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from '@/figma-make/components/base/dialog';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/figma-make/components/base/alert-dialog';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/figma-make/components/base/dropdown-menu';
|
|
import { toast } from 'sonner';
|
|
import useSWR from 'swr';
|
|
import { PhaseApi } from '@/services/api/daily-checklist/phase';
|
|
import { BaseApiResponse } from '@/types/api/api-general';
|
|
import { AxiosError } from 'axios';
|
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
import { PhaseActivityApi } from '@/services/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
|
|
const CATEGORIES = [
|
|
{ value: 'pullet_open', label: 'Pullet Open' },
|
|
{ value: 'pullet_close', label: 'Pullet Close' },
|
|
{ value: 'produksi_open', label: 'Produksi Open' },
|
|
{ value: 'produksi_close', label: 'Produksi Close' },
|
|
];
|
|
|
|
const TIME_TYPES = [
|
|
{ value: 'Umum', label: 'Umum' },
|
|
{ value: 'Pagi', label: 'Pagi' },
|
|
{ value: 'Siang', label: 'Siang' },
|
|
{ value: 'Sore', label: 'Sore' },
|
|
{ value: 'Malam', label: 'Malam' },
|
|
];
|
|
|
|
export function MasterAktivitasContent() {
|
|
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
|
const [selectedPhase, setSelectedPhase] = useState<Phase | null>(null);
|
|
|
|
const {
|
|
data: phases,
|
|
isLoading: isLoadingPhases,
|
|
mutate: refreshPhases,
|
|
} = useSWR<
|
|
BaseApiResponse<Phase[] | undefined>,
|
|
AxiosError<BaseApiResponse>,
|
|
SWRHttpKey
|
|
>(
|
|
selectedCategory
|
|
? `${PhaseApi.basePath}?page=1&limit=100&category=${selectedCategory}`
|
|
: '',
|
|
httpClientFetcher,
|
|
{
|
|
keepPreviousData: true,
|
|
}
|
|
);
|
|
|
|
const {
|
|
data: phaseActivities,
|
|
isLoading: isLoadingPhaseActivities,
|
|
mutate: refreshPhaseActivities,
|
|
} = useSWR<
|
|
BaseApiResponse<PhaseActivity[] | undefined>,
|
|
AxiosError<BaseApiResponse>,
|
|
SWRHttpKey
|
|
>(
|
|
selectedPhase?.id
|
|
? `${PhaseActivityApi.basePath}?page=1&limit=100&phase_ids=${selectedPhase.id}`
|
|
: '',
|
|
httpClientFetcher,
|
|
{
|
|
keepPreviousData: true,
|
|
}
|
|
);
|
|
|
|
const [showPhaseModal, setShowPhaseModal] = useState(false);
|
|
const [showActivityModal, setShowActivityModal] = useState(false);
|
|
const [showPhaseDeleteConfirm, setShowPhaseDeleteConfirm] = useState(false);
|
|
const [showActivityDeleteConfirm, setShowActivityDeleteConfirm] =
|
|
useState(false);
|
|
|
|
const [phaseToDelete, setPhaseToDelete] = useState<string | null>(null);
|
|
const [activityToDelete, setActivityToDelete] = useState<string | null>(null);
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
|
|
const [phaseSearchQuery, setPhaseSearchQuery] = useState('');
|
|
const [phaseModalMode, setPhaseModalMode] = useState<'create' | 'edit'>(
|
|
'create'
|
|
);
|
|
|
|
const filteredPhases =
|
|
(isResponseSuccess(phases) &&
|
|
phases?.data?.filter((phase) => {
|
|
return phase.name
|
|
.toLowerCase()
|
|
.includes(phaseSearchQuery.toLowerCase());
|
|
})) ||
|
|
[];
|
|
|
|
const [activityModalMode, setActivityModalMode] = useState<'create' | 'edit'>(
|
|
'create'
|
|
);
|
|
|
|
const [phaseForm, setPhaseForm] = useState({
|
|
id: '',
|
|
name: '',
|
|
});
|
|
|
|
const [activityForm, setActivityForm] = useState({
|
|
id: '',
|
|
name: '',
|
|
description: '',
|
|
time_type: 'umum',
|
|
});
|
|
|
|
useEffect(() => {
|
|
setInitialLoading(false);
|
|
}, []);
|
|
|
|
// Phase handlers
|
|
const handleAddPhase = () => {
|
|
if (!selectedCategory) {
|
|
toast.error('Pilih kategori terlebih dahulu');
|
|
return;
|
|
}
|
|
setPhaseModalMode('create');
|
|
setPhaseForm({ id: '', name: '' });
|
|
setShowPhaseModal(true);
|
|
};
|
|
|
|
const handleEditPhase = (phase: Phase) => {
|
|
setPhaseModalMode('edit');
|
|
setPhaseForm({
|
|
id: String(phase.id),
|
|
name: phase.name,
|
|
});
|
|
setShowPhaseModal(true);
|
|
};
|
|
|
|
const handleSavePhase = async () => {
|
|
if (!phaseForm.name.trim()) {
|
|
toast.error('Nama phase harus diisi');
|
|
return;
|
|
}
|
|
|
|
if (phaseForm.name.trim().length < 3) {
|
|
toast.error('Nama phase minimal 3 karakter!');
|
|
return;
|
|
}
|
|
|
|
if (!selectedCategory) {
|
|
toast.error('Pilih kategori terlebih dahulu');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
if (phaseModalMode === 'create') {
|
|
const createPhaseResponse = await PhaseApi.create({
|
|
category: selectedCategory,
|
|
name: phaseForm.name.trim(),
|
|
});
|
|
|
|
if (isResponseError(createPhaseResponse)) {
|
|
console.error('Error creating phase:', createPhaseResponse.message);
|
|
toast.error('Gagal menambahkan phase');
|
|
return;
|
|
}
|
|
|
|
refreshPhases();
|
|
toast.success('Phase berhasil ditambahkan');
|
|
} else {
|
|
const updatePhaseResponse = await PhaseApi.update(
|
|
Number(phaseForm.id),
|
|
{
|
|
name: phaseForm.name.trim(),
|
|
}
|
|
);
|
|
|
|
if (isResponseError(updatePhaseResponse)) {
|
|
console.error('Error creating phase:', updatePhaseResponse.message);
|
|
toast.error('Gagal menambahkan phase');
|
|
return;
|
|
}
|
|
|
|
refreshPhases();
|
|
|
|
toast.success('Phase berhasil diubah');
|
|
}
|
|
|
|
setShowPhaseModal(false);
|
|
setPhaseForm({ id: '', name: '' });
|
|
} catch (error) {
|
|
console.error('Error saving phase:', error);
|
|
toast.error('Terjadi kesalahan saat menyimpan phase');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDeletePhaseClick = (phaseId: string) => {
|
|
setPhaseToDelete(phaseId);
|
|
setShowPhaseDeleteConfirm(true);
|
|
};
|
|
|
|
const handleConfirmDeletePhase = async () => {
|
|
if (!phaseToDelete || !selectedCategory) return;
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const deletePhaseResponse = await PhaseApi.delete(Number(phaseToDelete));
|
|
|
|
if (isResponseError(deletePhaseResponse)) {
|
|
console.error('Error deleting phase:', deletePhaseResponse.message);
|
|
toast.error('Gagal menghapus phase');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
refreshPhases();
|
|
toast.success('Phase dan semua aktivitasnya berhasil dihapus');
|
|
setShowPhaseDeleteConfirm(false);
|
|
setPhaseToDelete(null);
|
|
|
|
// Clear selection if deleted phase was selected
|
|
if (selectedPhase?.id === Number(phaseToDelete)) {
|
|
setSelectedPhase(null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting phase:', error);
|
|
toast.error('Terjadi kesalahan saat menghapus phase');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Activity handlers
|
|
const handleAddActivity = () => {
|
|
if (!selectedPhase) {
|
|
toast.error('Pilih phase terlebih dahulu');
|
|
return;
|
|
}
|
|
setActivityModalMode('create');
|
|
setActivityForm({ id: '', name: '', description: '', time_type: 'umum' });
|
|
setShowActivityModal(true);
|
|
};
|
|
|
|
const handleEditActivity = (activity: PhaseActivity) => {
|
|
setActivityModalMode('edit');
|
|
setActivityForm({
|
|
id: String(activity.id),
|
|
name: activity.name,
|
|
description: activity.description || '',
|
|
time_type: activity.time_type,
|
|
});
|
|
setShowActivityModal(true);
|
|
};
|
|
|
|
const handleSaveActivity = async () => {
|
|
if (!activityForm.name.trim()) {
|
|
toast.error('Nama aktivitas harus diisi');
|
|
return;
|
|
}
|
|
|
|
if (activityForm.name.trim().length < 3) {
|
|
toast.error('Nama aktivitas minimal 3 karakter!');
|
|
return;
|
|
}
|
|
|
|
if (!selectedPhase) {
|
|
toast.error('Pilih phase terlebih dahulu');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
if (activityModalMode === 'create') {
|
|
const createActivityResponse = await PhaseActivityApi.create({
|
|
phase_id: Number(selectedPhase.id),
|
|
name: activityForm.name.trim(),
|
|
description: activityForm.description.trim() || '',
|
|
time_type: activityForm.time_type,
|
|
});
|
|
|
|
if (isResponseError(createActivityResponse)) {
|
|
console.error(
|
|
'Error creating activity:',
|
|
createActivityResponse.message
|
|
);
|
|
toast.error('Gagal menambahkan aktivitas');
|
|
return;
|
|
}
|
|
|
|
refreshPhases();
|
|
refreshPhaseActivities();
|
|
toast.success('Aktivitas berhasil ditambahkan');
|
|
} else {
|
|
const updateActivityResponse = await PhaseActivityApi.update(
|
|
Number(activityForm.id),
|
|
{
|
|
name: activityForm.name.trim(),
|
|
description: activityForm.description.trim() || '',
|
|
time_type: activityForm.time_type,
|
|
}
|
|
);
|
|
|
|
if (isResponseError(updateActivityResponse)) {
|
|
console.error(
|
|
'Error updating activity:',
|
|
updateActivityResponse.message
|
|
);
|
|
toast.error('Gagal mengubah aktivitas');
|
|
return;
|
|
}
|
|
|
|
refreshPhases();
|
|
refreshPhaseActivities();
|
|
toast.success('Aktivitas berhasil diubah');
|
|
}
|
|
|
|
setShowActivityModal(false);
|
|
setActivityForm({ id: '', name: '', description: '', time_type: 'umum' });
|
|
} catch (error) {
|
|
console.error('Error saving activity:', error);
|
|
toast.error('Terjadi kesalahan saat menyimpan aktivitas');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteActivityClick = (activityId: string) => {
|
|
setActivityToDelete(activityId);
|
|
setShowActivityDeleteConfirm(true);
|
|
};
|
|
|
|
const handleConfirmDeleteActivity = async () => {
|
|
if (!activityToDelete || !selectedPhase || !selectedCategory) return;
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const deleteActivityResponse = await PhaseActivityApi.delete(
|
|
Number(activityToDelete)
|
|
);
|
|
|
|
if (isResponseError(deleteActivityResponse)) {
|
|
console.error(
|
|
'Error deleting activity:',
|
|
deleteActivityResponse.message
|
|
);
|
|
toast.error('Gagal menghapus aktivitas');
|
|
return;
|
|
}
|
|
|
|
refreshPhases();
|
|
refreshPhaseActivities();
|
|
toast.success('Aktivitas berhasil dihapus');
|
|
setShowActivityDeleteConfirm(false);
|
|
setActivityToDelete(null);
|
|
} catch (error) {
|
|
console.error('Error deleting activity:', error);
|
|
toast.error('Terjadi kesalahan saat menghapus aktivitas');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getTimeTypeLabel = (timeType: string) => {
|
|
return TIME_TYPES.find((t) => t.value === timeType)?.label || timeType;
|
|
};
|
|
|
|
const getTimeTypeBadgeClass = (timeType: string) => {
|
|
switch (timeType.toLowerCase()) {
|
|
case 'umum':
|
|
return 'bg-gray-50 text-gray-700 border-gray-300';
|
|
case 'pagi':
|
|
return 'bg-orange-50 text-orange-700 border-orange-300';
|
|
case 'siang':
|
|
return 'bg-amber-50 text-amber-700 border-amber-300';
|
|
case 'sore':
|
|
return 'bg-purple-50 text-purple-700 border-purple-300';
|
|
case 'malam':
|
|
return 'bg-indigo-50 text-indigo-700 border-indigo-300';
|
|
default:
|
|
return 'bg-gray-50 text-gray-700 border-gray-300';
|
|
}
|
|
};
|
|
|
|
if (initialLoading) {
|
|
return (
|
|
<div className='min-h-screen'>
|
|
<div className='p-6'>
|
|
<div className='mb-6'>
|
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
|
Master Aktivitas
|
|
</h1>
|
|
<p className='text-sm text-gray-600 mt-1'>
|
|
Master Data • <span className='text-[#0069e0]'>Aktivitas</span>
|
|
</p>
|
|
</div>
|
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
|
<CardContent className='p-12 text-center text-gray-500'>
|
|
Memuat data...
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className='min-h-screen'>
|
|
<div className='p-6'>
|
|
{/* Page Title */}
|
|
<div className='mb-6'>
|
|
<h1 className='text-2xl font-semibold text-gray-900'>
|
|
Master Aktivitas
|
|
</h1>
|
|
<p className='text-sm text-gray-600 mt-1'>
|
|
Master Data • <span className='text-[#0069e0]'>Aktivitas</span>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Category Selector Card */}
|
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white mb-6'>
|
|
<CardContent className='p-6'>
|
|
<div className='max-w-md'>
|
|
<Label htmlFor='category'>
|
|
Kategori <span className='text-red-500'>*</span>
|
|
</Label>
|
|
<Select
|
|
value={selectedCategory}
|
|
onValueChange={setSelectedCategory}
|
|
>
|
|
<SelectTrigger id='category' className='mt-1.5 border-gray-200'>
|
|
<SelectValue placeholder='Pilih kategori aktivitas' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{CATEGORIES.map((cat) => (
|
|
<SelectItem key={cat.value} value={cat.value}>
|
|
{cat.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className='text-xs text-gray-500 mt-2'>
|
|
Pilih kategori untuk melihat dan mengelola phase dan aktivitas
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{selectedCategory ? (
|
|
<div className='grid grid-cols-1 lg:grid-cols-5 gap-6'>
|
|
{/* LEFT PANEL: Phase List */}
|
|
<div className='lg:col-span-2'>
|
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
|
<CardContent className='p-0'>
|
|
{/* Phase Toolbar */}
|
|
<div className='flex items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
|
|
<div className='relative flex-1'>
|
|
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
|
|
<Input
|
|
type='text'
|
|
placeholder='Cari phase...'
|
|
value={phaseSearchQuery}
|
|
onChange={(e) => setPhaseSearchQuery(e.target.value)}
|
|
className='pl-10 border-gray-200'
|
|
/>
|
|
</div>
|
|
<Button
|
|
onClick={handleAddPhase}
|
|
size='sm'
|
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white whitespace-nowrap'
|
|
>
|
|
<Plus className='w-4 h-4 mr-1' />
|
|
Phase
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Phase List */}
|
|
<div className='divide-y divide-gray-200/60'>
|
|
{!isResponseSuccess(phases) ||
|
|
(isResponseSuccess(phases) && phases.data?.length === 0) ? (
|
|
<div className='p-8 text-center text-gray-500'>
|
|
{phaseSearchQuery
|
|
? 'Tidak ada phase yang ditemukan'
|
|
: 'Belum ada data phase'}
|
|
</div>
|
|
) : (
|
|
filteredPhases.map((phase) => (
|
|
<div
|
|
key={phase.id}
|
|
className={`flex items-center justify-between p-4 cursor-pointer transition-colors ${
|
|
selectedPhase?.id === phase.id
|
|
? 'bg-blue-50 border-l-4 border-[#0069e0]'
|
|
: 'hover:bg-gray-50'
|
|
}`}
|
|
onClick={() => setSelectedPhase(phase)}
|
|
>
|
|
<div className='flex-1 min-w-0'>
|
|
<p className='text-sm font-medium text-gray-900 truncate'>
|
|
{phase.name}
|
|
</p>
|
|
<p className='text-xs text-gray-500 mt-1'>
|
|
{phase.activity_count || 0} aktivitas
|
|
</p>
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
asChild
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Button
|
|
variant='ghost'
|
|
size='sm'
|
|
className='h-8 w-8 p-0 hover:bg-gray-100'
|
|
>
|
|
<MoreVertical className='h-4 w-4 text-gray-600' />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align='end'>
|
|
<DropdownMenuItem
|
|
onClick={() => handleEditPhase(phase)}
|
|
>
|
|
<Pencil className='mr-2 h-4 w-4' />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() =>
|
|
handleDeletePhaseClick(String(phase.id))
|
|
}
|
|
className='text-red-600'
|
|
>
|
|
<Trash2 className='mr-2 h-4 w-4' />
|
|
Hapus
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* RIGHT PANEL: Activity Detail */}
|
|
<div className='lg:col-span-3'>
|
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
|
<CardContent className='p-0'>
|
|
{selectedPhase ? (
|
|
<>
|
|
{/* Activity Header */}
|
|
<div className='flex items-center justify-between p-6 border-b border-gray-200/60'>
|
|
<div>
|
|
<h2 className='text-lg font-semibold text-gray-900'>
|
|
Aktivitas di Phase: {selectedPhase.name}
|
|
</h2>
|
|
{isResponseSuccess(phaseActivities) && (
|
|
<p className='text-sm text-gray-600 mt-0.5'>
|
|
{phaseActivities.data?.length} aktivitas
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Button
|
|
onClick={handleAddActivity}
|
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
|
>
|
|
<Plus className='w-4 h-4 mr-2' />
|
|
Tambah Aktivitas
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Activity Table */}
|
|
<div className='overflow-x-auto'>
|
|
<table className='w-full'>
|
|
<thead>
|
|
<tr className='border-b border-gray-200/60 bg-gray-50/50'>
|
|
<th className='text-left py-3.5 px-6 text-sm font-semibold text-gray-700'>
|
|
Nama Aktivitas
|
|
</th>
|
|
<th className='text-center py-3.5 px-6 text-sm font-semibold text-gray-700 w-[80px]'>
|
|
Aksi
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className='divide-y divide-gray-200/60'>
|
|
{!isResponseSuccess(phaseActivities) ||
|
|
(isResponseSuccess(phaseActivities) &&
|
|
phaseActivities.data?.length === 0) ? (
|
|
<tr>
|
|
<td
|
|
colSpan={2}
|
|
className='text-center py-12 text-gray-500'
|
|
>
|
|
Belum ada aktivitas di phase ini
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
phaseActivities.data?.map((activity) => (
|
|
<tr
|
|
key={activity.id}
|
|
className='hover:bg-blue-50/30 transition-colors'
|
|
>
|
|
<td className='py-3.5 px-6'>
|
|
<div className='flex items-center gap-2'>
|
|
<div className='flex-1'>
|
|
<p className='text-sm text-gray-900'>
|
|
{activity.name}
|
|
</p>
|
|
{activity.description && (
|
|
<p className='text-xs text-gray-500 mt-1'>
|
|
{activity.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<span
|
|
className={`text-xs px-2 py-0.5 rounded-full ${getTimeTypeBadgeClass(activity.time_type)}`}
|
|
>
|
|
{getTimeTypeLabel(activity.time_type)}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className='py-3.5 px-6 text-center'>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant='ghost'
|
|
size='sm'
|
|
className='h-8 w-8 p-0 hover:bg-gray-100'
|
|
>
|
|
<MoreVertical className='h-4 w-4 text-gray-600' />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align='end'>
|
|
<DropdownMenuItem
|
|
onClick={() =>
|
|
handleEditActivity(activity)
|
|
}
|
|
>
|
|
<Pencil className='mr-2 h-4 w-4' />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() =>
|
|
handleDeleteActivityClick(
|
|
String(activity.id)
|
|
)
|
|
}
|
|
className='text-red-600'
|
|
>
|
|
<Trash2 className='mr-2 h-4 w-4' />
|
|
Hapus
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className='p-12 text-center text-gray-500'>
|
|
Pilih phase untuk melihat aktivitas
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
|
|
<CardContent className='p-12 text-center text-gray-500'>
|
|
Pilih kategori terlebih dahulu untuk mengelola phase dan aktivitas
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
|
|
{/* Phase Add/Edit Modal */}
|
|
<Dialog open={showPhaseModal} onOpenChange={setShowPhaseModal}>
|
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{phaseModalMode === 'create' ? 'Tambah Phase' : 'Edit Phase'}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{phaseModalMode === 'create'
|
|
? 'Masukkan detail phase baru'
|
|
: 'Ubah detail phase'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className='space-y-4 py-4'>
|
|
<div>
|
|
<Label htmlFor='nama-phase'>
|
|
Nama Phase <span className='text-red-500'>*</span>
|
|
</Label>
|
|
<Input
|
|
id='nama-phase'
|
|
value={phaseForm.name}
|
|
onChange={(e) =>
|
|
setPhaseForm({ ...phaseForm, name: e.target.value })
|
|
}
|
|
placeholder='Masukkan nama phase'
|
|
className='mt-1.5'
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
variant='outline'
|
|
onClick={() => setShowPhaseModal(false)}
|
|
disabled={loading}
|
|
>
|
|
Batal
|
|
</Button>
|
|
<Button
|
|
onClick={handleSavePhase}
|
|
disabled={loading}
|
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
|
>
|
|
{loading ? 'Menyimpan...' : 'Simpan'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Activity Add/Edit Modal */}
|
|
<Dialog open={showActivityModal} onOpenChange={setShowActivityModal}>
|
|
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{activityModalMode === 'create'
|
|
? 'Tambah Aktivitas'
|
|
: 'Edit Aktivitas'}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{activityModalMode === 'create'
|
|
? 'Masukkan detail aktivitas baru'
|
|
: 'Ubah detail aktivitas'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className='space-y-4 py-4'>
|
|
<div>
|
|
<Label htmlFor='nama-aktivitas'>
|
|
Nama Aktivitas <span className='text-red-500'>*</span>
|
|
</Label>
|
|
<Input
|
|
id='nama-aktivitas'
|
|
value={activityForm.name}
|
|
onChange={(e) =>
|
|
setActivityForm({ ...activityForm, name: e.target.value })
|
|
}
|
|
placeholder='Masukkan nama aktivitas'
|
|
className='mt-1.5'
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='keterangan'>Keterangan (Opsional)</Label>
|
|
<Textarea
|
|
id='keterangan'
|
|
value={activityForm.description}
|
|
onChange={(e) =>
|
|
setActivityForm({
|
|
...activityForm,
|
|
description: e.target.value,
|
|
})
|
|
}
|
|
placeholder='Masukkan keterangan aktivitas'
|
|
className='mt-1.5'
|
|
rows={3}
|
|
disabled={loading}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor='tipe-aktivitas'>
|
|
Tipe Aktivitas <span className='text-red-500'>*</span>
|
|
</Label>
|
|
<Select
|
|
value={activityForm.time_type}
|
|
onValueChange={(value) =>
|
|
setActivityForm({ ...activityForm, time_type: value })
|
|
}
|
|
disabled={loading}
|
|
>
|
|
<SelectTrigger
|
|
id='tipe-aktivitas'
|
|
className='mt-1.5 border-gray-200'
|
|
>
|
|
<SelectValue placeholder='Pilih tipe aktivitas' />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TIME_TYPES.map((type) => (
|
|
<SelectItem key={type.value} value={type.value}>
|
|
{type.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
variant='outline'
|
|
onClick={() => setShowActivityModal(false)}
|
|
disabled={loading}
|
|
>
|
|
Batal
|
|
</Button>
|
|
<Button
|
|
onClick={handleSaveActivity}
|
|
disabled={loading}
|
|
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
|
|
>
|
|
{loading ? 'Menyimpan...' : 'Simpan'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Phase Delete Confirmation */}
|
|
<AlertDialog
|
|
open={showPhaseDeleteConfirm}
|
|
onOpenChange={setShowPhaseDeleteConfirm}
|
|
>
|
|
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Hapus Phase?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Menghapus phase akan menghapus semua aktivitas di dalamnya secara
|
|
permanen.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleConfirmDeletePhase}
|
|
disabled={loading}
|
|
className='bg-red-600 hover:bg-red-700 text-white'
|
|
>
|
|
{loading ? 'Menghapus...' : 'Hapus'}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* Activity Delete Confirmation */}
|
|
<AlertDialog
|
|
open={showActivityDeleteConfirm}
|
|
onOpenChange={setShowActivityDeleteConfirm}
|
|
>
|
|
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Hapus Aktivitas?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Hapus aktivitas ini secara permanen?
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleConfirmDeleteActivity}
|
|
disabled={loading}
|
|
className='bg-red-600 hover:bg-red-700 text-white'
|
|
>
|
|
{loading ? 'Menghapus...' : 'Hapus'}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|