Add manifest.json for graphify-out documentation with metadata for various files

This commit is contained in:
hamed
2026-06-30 22:12:26 +03:30
parent 9eb5a03258
commit cc193c866b
47 changed files with 20368 additions and 42 deletions
+30 -6
View File
@@ -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>
+48 -24
View File
@@ -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>