feat(inquiries): top-level Inquiries page (list + detail + convert), nav entries; retire admin inbox
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
127
src/components/inquiries/inquiry-list.tsx
Normal file
127
src/components/inquiries/inquiry-list.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider';
|
||||
import { DataTable } from '@/components/shared/data-table';
|
||||
import { FilterBar } from '@/components/shared/filter-bar';
|
||||
import { ColumnPicker } from '@/components/shared/column-picker';
|
||||
import { PageHeader } from '@/components/shared/page-header';
|
||||
import { EmptyState } from '@/components/shared/empty-state';
|
||||
import { TableSkeleton } from '@/components/shared/loading-skeleton';
|
||||
import { usePaginatedQuery } from '@/hooks/use-paginated-query';
|
||||
import { useTablePreferences } from '@/hooks/use-table-preferences';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { toastError } from '@/lib/api/toast-error';
|
||||
import { inquiryFilterDefinitions } from '@/components/inquiries/inquiry-filters';
|
||||
import {
|
||||
getInquiryColumns,
|
||||
INQUIRY_COLUMN_OPTIONS,
|
||||
INQUIRY_DEFAULT_HIDDEN,
|
||||
type InquiryRow,
|
||||
type InquiryTriageState,
|
||||
} from '@/components/inquiries/inquiry-columns';
|
||||
import { InquiryCard } from '@/components/inquiries/inquiry-card';
|
||||
|
||||
export function InquiryList() {
|
||||
const params = useParams<{ portSlug: string }>();
|
||||
const portSlug = params?.portSlug ?? '';
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { setChrome } = useMobileChrome();
|
||||
useEffect(() => {
|
||||
setChrome({ title: 'Inquiries', showBackButton: false });
|
||||
return () => setChrome({ title: null, showBackButton: false });
|
||||
}, [setChrome]);
|
||||
|
||||
const {
|
||||
data,
|
||||
pagination,
|
||||
isLoading,
|
||||
isFetching,
|
||||
sort,
|
||||
setSort,
|
||||
setPage,
|
||||
setPageSize,
|
||||
filters,
|
||||
setFilter,
|
||||
clearFilters,
|
||||
} = usePaginatedQuery<InquiryRow>({
|
||||
queryKey: ['inquiries'],
|
||||
endpoint: '/api/v1/inquiries',
|
||||
initialSort: { field: 'receivedAt', direction: 'desc' },
|
||||
filterDefinitions: inquiryFilterDefinitions,
|
||||
});
|
||||
|
||||
const triageMutation = useMutation({
|
||||
mutationFn: (args: { id: string; state: InquiryTriageState }) =>
|
||||
apiFetch(`/api/v1/inquiries/${args.id}/triage`, {
|
||||
method: 'PATCH',
|
||||
body: { state: args.state },
|
||||
}),
|
||||
onSuccess: (_d, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['inquiries'] });
|
||||
toast.success(`Marked ${vars.state}.`);
|
||||
},
|
||||
onError: (err: unknown) => toastError(err, 'Update failed'),
|
||||
});
|
||||
|
||||
const columns = getInquiryColumns({
|
||||
portSlug,
|
||||
onTriage: (row, state) => triageMutation.mutate({ id: row.id, state }),
|
||||
});
|
||||
|
||||
const { hidden, setHidden } = useTablePreferences('inquiries', INQUIRY_DEFAULT_HIDDEN);
|
||||
const columnVisibility = Object.fromEntries(hidden.map((id) => [id, false]));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageHeader
|
||||
title="Inquiries"
|
||||
description="Submissions captured from the public marketing site (berth, residence, and contact forms)."
|
||||
variant="gradient"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<FilterBar
|
||||
filters={inquiryFilterDefinitions}
|
||||
values={filters}
|
||||
onChange={setFilter}
|
||||
onClear={clearFilters}
|
||||
/>
|
||||
<div className="ml-auto flex flex-wrap items-center gap-2">
|
||||
<ColumnPicker columns={INQUIRY_COLUMN_OPTIONS} hidden={hidden} onChange={setHidden} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<TableSkeleton />
|
||||
) : (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
data={data}
|
||||
pagination={pagination}
|
||||
onPaginationChange={(p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
}}
|
||||
sort={sort}
|
||||
onSortChange={setSort}
|
||||
isLoading={isFetching && !isLoading}
|
||||
getRowId={(row) => row.id}
|
||||
cardRender={(row) => <InquiryCard inquiry={row.original} portSlug={portSlug} />}
|
||||
emptyState={
|
||||
<EmptyState
|
||||
title="No inquiries found"
|
||||
description="Submissions from the marketing site will appear here."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user