feat: integrate MasterAktivitasContent component to API

This commit is contained in:
ValdiANS
2026-01-08 09:39:50 +07:00
parent 5ba58c92d4
commit 06dd9a3609
@@ -39,7 +39,14 @@ import {
DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner';
import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase';
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';
// Static categories - tidak bisa CRUD
const CATEGORIES = [
@@ -50,11 +57,11 @@ const CATEGORIES = [
];
const TIME_TYPES = [
{ value: 'umum', label: 'Umum' },
{ value: 'pagi', label: 'Pagi' },
{ value: 'siang', label: 'Siang' },
{ value: 'sore', label: 'Sore' },
{ value: 'malam', label: 'Malam' },
{ value: 'Umum', label: 'Umum' },
{ value: 'Pagi', label: 'Pagi' },
{ value: 'Siang', label: 'Siang' },
{ value: 'Sore', label: 'Sore' },
{ value: 'Malam', label: 'Malam' },
];
interface Phase {
@@ -64,20 +71,46 @@ interface Phase {
activityCount?: number;
}
interface Activity {
id: string;
phase_id: string;
name: string;
description?: string;
time_type: string;
}
export function MasterAktivitasContent() {
const [selectedCategory, setSelectedCategory] = useState<string>('');
const [phases, setPhases] = useState<Phase[]>([]);
const [activities, setActivities] = useState<Activity[]>([]);
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_id=${selectedPhase.id}`
: '',
httpClientFetcher,
{
keepPreviousData: true,
}
);
const [showPhaseModal, setShowPhaseModal] = useState(false);
const [showActivityModal, setShowActivityModal] = useState(false);
const [showPhaseDeleteConfirm, setShowPhaseDeleteConfirm] = useState(false);
@@ -94,6 +127,16 @@ export function MasterAktivitasContent() {
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'
);
@@ -114,115 +157,6 @@ export function MasterAktivitasContent() {
setInitialLoading(false);
}, []);
// Fetch phases when category changes
useEffect(() => {
if (selectedCategory) {
fetchPhases(selectedCategory);
} else {
setPhases([]);
setSelectedPhase(null);
setActivities([]);
}
}, [selectedCategory]);
// Fetch activities when phase changes
useEffect(() => {
if (selectedPhase) {
fetchActivities(selectedPhase.id);
} else {
setActivities([]);
}
}, [selectedPhase]);
const fetchPhases = async (
category: string,
preserveSelectedPhaseId?: string
) => {
if (!isSupabaseConfigured()) {
console.warn(
'Supabase not configured. Please add environment variables.'
);
return;
}
try {
const { data, error } = await supabase
.from('phases')
.select('*')
.eq('category', category)
.order('id', { ascending: true }); // ✅ Urutan berdasarkan ID (yang paling awal diinput di atas)
if (error) {
console.error('Error fetching phases:', error);
toast.error('Gagal memuat data phase');
return;
}
// Fetch activity counts for each phase
const phasesWithCounts = await Promise.all(
(data || []).map(async (phase) => {
const { count } = await supabase
.from('phase_activities')
.select('*', { count: 'exact', head: true })
.eq('phase_id', phase.id);
return { ...phase, activityCount: count || 0 };
})
);
setPhases(phasesWithCounts);
// Preserve selected phase if ID is provided
if (preserveSelectedPhaseId) {
const phaseToSelect = phasesWithCounts.find(
(p) => p.id === preserveSelectedPhaseId
);
if (phaseToSelect) {
setSelectedPhase(phaseToSelect);
} else if (phasesWithCounts.length > 0) {
setSelectedPhase(phasesWithCounts[0]);
} else {
setSelectedPhase(null);
}
} else {
// Select first phase by default if exists
if (phasesWithCounts.length > 0) {
setSelectedPhase(phasesWithCounts[0]);
} else {
setSelectedPhase(null);
}
}
} catch (error) {
console.error('Error fetching phases:', error);
toast.error('Gagal memuat data phase');
}
};
const fetchActivities = async (phaseId: string) => {
if (!isSupabaseConfigured()) {
return;
}
try {
const { data, error } = await supabase
.from('phase_activities')
.select('*')
.eq('phase_id', phaseId)
.order('id', { ascending: true }); // ✅ Urutan berdasarkan ID (yang paling awal diinput di atas)
if (error) {
console.error('Error fetching activities:', error);
toast.error('Gagal memuat data aktivitas');
return;
}
setActivities(data || []);
} catch (error) {
console.error('Error fetching activities:', error);
toast.error('Gagal memuat data aktivitas');
}
};
// Phase handlers
const handleAddPhase = () => {
if (!selectedCategory) {
@@ -249,15 +183,13 @@ export function MasterAktivitasContent() {
return;
}
if (!selectedCategory) {
toast.error('Pilih kategori terlebih dahulu');
if (phaseForm.name.trim().length < 3) {
toast.error('Nama phase minimal 3 karakter!');
return;
}
if (!isSupabaseConfigured()) {
toast.error(
'Supabase belum dikonfigurasi. Tambahkan environment variables.'
);
if (!selectedCategory) {
toast.error('Pilih kategori terlebih dahulu');
return;
}
@@ -265,40 +197,40 @@ export function MasterAktivitasContent() {
try {
if (phaseModalMode === 'create') {
const { error } = await supabase.from('phases').insert([
{
name: phaseForm.name.trim(),
category: selectedCategory,
},
]);
const createPhaseResponse = await PhaseApi.create({
category: selectedCategory,
name: phaseForm.name.trim(),
});
if (error) {
console.error('Error creating phase:', error);
if (isResponseError(createPhaseResponse)) {
console.error('Error creating phase:', createPhaseResponse.message);
toast.error('Gagal menambahkan phase');
return;
}
refreshPhases();
toast.success('Phase berhasil ditambahkan');
} else {
const { error } = await supabase
.from('phases')
.update({
const updatePhaseResponse = await PhaseApi.update(
Number(phaseForm.id),
{
name: phaseForm.name.trim(),
})
.eq('id', phaseForm.id);
}
);
if (error) {
console.error('Error updating phase:', error);
toast.error('Gagal mengubah phase');
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: '' });
await fetchPhases(selectedCategory);
} catch (error) {
console.error('Error saving phase:', error);
toast.error('Terjadi kesalahan saat menyimpan phase');
@@ -318,32 +250,16 @@ export function MasterAktivitasContent() {
setLoading(true);
try {
// First, delete all activities in this phase
const { error: activitiesError } = await supabase
.from('phase_activities')
.delete()
.eq('phase_id', phaseToDelete);
const deletePhaseResponse = await PhaseApi.delete(Number(phaseToDelete));
if (activitiesError) {
console.error('Error deleting phase activities:', activitiesError);
toast.error('Gagal menghapus aktivitas phase');
setLoading(false);
return;
}
// Then delete the phase
const { error: phaseError } = await supabase
.from('phases')
.delete()
.eq('id', phaseToDelete);
if (phaseError) {
console.error('Error deleting phase:', phaseError);
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);
@@ -351,10 +267,7 @@ export function MasterAktivitasContent() {
// Clear selection if deleted phase was selected
if (selectedPhase?.id === phaseToDelete) {
setSelectedPhase(null);
setActivities([]);
}
await fetchPhases(selectedCategory);
} catch (error) {
console.error('Error deleting phase:', error);
toast.error('Terjadi kesalahan saat menghapus phase');
@@ -374,10 +287,10 @@ export function MasterAktivitasContent() {
setShowActivityModal(true);
};
const handleEditActivity = (activity: Activity) => {
const handleEditActivity = (activity: PhaseActivity) => {
setActivityModalMode('edit');
setActivityForm({
id: activity.id,
id: String(activity.id),
name: activity.name,
description: activity.description || '',
time_type: activity.time_type,
@@ -391,15 +304,13 @@ export function MasterAktivitasContent() {
return;
}
if (!selectedPhase) {
toast.error('Pilih phase terlebih dahulu');
if (activityForm.name.trim().length < 3) {
toast.error('Nama aktivitas minimal 3 karakter!');
return;
}
if (!isSupabaseConfigured()) {
toast.error(
'Supabase belum dikonfigurasi. Tambahkan environment variables.'
);
if (!selectedPhase) {
toast.error('Pilih phase terlebih dahulu');
return;
}
@@ -407,47 +318,49 @@ export function MasterAktivitasContent() {
try {
if (activityModalMode === 'create') {
const { error } = await supabase.from('phase_activities').insert([
{
phase_id: selectedPhase.id,
name: activityForm.name.trim(),
description: activityForm.description.trim() || null,
time_type: activityForm.time_type,
},
]);
const createActivityResponse = await PhaseActivityApi.create({
phase_id: Number(selectedPhase.id),
name: activityForm.name.trim(),
description: activityForm.description.trim() || '',
time_type: activityForm.time_type,
});
if (error) {
console.error('Error creating activity:', error);
if (isResponseError(createActivityResponse)) {
console.error(
'Error creating activity:',
createActivityResponse.message
);
toast.error('Gagal menambahkan aktivitas');
return;
}
refreshPhaseActivities();
toast.success('Aktivitas berhasil ditambahkan');
} else {
const { error } = await supabase
.from('phase_activities')
.update({
const updateActivityResponse = await PhaseActivityApi.update(
Number(activityForm.id),
{
name: activityForm.name.trim(),
description: activityForm.description.trim() || null,
description: activityForm.description.trim() || '',
time_type: activityForm.time_type,
})
.eq('id', activityForm.id);
}
);
if (error) {
console.error('Error updating activity:', error);
if (isResponseError(updateActivityResponse)) {
console.error(
'Error updating activity:',
updateActivityResponse.message
);
toast.error('Gagal mengubah aktivitas');
return;
}
refreshPhaseActivities();
toast.success('Aktivitas berhasil diubah');
}
setShowActivityModal(false);
setActivityForm({ id: '', name: '', description: '', time_type: 'umum' });
await fetchActivities(selectedPhase.id);
if (selectedCategory) {
await fetchPhases(selectedCategory, selectedPhase.id); // ✅ Preserve selected phase
}
} catch (error) {
console.error('Error saving activity:', error);
toast.error('Terjadi kesalahan saat menyimpan aktivitas');
@@ -467,22 +380,23 @@ export function MasterAktivitasContent() {
setLoading(true);
try {
const { error } = await supabase
.from('phase_activities')
.delete()
.eq('id', activityToDelete);
const deleteActivityResponse = await PhaseActivityApi.delete(
Number(activityToDelete)
);
if (error) {
console.error('Error deleting activity:', error);
if (isResponseError(deleteActivityResponse)) {
console.error(
'Error deleting activity:',
deleteActivityResponse.message
);
toast.error('Gagal menghapus aktivitas');
return;
}
refreshPhaseActivities();
toast.success('Aktivitas berhasil dihapus');
setShowActivityDeleteConfirm(false);
setActivityToDelete(null);
await fetchActivities(selectedPhase.id);
await fetchPhases(selectedCategory, selectedPhase.id); // ✅ Preserve selected phase
} catch (error) {
console.error('Error deleting activity:', error);
toast.error('Terjadi kesalahan saat menghapus aktivitas');
@@ -491,16 +405,12 @@ export function MasterAktivitasContent() {
}
};
const filteredPhases = phases.filter((phase) =>
phase.name.toLowerCase().includes(phaseSearchQuery.toLowerCase())
);
const getTimeTypeLabel = (timeType: string) => {
return TIME_TYPES.find((t) => t.value === timeType)?.label || timeType;
};
const getTimeTypeBadgeClass = (timeType: string) => {
switch (timeType) {
switch (timeType.toLowerCase()) {
case 'umum':
return 'bg-gray-50 text-gray-700 border-gray-300';
case 'pagi':
@@ -610,7 +520,8 @@ export function MasterAktivitasContent() {
{/* Phase List */}
<div className='divide-y divide-gray-200/60'>
{filteredPhases.length === 0 ? (
{!isResponseSuccess(phases) ||
(isResponseSuccess(phases) && phases.data?.length === 0) ? (
<div className='p-8 text-center text-gray-500'>
{phaseSearchQuery
? 'Tidak ada phase yang ditemukan'
@@ -684,9 +595,11 @@ export function MasterAktivitasContent() {
<h2 className='text-lg font-semibold text-gray-900'>
Aktivitas di Phase: {selectedPhase.name}
</h2>
<p className='text-sm text-gray-600 mt-0.5'>
{activities.length} aktivitas
</p>
{isResponseSuccess(phaseActivities) && (
<p className='text-sm text-gray-600 mt-0.5'>
{phaseActivities.data?.length} aktivitas
</p>
)}
</div>
<Button
onClick={handleAddActivity}
@@ -711,7 +624,9 @@ export function MasterAktivitasContent() {
</tr>
</thead>
<tbody className='divide-y divide-gray-200/60'>
{activities.length === 0 ? (
{!isResponseSuccess(phaseActivities) ||
(isResponseSuccess(phaseActivities) &&
phaseActivities.data?.length === 0) ? (
<tr>
<td
colSpan={2}
@@ -721,7 +636,7 @@ export function MasterAktivitasContent() {
</td>
</tr>
) : (
activities.map((activity) => (
phaseActivities.data?.map((activity) => (
<tr
key={activity.id}
className='hover:bg-blue-50/30 transition-colors'
@@ -768,7 +683,7 @@ export function MasterAktivitasContent() {
<DropdownMenuItem
onClick={() =>
handleDeleteActivityClick(
activity.id
String(activity.id)
)
}
className='text-red-600'