mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat: integrate MasterAktivitasContent component to API
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user