Add manifest.json for graphify-out documentation with metadata for various files
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import { MagnifyingGlassIcon, ChevronUpIcon, ChevronDownIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export interface Column<T> {
|
||||
key: string;
|
||||
@@ -20,6 +20,9 @@ interface Props<T> {
|
||||
emptyMessage?: string;
|
||||
emptyAction?: React.ReactNode;
|
||||
headerExtra?: React.ReactNode;
|
||||
sortKey?: string | null;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
onSort?: (key: string) => void;
|
||||
}
|
||||
|
||||
function SkeletonRow({ cols }: { cols: number }) {
|
||||
@@ -45,6 +48,9 @@ export default function DataTable<T extends object>({
|
||||
emptyMessage = 'هیچ موردی یافت نشد',
|
||||
emptyAction,
|
||||
headerExtra,
|
||||
sortKey,
|
||||
sortDir,
|
||||
onSort,
|
||||
}: Props<T>) {
|
||||
const allColumns = actions
|
||||
? [...columns, { key: '__actions', header: 'اقدامات', className: 'w-[120px]' }]
|
||||
@@ -74,11 +80,29 @@ export default function DataTable<T extends object>({
|
||||
<table className="t">
|
||||
<thead>
|
||||
<tr>
|
||||
{allColumns.map((col) => (
|
||||
<th key={col.key} className={col.className ?? ''}>
|
||||
{col.header}
|
||||
</th>
|
||||
))}
|
||||
{allColumns.map((col) => {
|
||||
const sortable = (col as Column<T>).sortable && onSort;
|
||||
const active = sortKey === col.key;
|
||||
return (
|
||||
<th
|
||||
key={col.key}
|
||||
className={col.className ?? ''}
|
||||
onClick={sortable ? () => onSort!(col.key) : undefined}
|
||||
style={sortable ? { cursor: 'pointer', userSelect: 'none' } : undefined}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
{col.header}
|
||||
{sortable && (
|
||||
active
|
||||
? (sortDir === 'desc'
|
||||
? <ChevronDownIcon style={{ width: 13, height: 13 }} />
|
||||
: <ChevronUpIcon style={{ width: 13, height: 13 }} />)
|
||||
: <ChevronUpDownIcon style={{ width: 13, height: 13, opacity: 0.4 }} />
|
||||
)}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -276,9 +276,13 @@ function ProvincesTab() {
|
||||
const [editTarget, setEditTarget] = useState<Province | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Province | null>(null);
|
||||
|
||||
const [idSort, setIdSort] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortQs = idSort ? `&sort=id&order=${idSort}` : '';
|
||||
const toggleSort = () => { setIdSort((p) => (p === 'asc' ? 'desc' : 'asc')); setPage(1); };
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin-provinces', page, search],
|
||||
queryFn: () => api.get<PaginatedResponse<Province>>(`/api/v1/admin/provinces?page=${page}&limit=20&search=${encodeURIComponent(search)}`),
|
||||
queryKey: ['admin-provinces', page, search, idSort],
|
||||
queryFn: () => api.get<PaginatedResponse<Province>>(`/api/v1/admin/provinces?page=${page}&limit=20&search=${encodeURIComponent(search)}${sortQs}`),
|
||||
});
|
||||
|
||||
const items = data?.data ?? [];
|
||||
@@ -315,7 +319,7 @@ function ProvincesTab() {
|
||||
};
|
||||
|
||||
const columns: Column<Province>[] = [
|
||||
{ key: 'id', header: 'شناسه', render: (p) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{p.id}</span> },
|
||||
{ key: 'id', sortable: true, header: 'شناسه', render: (p) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{p.id}</span> },
|
||||
{ key: 'name', header: 'نام', render: (p) => <b>{p.name}</b> },
|
||||
{ key: 'weight', header: 'ترتیب', render: (p) => <span className="muted">{p.weight}</span> },
|
||||
{ key: 'status', header: 'وضعیت', render: (p) => <SBadge status={p.status} /> },
|
||||
@@ -324,7 +328,7 @@ function ProvincesTab() {
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن استان" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/provinces/export" exportFile="state.json" bundle="provinces" entityLabel="استانها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-provinces'] })} />
|
||||
<DataTable<Province> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در استانها..." emptyMessage="هیچ استانی یافت نشد"
|
||||
<DataTable<Province> columns={columns} data={items} loading={isLoading} sortKey={idSort ? 'id' : null} sortDir={idSort ?? undefined} onSort={toggleSort} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در استانها..." emptyMessage="هیچ استانی یافت نشد"
|
||||
actions={(p) => (
|
||||
<>
|
||||
<button onClick={() => openEdit(p)} className="mini-btn" title="ویرایش"><PencilIcon style={{ width: 14, height: 14 }} /></button>
|
||||
@@ -388,9 +392,13 @@ function CitiesTab() {
|
||||
const [editTarget, setEditTarget] = useState<City | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<City | null>(null);
|
||||
|
||||
const [idSort, setIdSort] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortQs = idSort ? `&sort=id&order=${idSort}` : '';
|
||||
const toggleSort = () => { setIdSort((p) => (p === 'asc' ? 'desc' : 'asc')); setPage(1); };
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin-cities', page, search],
|
||||
queryFn: () => api.get<PaginatedResponse<City>>(`/api/v1/admin/cities?page=${page}&limit=20&search=${encodeURIComponent(search)}`),
|
||||
queryKey: ['admin-cities', page, search, idSort],
|
||||
queryFn: () => api.get<PaginatedResponse<City>>(`/api/v1/admin/cities?page=${page}&limit=20&search=${encodeURIComponent(search)}${sortQs}`),
|
||||
});
|
||||
|
||||
const provincesQuery = useQuery({
|
||||
@@ -472,7 +480,7 @@ function CitiesTab() {
|
||||
};
|
||||
|
||||
const columns: Column<City>[] = [
|
||||
{ key: 'id', header: 'شناسه', render: (c) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{c.id}</span> },
|
||||
{ key: 'id', sortable: true, header: 'شناسه', render: (c) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{c.id}</span> },
|
||||
{ key: 'name', header: 'نام', render: (c) => <b>{c.name}</b> },
|
||||
{ key: 'province_id', header: 'استان', render: (c) => c.province_id ? <span className="chip">{provinceMap[c.province_id] ?? `#${c.province_id}`}</span> : <span className="muted">—</span> },
|
||||
{ key: 'representation_id', header: 'نماینده', render: (c) => c.representation_id ? <span className="muted" style={{ fontSize: 12 }}>{representationMap[c.representation_id] ?? `#${c.representation_id}`}</span> : <span className="muted">—</span> },
|
||||
@@ -484,7 +492,7 @@ function CitiesTab() {
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن شهر" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/cities/export" exportFile="city.json" bundle="cities" entityLabel="شهرها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-cities'] })} />
|
||||
<DataTable<City> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در شهرها..." emptyMessage="هیچ شهری یافت نشد"
|
||||
<DataTable<City> columns={columns} data={items} loading={isLoading} sortKey={idSort ? 'id' : null} sortDir={idSort ?? undefined} onSort={toggleSort} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در شهرها..." emptyMessage="هیچ شهری یافت نشد"
|
||||
actions={(c) => (
|
||||
<>
|
||||
<button onClick={() => openEdit(c)} className="mini-btn" title="ویرایش"><PencilIcon style={{ width: 14, height: 14 }} /></button>
|
||||
@@ -584,9 +592,13 @@ function SpecialtiesTab() {
|
||||
const [editTarget, setEditTarget] = useState<SpecialtyFull | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<SpecialtyFull | null>(null);
|
||||
|
||||
const [idSort, setIdSort] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortQs = idSort ? `&sort=id&order=${idSort}` : '';
|
||||
const toggleSort = () => { setIdSort((p) => (p === 'asc' ? 'desc' : 'asc')); setPage(1); };
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin-specialties', page, search],
|
||||
queryFn: () => api.get<PaginatedResponse<SpecialtyFull>>(`/api/v1/admin/specialties?page=${page}&limit=20&search=${encodeURIComponent(search)}`),
|
||||
queryKey: ['admin-specialties', page, search, idSort],
|
||||
queryFn: () => api.get<PaginatedResponse<SpecialtyFull>>(`/api/v1/admin/specialties?page=${page}&limit=20&search=${encodeURIComponent(search)}${sortQs}`),
|
||||
});
|
||||
|
||||
const { data: rootData } = useQuery({
|
||||
@@ -633,7 +645,7 @@ function SpecialtiesTab() {
|
||||
const parentOptions = rootItems.map((s) => ({ value: s.id, label: s.name }));
|
||||
|
||||
const columns: Column<SpecialtyFull>[] = [
|
||||
{ key: 'id', header: 'شناسه', render: (s) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{s.id}</span> },
|
||||
{ key: 'id', sortable: true, header: 'شناسه', render: (s) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{s.id}</span> },
|
||||
{ key: 'name', header: 'نام', render: (s) => <b>{s.name}</b> },
|
||||
{ key: 'slug', header: 'slug', render: (s) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{s.slug}</span> },
|
||||
{ key: 'parent_id', header: 'والد', render: (s) => s.parent_id ? <span className="chip">{parentMap[s.parent_id] ?? `#${s.parent_id}`}</span> : <span className="muted">—</span> },
|
||||
@@ -644,7 +656,7 @@ function SpecialtiesTab() {
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن تخصص" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/specialties/export" exportFile="specialties.json" bundle="specialties" entityLabel="تخصصها" onImported={() => { qc.invalidateQueries({ queryKey: ['admin-specialties'] }); qc.invalidateQueries({ queryKey: ['admin-specialties-roots'] }); }} />
|
||||
<DataTable<SpecialtyFull> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در تخصصها..." emptyMessage="هیچ تخصصی یافت نشد"
|
||||
<DataTable<SpecialtyFull> columns={columns} data={items} loading={isLoading} sortKey={idSort ? 'id' : null} sortDir={idSort ?? undefined} onSort={toggleSort} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در تخصصها..." emptyMessage="هیچ تخصصی یافت نشد"
|
||||
actions={(s) => (
|
||||
<>
|
||||
<button onClick={() => openEdit(s)} className="mini-btn" title="ویرایش"><PencilIcon style={{ width: 14, height: 14 }} /></button>
|
||||
@@ -708,9 +720,13 @@ function DoctorServicesTab() {
|
||||
const [editTarget, setEditTarget] = useState<DoctorService | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<DoctorService | null>(null);
|
||||
|
||||
const [idSort, setIdSort] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortQs = idSort ? `&sort=id&order=${idSort}` : '';
|
||||
const toggleSort = () => { setIdSort((p) => (p === 'asc' ? 'desc' : 'asc')); setPage(1); };
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin-doctor-services', page, search],
|
||||
queryFn: () => api.get<PaginatedResponse<DoctorService>>(`/api/v1/admin/doctor-services?page=${page}&limit=20&search=${encodeURIComponent(search)}`),
|
||||
queryKey: ['admin-doctor-services', page, search, idSort],
|
||||
queryFn: () => api.get<PaginatedResponse<DoctorService>>(`/api/v1/admin/doctor-services?page=${page}&limit=20&search=${encodeURIComponent(search)}${sortQs}`),
|
||||
});
|
||||
|
||||
const specialtiesQuery = useQuery({
|
||||
@@ -755,7 +771,7 @@ function DoctorServicesTab() {
|
||||
};
|
||||
|
||||
const columns: Column<DoctorService>[] = [
|
||||
{ key: 'id', header: 'شناسه', render: (s) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{s.id}</span> },
|
||||
{ key: 'id', sortable: true, header: 'شناسه', render: (s) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{s.id}</span> },
|
||||
{ key: 'name', header: 'نام', render: (s) => <b>{s.name}</b> },
|
||||
{ key: 'specialty_id', header: 'تخصص', render: (s) => s.specialty_id ? <span className="badge violet"><span className="bdot" />{specialtyMap[s.specialty_id] ?? `#${s.specialty_id}`}</span> : <span className="muted">—</span> },
|
||||
{ key: 'weight', header: 'ترتیب', render: (s) => <span className="muted">{s.weight}</span> },
|
||||
@@ -765,7 +781,7 @@ function DoctorServicesTab() {
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن خدمت" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/doctor_services/export" exportFile="doctor-services.json" bundle="doctor_services" entityLabel="خدمات پزشک" onImported={() => qc.invalidateQueries({ queryKey: ['admin-doctor-services'] })} />
|
||||
<DataTable<DoctorService> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در خدمات..." emptyMessage="هیچ خدمتی یافت نشد"
|
||||
<DataTable<DoctorService> columns={columns} data={items} loading={isLoading} sortKey={idSort ? 'id' : null} sortDir={idSort ?? undefined} onSort={toggleSort} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در خدمات..." emptyMessage="هیچ خدمتی یافت نشد"
|
||||
actions={(s) => (
|
||||
<>
|
||||
<button onClick={() => openEdit(s)} className="mini-btn" title="ویرایش"><PencilIcon style={{ width: 14, height: 14 }} /></button>
|
||||
@@ -830,9 +846,13 @@ function InsurancesTab() {
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [uploadTarget, setUploadTarget] = useState<number | null>(null);
|
||||
|
||||
const [idSort, setIdSort] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortQs = idSort ? `&sort=id&order=${idSort}` : '';
|
||||
const toggleSort = () => { setIdSort((p) => (p === 'asc' ? 'desc' : 'asc')); setPage(1); };
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin-insurances', page, search],
|
||||
queryFn: () => api.get<PaginatedResponse<Insurance>>(`/api/v1/admin/insurances?page=${page}&limit=20&search=${encodeURIComponent(search)}`),
|
||||
queryKey: ['admin-insurances', page, search, idSort],
|
||||
queryFn: () => api.get<PaginatedResponse<Insurance>>(`/api/v1/admin/insurances?page=${page}&limit=20&search=${encodeURIComponent(search)}${sortQs}`),
|
||||
});
|
||||
|
||||
const items = data?.data ?? [];
|
||||
@@ -871,7 +891,7 @@ function InsurancesTab() {
|
||||
};
|
||||
|
||||
const columns: Column<Insurance>[] = [
|
||||
{ key: 'id', header: 'شناسه', render: (i) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{i.id}</span> },
|
||||
{ key: 'id', sortable: true, header: 'شناسه', render: (i) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{i.id}</span> },
|
||||
{ key: 'name', header: 'نام', render: (i) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{i.logo_url ? (
|
||||
@@ -891,7 +911,7 @@ function InsurancesTab() {
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن بیمه" onClick={() => { reset({ status: '1', type: 'basic' }); setLogoUrl(null); setUploadTarget(null); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/insurances/export" exportFile="insurances.json" bundle="insurances" entityLabel="بیمهها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-insurances'] })} />
|
||||
<DataTable<Insurance> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در بیمهها..." emptyMessage="هیچ بیمهای یافت نشد"
|
||||
<DataTable<Insurance> columns={columns} data={items} loading={isLoading} sortKey={idSort ? 'id' : null} sortDir={idSort ?? undefined} onSort={toggleSort} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در بیمهها..." emptyMessage="هیچ بیمهای یافت نشد"
|
||||
actions={(i) => (
|
||||
<>
|
||||
<button onClick={() => openEdit(i)} className="mini-btn" title="ویرایش"><PencilIcon style={{ width: 14, height: 14 }} /></button>
|
||||
@@ -957,9 +977,13 @@ function TagsTab() {
|
||||
const [editTarget, setEditTarget] = useState<Tag | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Tag | null>(null);
|
||||
|
||||
const [idSort, setIdSort] = useState<'asc' | 'desc' | null>(null);
|
||||
const sortQs = idSort ? `&sort=id&order=${idSort}` : '';
|
||||
const toggleSort = () => { setIdSort((p) => (p === 'asc' ? 'desc' : 'asc')); setPage(1); };
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin-tags', page, search],
|
||||
queryFn: () => api.get<PaginatedResponse<Tag>>(`/api/v1/admin/tags?page=${page}&limit=20&search=${encodeURIComponent(search)}`),
|
||||
queryKey: ['admin-tags', page, search, idSort],
|
||||
queryFn: () => api.get<PaginatedResponse<Tag>>(`/api/v1/admin/tags?page=${page}&limit=20&search=${encodeURIComponent(search)}${sortQs}`),
|
||||
});
|
||||
|
||||
const items = data?.data ?? [];
|
||||
@@ -996,7 +1020,7 @@ function TagsTab() {
|
||||
};
|
||||
|
||||
const columns: Column<Tag>[] = [
|
||||
{ key: 'id', header: 'شناسه', render: (t) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{t.id}</span> },
|
||||
{ key: 'id', sortable: true, header: 'شناسه', render: (t) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{t.id}</span> },
|
||||
{ key: 'name', header: 'نام', render: (t) => <span className="chip">{t.name}</span> },
|
||||
{ key: 'slug', header: 'slug', render: (t) => <span className="muted" style={{ fontFamily: 'monospace', fontSize: 12 }}>{t.slug}</span> },
|
||||
{ key: 'status', header: 'وضعیت', render: (t) => <SBadge status={t.status} /> },
|
||||
@@ -1005,7 +1029,7 @@ function TagsTab() {
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن تگ" onClick={() => { reset({ status: '1' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/tags/export" exportFile="tags.json" bundle="tags" entityLabel="تگها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-tags'] })} />
|
||||
<DataTable<Tag> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در تگها..." emptyMessage="هیچ تگی یافت نشد"
|
||||
<DataTable<Tag> columns={columns} data={items} loading={isLoading} sortKey={idSort ? 'id' : null} sortDir={idSort ?? undefined} onSort={toggleSort} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در تگها..." emptyMessage="هیچ تگی یافت نشد"
|
||||
actions={(t) => (
|
||||
<>
|
||||
<button onClick={() => openEdit(t)} className="mini-btn" title="ویرایش"><PencilIcon style={{ width: 14, height: 14 }} /></button>
|
||||
|
||||
Reference in New Issue
Block a user