// Phòng họp (Meeting Rooms catalog) — Phase 10.2 G-O2 (S36 2026-05-28). // Catalog CRUD admin-only — Pattern Master/Catalogs/CatalogsPage table CRUD. // File này MIRROR SHA256 identical với fe-user counterpart. // Pattern 16-bis 4-place mirror foundation (7× cumulative). import { useMemo, useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Building2, Pencil, Plus, Search, Trash2 } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' 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 { Dialog } from '@/components/ui/Dialog' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' import type { MeetingRoomCreateInput, MeetingRoomDto, MeetingRoomUpdateInput } from '@/types/meeting' type FormState = { id?: string code: string name: string capacity: number | '' location: string equipment: string isActive: boolean } const EMPTY_FORM: FormState = { code: '', name: '', capacity: '', location: '', equipment: '', isActive: true, } export function MeetingRoomsPage() { const qc = useQueryClient() const [search, setSearch] = useState('') const [showInactive, setShowInactive] = useState(false) const [open, setOpen] = useState(false) const [form, setForm] = useState(EMPTY_FORM) const isEdit = !!form.id const list = useQuery({ queryKey: ['meeting-rooms', { search, showInactive }], queryFn: async () => (await api.get('/meeting-rooms', { params: { search: search || undefined, isActiveOnly: showInactive ? undefined : true, }, })).data, }) const save = useMutation({ mutationFn: async () => { if (!form.code.trim() || !form.name.trim() || form.capacity === '' || Number(form.capacity) <= 0) { throw new Error('Vui lòng nhập đầy đủ Mã, Tên, Sức chứa (> 0)') } if (isEdit) { const body: MeetingRoomUpdateInput & { id: string } = { id: form.id!, code: form.code.trim(), name: form.name.trim(), capacity: Number(form.capacity), location: form.location.trim() || null, equipment: form.equipment.trim() || null, isActive: form.isActive, } await api.put(`/meeting-rooms/${form.id}`, body) } else { const body: MeetingRoomCreateInput = { code: form.code.trim(), name: form.name.trim(), capacity: Number(form.capacity), location: form.location.trim() || null, equipment: form.equipment.trim() || null, } await api.post('/meeting-rooms', body) } }, onSuccess: () => { toast.success(isEdit ? 'Đã lưu phòng họp' : 'Đã thêm phòng họp') qc.invalidateQueries({ queryKey: ['meeting-rooms'] }) setOpen(false) setForm(EMPTY_FORM) }, onError: err => toast.error(getErrorMessage(err)), }) const remove = useMutation({ mutationFn: async (id: string) => { await api.delete(`/meeting-rooms/${id}`) }, onSuccess: () => { toast.success('Đã tắt phòng họp (IsActive=false)') qc.invalidateQueries({ queryKey: ['meeting-rooms'] }) }, onError: err => toast.error(getErrorMessage(err)), }) function openCreate() { setForm(EMPTY_FORM) setOpen(true) } function openEdit(row: MeetingRoomDto) { setForm({ id: row.id, code: row.code, name: row.name, capacity: row.capacity, location: row.location ?? '', equipment: row.equipment ?? '', isActive: row.isActive, }) setOpen(true) } const filtered = useMemo(() => list.data ?? [], [list.data]) const total = filtered.length return (
Phòng họp } description={list.isLoading ? 'Đang tải…' : `${total} phòng họp`} actions={ } /> {/* Filter row */}
setSearch(e.target.value)} placeholder="Tìm theo mã / tên / vị trí…" className="pl-8" />
{/* Table */}
{list.isLoading && ( )} {!list.isLoading && total === 0 && ( )} {filtered.map(row => ( ))}
Tên Sức chứa Vị trí Thiết bị Trạng thái
Đang tải…
Chưa có phòng họp — bấm Thêm để tạo mới.
{row.code} {row.name} {row.capacity} chỗ {row.location ?? '—'} {row.equipment ?? '—'} {row.isActive ? ( Đang dùng ) : ( Đã tắt )}
{row.isActive && ( )}
setOpen(false)} title={`${isEdit ? 'Sửa' : 'Thêm'} phòng họp`} size="lg" footer={ <> } >
{ e.preventDefault(); save.mutate() }} className="grid grid-cols-1 gap-3 sm:grid-cols-2" >
setForm(s => ({ ...s, code: e.target.value }))} placeholder="VD: ROOM-A1" required maxLength={50} />
setForm(s => ({ ...s, name: e.target.value }))} placeholder="VD: Phòng họp tầng 3" required maxLength={200} />
setForm(s => ({ ...s, capacity: e.target.value === '' ? '' : Number(e.target.value) }))} placeholder="10" required />
setForm(s => ({ ...s, location: e.target.value }))} placeholder="VD: Tầng 3, Toà nhà A" maxLength={200} />