import React, { useState, useEffect, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; import { ColumnDef } from '@tanstack/react-table'; import { Info, Pencil, Trash2 } from 'lucide-react'; import { Link, useNavigate } from 'react-router-dom'; import { useForm, Controller } from 'react-hook-form'; import { useCalendars, useUpdateCalendar, useDeleteCalendar, useCalendar, useCalendarStats } from '@/hooks/useCalendars'; import { useExploreView } from '@/hooks/useExploreView'; import { Calendar as CalendarType, CalendarListParams } from '@/types/api'; import { getSavedPageSize } from '@/utils/tablePagination'; import { formatStatusLabel } from '@/utils/statusLabels'; import { ExploreListShell } from '@/components/explore/ExploreListShell'; import { ExploreSearch } from '@/components/explore/ExploreSearch'; import { ExploreDataView } from '@/components/explore/ExploreDataView'; import { ExploreInsightsCollapse } from '@/components/explore/ExploreInsightsCollapse'; import { RowActionsMenu, type RowAction } from '@/components/explore/RowActionsMenu'; import type { DenseRowModel } from '@/components/explore/DenseRowList'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { EntitySlideOver, type ExplorePreviewTarget } from '@/components/EntitySlideOver'; const TABLE_KEY = 'calendars'; const calendarTypeBadgeVariant = (type: string) => { if (type === 'commercial') return 'secondary' as const; return 'default' as const; }; const calendarStatusVariant = (status: string) => { if (status === 'active') return 'default' as const; if (status === 'frozen') return 'secondary' as const; return 'destructive' as const; }; interface EditFormValues { title?: string; description?: string; type?: string; status?: string; category?: string; reason?: string; } function calendarActions( handlers: { onOpen: () => void; onEdit: () => void; onDelete: () => void; }, t: TFunction ): RowAction[] { return [ { label: t('common.open'), icon: , onClick: handlers.onOpen }, { label: t('common.edit'), icon: , onClick: handlers.onEdit }, { label: t('common.delete'), icon: , onClick: handlers.onDelete, destructive: true, separatorBefore: true, }, ]; } const CalendarListPage: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const { viewMode, setViewMode } = useExploreView(); const [preview, setPreview] = useState(null); const [params, setParams] = useState({ limit: getSavedPageSize(TABLE_KEY), offset: 0, sort: 'created_at', order: 'desc', }); const [searchDraft, setSearchDraft] = useState(''); const { data, isLoading } = useCalendars(params); const { data: stats } = useCalendarStats(); const updateCalendar = useUpdateCalendar(); const deleteCalendar = useDeleteCalendar(); const [editModal, setEditModal] = useState<{ open: boolean; calendarId: string | null }>({ open: false, calendarId: null, }); const { data: editingCalendar, isLoading: loadingCalendar } = useCalendar(editModal.calendarId || ''); const editForm = useForm(); const originalCalendarRef = useRef(null); const [deleteConfirm, setDeleteConfirm] = useState<{ open: boolean; calendarId: string | null }>({ open: false, calendarId: null, }); const handleEdit = (id: string) => setEditModal({ open: true, calendarId: id }); const handleSaveEdit = editForm.handleSubmit((values) => { if (!editModal.calendarId || !originalCalendarRef.current) return; const original = originalCalendarRef.current; const cleanedValues: Record = {}; // Only form fields — do not null-out entity keys absent from the form (e.g. id, owner_id). Object.keys(values).forEach((key) => { const newVal = (values as Record)[key]; if (newVal === '' || newVal === undefined || newVal === null) { const origVal = (original as unknown as Record)[key]; if (origVal && origVal !== '-' && origVal !== '' && origVal !== undefined && origVal !== null) { cleanedValues[key] = null; } return; } if (newVal === '-') return; cleanedValues[key] = newVal; }); const payload = Object.fromEntries( Object.entries(cleanedValues).filter(([key, v]) => { const origVal = (original as unknown as Record)[key]; return JSON.stringify(v) !== JSON.stringify(origVal === '-' ? undefined : origVal); }) ); updateCalendar.mutate( { id: editModal.calendarId, data: payload }, { onSuccess: () => setEditModal({ open: false, calendarId: null }) } ); }); useEffect(() => { if (!editModal.open || loadingCalendar || !editingCalendar) return; originalCalendarRef.current = editingCalendar; const clean = (val: unknown) => (val === '-' || val === 'undefined' ? '' : val); editForm.reset({ title: clean(editingCalendar.title) as string, description: clean(editingCalendar.description) as string, type: clean(editingCalendar.type) as string, status: clean(editingCalendar.status) as string, category: clean(editingCalendar.category) as string, reason: clean(editingCalendar.reason) as string, }); }, [editingCalendar, editModal.open, loadingCalendar, editForm]); const handleDelete = (id: string) => setDeleteConfirm({ open: true, calendarId: id }); const confirmDelete = () => { if (!deleteConfirm.calendarId) return; deleteCalendar.mutate(deleteConfirm.calendarId, { onSuccess: () => setDeleteConfirm({ open: false, calendarId: null }), }); }; const applySearch = () => { setParams((prev) => ({ ...prev, q: searchDraft.trim() || undefined, offset: 0 })); }; const exploreStats = useMemo(() => { if (!stats) return []; const items = [{ label: t('common.total'), value: stats.total_calendars }]; Object.entries(stats.calendars_by_status || {}) .slice(0, 3) .forEach(([status, count]) => { items.push({ label: formatStatusLabel(status), value: count }); }); return items.slice(0, 4); }, [stats, t]); const columns = useMemo[]>( () => [ { accessorKey: 'title', header: t('common.title'), enableSorting: true, cell: ({ row, getValue }) => ( setPreview({ type: 'calendar', id: row.original.id })} > {getValue() || row.original.id} ), }, { accessorKey: 'type', header: t('common.type'), size: 110, enableSorting: true, cell: ({ getValue }) => { const type = getValue(); return {type}; }, }, { accessorKey: 'status', header: t('common.status'), size: 100, enableSorting: true, cell: ({ getValue }) => { const status = getValue(); return {formatStatusLabel(status)}; }, }, { accessorKey: 'owner_id', header: t('common.owner'), enableSorting: false, cell: ({ getValue }) => { const ownerId = getValue(); return {ownerId}; }, }, { id: 'rating', header: t('common.rating'), size: 90, enableSorting: false, cell: ({ row }) => `${row.original.rating_avg} (${row.original.rating_count})`, }, { id: 'actions', header: '', size: 48, enableSorting: false, cell: ({ row }) => { const record = row.original; return ( navigate(`/calendars/${record.id}`), onEdit: () => handleEdit(record.id), onDelete: () => handleDelete(record.id), }, t)} /> ); }, }, ], [navigate, t] ); const editTitle = editForm.watch('title'); const editStatus = editForm.watch('status'); const editHasErrors = !editTitle || !editStatus; const denseRows = useMemo(() => { return (data?.data ?? []).map((record) => ({ id: record.id, title: record.title || record.id, subtitle: record.owner_id ? t('explore.ownerId', { id: record.owner_id }) : undefined, badges: ( <> {record.type} {formatStatusLabel(record.status)} > ), meta: `★ ${record.rating_avg} (${record.rating_count})`, onActivate: () => setPreview({ type: 'calendar', id: record.id }), actions: calendarActions({ onOpen: () => navigate(`/calendars/${record.id}`), onEdit: () => handleEdit(record.id), onDelete: () => handleDelete(record.id), }, t), })); }, [data?.data, navigate, t]); return ( <> setParams((prev) => ({ ...prev, status: v === 'all' ? undefined : (v as CalendarListParams['status']), offset: 0, })) } > {t('common.allStatuses')} active frozen deleted setParams((prev) => ({ ...prev, type: v === 'all' ? undefined : (v as CalendarListParams['type']), offset: 0, })) } > {t('common.allTypes')} personal commercial } > {stats && ( ({ id: row.id, primary: {row.title || row.id}, value: row.rating_avg, })), }, { id: 'reviews', label: t('explore.byReviews'), rows: (stats.top_calendars_by_reviews ?? []).map((row) => ({ id: row.id, primary: {row.title || row.id}, value: row.rating_avg, })), }, { id: 'positive', label: t('explore.positive'), rows: (stats.top_calendars_by_positive_reviews ?? []).map((row) => ({ id: row.id, primary: {row.title || row.id}, value: row.rating_avg, })), }, { id: 'negative', label: t('explore.negative'), rows: (stats.top_calendars_by_negative_reviews ?? []).map((row) => ({ id: row.id, primary: {row.title || row.id}, value: row.rating_avg, })), }, ]} /> )} !open && setEditModal({ open: false, calendarId: null })}> {t('calendars.editTitle')} {loadingCalendar ? ( ) : ( {t('common.title')} {t('common.description')} {t('common.type')} ( personal commercial )} /> {t('common.status')} ( active frozen deleted )} /> {t('common.category')} {t('common.reason')} )} setEditModal({ open: false, calendarId: null })}> {t('common.cancel')} {t('common.save')} !open && setDeleteConfirm({ open: false, calendarId: null })}> {t('calendars.deleteTitle')} {t('common.irreversible')} setDeleteConfirm({ open: false, calendarId: null })}> {t('common.cancel')} {t('common.delete')} !open && setPreview(null)} /> > ); }; export default CalendarListPage;
{t('common.irreversible')}