// AdminView.jsx — لوحة تحكم الأدمن // التبويبات: نظرة عامة، المستخدمين، المحامون، الحجوزات، الاشتراكات، الخطط، الكوبونات // // الوصول: فقط للمستخدمين اللي is_admin=true (الـ wiring في Platform.html بيشيك على ده) /* ── Shared styles ─────────────────────────────────── */ const adminStyles = { card: { background: sanadColors.bgElevated, borderRadius: 12, boxShadow: '0 0 0 1px rgba(239,231,214,0.08), 0 2px 6px rgba(0,0,0,0.04)', }, emptyBox: { padding: '48px 24px', textAlign: 'center', background: sanadColors.bgElevated, borderRadius: 12, boxShadow: '0 0 0 1px rgba(239,231,214,0.08)', color: sanadColors.textMuted, fontSize: 14, }, btnPrimary: { padding: '7px 14px', borderRadius: 8, border: 'none', background: sanadColors.brand, color: '#fff', cursor: 'pointer', fontFamily: sanadFonts.arabic, fontSize: 12, fontWeight: 600, }, btnNeutral: { padding: '6px 14px', borderRadius: 8, border: `1px solid ${sanadColors.border}`, background: sanadColors.bgElevated, color: sanadColors.textSecondary, cursor: 'pointer', fontFamily: sanadFonts.arabic, fontSize: 12, fontWeight: 500, }, btnDanger: { padding: '6px 12px', borderRadius: 8, border: `1px solid ${sanadColors.error}`, background: 'transparent', color: sanadColors.error, cursor: 'pointer', fontFamily: sanadFonts.arabic, fontSize: 12, fontWeight: 500, }, pill: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 9px', borderRadius: 9999, fontSize: 11, fontWeight: 600, }, tableHead: { fontSize: 11, fontWeight: 700, color: sanadColors.textMuted, padding: '10px 14px', textAlign: 'right', background: sanadColors.bgSubtle, letterSpacing: '0.02em', }, tableCell: { fontSize: 12.5, color: sanadColors.textPrimary, padding: '11px 14px', borderTop: `1px solid ${sanadColors.borderLight}`, textAlign: 'right', verticalAlign: 'middle', }, }; function fmtAdminDate(iso, withTime = false) { if (!iso) return '—'; try { const d = new Date(iso); return d.toLocaleString('ar-SA', withTime ? { dateStyle: 'short', timeStyle: 'short' } : { dateStyle: 'medium' }); } catch { return iso; } } const USER_TYPE_LABEL = { individual: 'فرد', lawyer: 'محامي', firm: 'مكتب', student: 'طالب', business: 'منشأة', }; const APPROVAL_LABEL = { pending: { label: 'قيد المراجعة', bg: 'rgba(224,160,82,0.14)', color: '#e0a052' }, approved: { label: 'معتمد', bg: 'rgba(95,191,138,0.16)', color: sanadColors.success }, rejected: { label: 'مرفوض', bg: 'rgba(255,138,138,0.12)', color: sanadColors.error }, }; const BOOKING_STATUS_LABEL = { requested: { label: 'مطلوب', bg: '#1d2a25', color: sanadColors.textSecondary }, awaiting_lawyer: { label: 'بانتظار المحامي', bg: 'rgba(224,160,82,0.14)', color: '#e0a052' }, approved: { label: 'بانتظار الدفع', bg: 'rgba(224,160,82,0.14)', color: '#e0a052' }, paid: { label: 'مدفوع', bg: 'rgba(95,191,138,0.16)', color: sanadColors.success }, active: { label: 'نشط', bg: sanadColors.brand, color: '#fff' }, done: { label: 'مكتمل', bg: '#1d2a25', color: sanadColors.textSecondary }, rejected: { label: 'مرفوض', bg: 'rgba(255,138,138,0.12)', color: sanadColors.error }, cancelled: { label: 'ملغي', bg: '#1d2a25', color: sanadColors.textSecondary }, }; /* ══════════════════════════════════════════════════════ * Tab 1: نظرة عامة (Overview + Activity chart) * ══════════════════════════════════════════════════════ */ function AdminOverviewTab({ onJumpTab }) { const [overview, setOverview] = React.useState(null); const [activity, setActivity] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [days, setDays] = React.useState(7); React.useEffect(() => { let cancel = false; (async () => { setLoading(true); try { const [o, a] = await Promise.all([ window.api.admin.statsOverview(), window.api.admin.statsActivity(days), ]); if (!cancel) { setOverview(o); setActivity(a); setError(null); } } catch (e) { if (!cancel) setError(e.message || 'فشل جلب الإحصائيات'); } finally { if (!cancel) setLoading(false); } })(); return () => { cancel = true; }; }, [days]); if (loading) return
جارٍ التحميل…
; if (error) return
{error}
; if (!overview) return null; const userTypes = Object.entries(overview.users_by_type || {}); // au1: سوقُ المحامين مؤجَّل — بطاقاتُ هذه النسخة: المستخدمون، الاشتراكات، الإيرادات، المساعد const primaryKpis = !ADMIN_SHOW_MARKETPLACE ? [ { label: 'إجمالي المستخدمين', value: overview.users_total, sub: `${overview.users_active} حساب مفعّل · ${overview.users_admins} أدمن` }, { label: 'اشتراكات سارية', value: overview.subscriptions_active, sub: 'نشطة ولم ينتهِ تاريخها' }, { label: 'قيمة الاشتراكات السارية', value: `${overview.subscriptions_revenue.toLocaleString('en-US')} ر.س`, sub: 'قبل الضريبة · الدفع في نسخة العرض تجريبيّ', accent: true }, { label: 'محادثات المساعد', value: overview.conversations_total, sub: `${overview.messages_total.toLocaleString('en-US')} رسالة` }, ] : [ { label: 'إجمالي المستخدمين', value: overview.users_total, sub: `${overview.users_active} نشط · ${overview.users_admins} أدمن`, }, { label: 'المحامون المعتمدون', value: overview.lawyers_approved, sub: `من ${overview.lawyers_total} بروفايل`, }, { label: 'الحجوزات', value: overview.bookings_total, sub: `${overview.bookings_active} نشط · ${overview.bookings_completed} مكتمل`, }, { label: 'إجمالي إيرادات المنصة', value: `${overview.platform_fee_collected.toLocaleString('ar')} ر.س`, sub: `من ${overview.bookings_revenue.toLocaleString('ar')} ر.س جلسات`, accent: true, }, ]; // Secondary KPIs — اشتراكات + AI const secondaryKpis = !ADMIN_SHOW_MARKETPLACE ? [ { label: 'نقاط مستهلكة في المساعد', value: overview.credits_used_total, sub: 'أسئلةُ المساعد (الخلاصة والشرح غير محسوبين هنا)' }, ] : [ { label: 'اشتراكات نشطة', value: overview.subscriptions_active, sub: `${overview.subscriptions_revenue.toLocaleString('ar')} ر.س إيرادات`, }, { label: 'محادثات المساعد', value: overview.conversations_total, sub: `${overview.messages_total} رسالة`, }, { label: 'كريدت مستخدمة', value: overview.credits_used_total, sub: 'إجمالي الاستهلاك', }, ]; const maxCount = Math.max( 1, ...(activity?.new_users || []).map(d => d.count), ...(ADMIN_SHOW_MARKETPLACE ? (activity?.new_bookings || []) : []).map(d => d.count), ...(activity?.new_subscriptions || []).map(d => d.count), ); return (
{/* Pending lawyers callout */} {ADMIN_SHOW_MARKETPLACE && overview.lawyers_pending > 0 && (
{overview.lawyers_pending} طلب اعتماد محامي بانتظار المراجعة
راجع طلبات المحامين الجدد قبل أن يظهروا في السوق
)} {/* Pending documents callout */} {ADMIN_SHOW_MARKETPLACE && overview.docs_pending > 0 && (
{overview.docs_pending} مستند تحقّق بانتظار المراجعة
راجع رخص المحاماة والهويات والشهادات قبل اعتماد المحامي
)} {/* Pending videos callout */} {ADMIN_SHOW_MARKETPLACE && overview.videos_pending > 0 && (
{overview.videos_pending} فيديو تعريفي بانتظار المراجعة
راجع فيديوهات المحامين قبل ظهورها في كروتهم العامة
)} {/* Primary KPIs */}
{primaryKpis.map((k, i) => (
{k.label}
{typeof k.value === 'number' ? k.value.toLocaleString('ar') : k.value}
{k.sub}
))}
{/* Secondary KPIs */}
{secondaryKpis.map((k, i) => (
{k.label}
{typeof k.value === 'number' ? k.value.toLocaleString('ar') : k.value}
{k.sub}
))}
{/* Users by type + Lawyer approval breakdown */}
توزيع المستخدمين حسب النوع
{userTypes.length === 0 ? (
لا توجد بيانات
) : (
{userTypes.map(([type, count]) => { const pct = overview.users_total ? Math.round((count / overview.users_total) * 100) : 0; return (
{USER_TYPE_LABEL[type] || type} {count} ({pct}%)
); })}
)}
{ADMIN_SHOW_MARKETPLACE &&
بروفايلات المحامين
{overview.lawyers_total === 0 ? (
لا يوجد محامون مسجّلون بعد
) : (
{[ { key: 'approved', label: 'معتمد', count: overview.lawyers_approved, color: sanadColors.success }, { key: 'pending', label: 'قيد المراجعة', count: overview.lawyers_pending, color: '#e0a052' }, { key: 'rejected', label: 'مرفوض', count: overview.lawyers_rejected, color: sanadColors.error }, ].map(row => { const pct = overview.lawyers_total ? Math.round((row.count / overview.lawyers_total) * 100) : 0; return (
{row.label} {row.count} ({pct}%)
); })}
)}
}
{/* Activity chart */}
النشاط الأخير
{[7, 14, 30].map(n => ( ))}
{/* Legend */}
مستخدمون جدد {ADMIN_SHOW_MARKETPLACE && حجوزات } اشتراكات
{/* Simple bar chart */}
{(activity?.new_users || []).map((day, i) => { const bookings = ADMIN_SHOW_MARKETPLACE ? (activity.new_bookings[i]?.count || 0) : 0; const subs = activity.new_subscriptions[i]?.count || 0; const users = day.count; return (
0 ? 3 : 0 }} /> {ADMIN_SHOW_MARKETPLACE &&
0 ? 3 : 0 }} />}
0 ? 3 : 0 }} />
{day.date.slice(5)}
); })}
); } /* ══════════════════════════════════════════════════════ * Tab 2: المستخدمين * ══════════════════════════════════════════════════════ */ function AdminUsersTab({ currentAdminId }) { const [users, setUsers] = React.useState([]); const [total, setTotal] = React.useState(0); const [page, setPage] = React.useState(1); const [search, setSearch] = React.useState(''); const [userType, setUserType] = React.useState(''); const [loading, setLoading] = React.useState(true); const [editing, setEditing] = React.useState(null); // user being edited const [resetting, setResetting] = React.useState(null); const PAGE_SIZE = 20; const refresh = React.useCallback(async () => { setLoading(true); try { const params = { page, page_size: PAGE_SIZE }; if (search) params.search = search; if (userType) params.user_type = userType; const res = await window.api.admin.users.list(params); setUsers(res.items || []); setTotal(res.total || 0); } catch (e) { alert('فشل جلب المستخدمين: ' + e.message); } finally { setLoading(false); } }, [page, search, userType]); React.useEffect(() => { refresh(); }, [refresh]); const handleDelete = async (u) => { if (u.id === currentAdminId) { alert('لا يمكنك حذف نفسك'); return; } if (!window.confirm(`حذف ${u.full_name || u.email}؟ (يحذف كل بياناته)`)) return; try { await window.api.admin.users.delete(u.id); refresh(); } catch (e) { alert(e.message); } }; const handleToggleActive = async (u) => { if (u.id === currentAdminId && u.is_active) { alert('لا يمكنك تعطيل حسابك أنت'); return; } if (u.is_active && !window.confirm(`تعطيل حساب ${u.full_name || u.email}؟`)) return; try { await window.api.admin.users.update(u.id, { is_active: !u.is_active }); refresh(); } catch (e) { alert(e.message); } }; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); return (
{/* Filters */}
{ setPage(1); setSearch(e.target.value); }} style={{ padding: '8px 12px', borderRadius: 8, border: `1px solid ${sanadColors.border}`, background: sanadColors.bgElevated, fontFamily: sanadFonts.arabic, fontSize: 13, width: 240, outline: 'none', direction: 'rtl', }} /> الإجمالي: {total.toLocaleString('ar')}
{loading ? (
جارٍ التحميل…
) : users.length === 0 ? (
لا يوجد مستخدمون مطابقون
) : (
{users.map(u => ( ))}
# الاسم الإيميل النوع الحالة سُجِّل إجراءات
{u.id}
{u.full_name || '—'}
{u.email} {USER_TYPE_LABEL[u.user_type] || u.user_type}
{u.is_active ? 'نشط' : 'معطّل'} {u.is_admin && ( أدمن )} {u.is_verified && ( موثّق )}
{fmtAdminDate(u.created_at)}
)} {/* Pagination */} {totalPages > 1 && (
صفحة {page} من {totalPages}
)} {/* Edit modal */} {editing && ( setEditing(null)} onSaved={() => { setEditing(null); refresh(); }} /> )} {resetting && ( setResetting(null)} onDone={() => setResetting(null)} /> )}
); } function AdminEditUserModal({ user, isSelf, onClose, onSaved }) { const [fullName, setFullName] = React.useState(user.full_name || ''); const [userType, setUserType] = React.useState(user.user_type); const [isActive, setIsActive] = React.useState(user.is_active); const [isVerified, setIsVerified] = React.useState(user.is_verified); const [isAdmin, setIsAdmin] = React.useState(user.is_admin); const [saving, setSaving] = React.useState(false); const [err, setErr] = React.useState(null); const handleSave = async () => { setSaving(true); setErr(null); try { await window.api.admin.users.update(user.id, { full_name: fullName, user_type: userType, is_active: isActive, is_verified: isVerified, is_admin: isAdmin, }); onSaved(); } catch (e) { setErr(e.message); } finally { setSaving(false); } }; return (
setFullName(e.target.value)} style={inputStyle} />
{err &&
{err}
}
); } function AdminResetPasswordModal({ user, onClose, onDone }) { const [pw, setPw] = React.useState(''); const [saving, setSaving] = React.useState(false); const [err, setErr] = React.useState(null); const handleSave = async () => { if (pw.length < 8) { setErr('كلمة المرور 8 أحرف على الأقل'); return; } setSaving(true); setErr(null); try { await window.api.admin.users.resetPassword(user.id, pw); alert(`تم إعادة تعيين كلمة المرور لـ ${user.email}`); onDone(); } catch (e) { setErr(e.message); } finally { setSaving(false); } }; return (
setPw(e.target.value)} autoFocus style={inputStyle} placeholder="••••••••" /> {err &&
{err}
}
); } /* ══════════════════════════════════════════════════════ * Tab 3: المحامين (موافقة/رفض) * ══════════════════════════════════════════════════════ */ function AdminLawyersTab() { const [lawyers, setLawyers] = React.useState([]); const [total, setTotal] = React.useState(0); const [page, setPage] = React.useState(1); const [filter, setFilter] = React.useState(''); // ''|pending|approved|rejected const [videoFilter, setVideoFilter] = React.useState(''); // ''|pending|approved|rejected|none const [search, setSearch] = React.useState(''); const [searchDebounced, setSearchDebounced] = React.useState(''); const [loading, setLoading] = React.useState(true); const [editing, setEditing] = React.useState(null); const [videoPreview, setVideoPreview] = React.useState(null); const [docsModal, setDocsModal] = React.useState(null); // lawyer whose docs we're viewing const PAGE_SIZE = 20; React.useEffect(() => { const t = setTimeout(() => setSearchDebounced(search), 300); return () => clearTimeout(t); }, [search]); const refresh = React.useCallback(async () => { setLoading(true); try { const params = { page, page_size: PAGE_SIZE }; if (filter) params.approval_status = filter; if (videoFilter) params.video_status = videoFilter; if (searchDebounced) params.search = searchDebounced; const res = await window.api.admin.lawyers.list(params); setLawyers(res.items || []); setTotal(res.total || 0); } catch (e) { alert('فشل: ' + e.message); } finally { setLoading(false); } }, [page, filter, videoFilter, searchDebounced]); React.useEffect(() => { refresh(); }, [refresh]); const setStatus = async (lw, status) => { try { await window.api.admin.lawyers.update(lw.id, { approval_status: status }); refresh(); } catch (e) { alert(e.message); } }; const toggleVerified = async (lw) => { try { await window.api.admin.lawyers.update(lw.id, { is_verified: !lw.is_verified }); refresh(); } catch (e) { alert(e.message); } }; const togglePremium = async (lw) => { try { await window.api.admin.lawyers.update(lw.id, { is_premium: !lw.is_premium }); refresh(); } catch (e) { alert(e.message); } }; const setVideoStatus = async (lw, vstatus, reason = null) => { try { const body = { video_approval_status: vstatus }; if (reason) body.video_rejection_reason = reason; await window.api.admin.lawyers.update(lw.id, body); refresh(); } catch (e) { alert(e.message); } }; const handleVideoReject = (lw) => { const reason = window.prompt('سبب رفض الفيديو (يظهر للمحامي):', 'الفيديو غير واضح أو الصوت رديء، نرجو إعادة الرفع.'); if (!reason) return; setVideoStatus(lw, 'rejected', reason); }; const handleDelete = async (lw) => { if (!window.confirm(`حذف بروفايل ${lw.display_name}؟`)) return; try { await window.api.admin.lawyers.delete(lw.id); refresh(); } catch (e) { alert(e.message); } }; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); return (
{/* Search */}
{ setPage(1); setSearch(e.target.value); }} style={{ padding: '8px 12px', borderRadius: 8, border: `1px solid ${sanadColors.border}`, background: sanadColors.bgElevated, fontFamily: sanadFonts.arabic, fontSize: 13, width: '100%', maxWidth: 420, outline: 'none', direction: 'rtl', }} />
{/* Filters — Approval status */}
البروفايل: {[ { id: '', label: 'الكل' }, { id: 'pending', label: 'قيد المراجعة' }, { id: 'approved', label: 'معتمد' }, { id: 'rejected', label: 'مرفوض' }, ].map(f => { const active = filter === f.id; return ( ); })} الإجمالي: {total}
{/* Filters — Video status */}
الفيديو: {[ { id: '', label: 'الكل' }, { id: 'pending', label: '⏳ ينتظر مراجعة' }, { id: 'approved', label: '✓ معتمد' }, { id: 'rejected', label: '✕ مرفوض' }, { id: 'none', label: 'بدون فيديو' }, ].map(f => { const active = videoFilter === f.id; return ( ); })}
{loading ? (
جارٍ التحميل…
) : lawyers.length === 0 ? (
لا يوجد محامون في هذه الفئة
) : (
{lawyers.map(lw => { const ap = APPROVAL_LABEL[lw.approval_status] || APPROVAL_LABEL.pending; const avatar = lw.avatar_url || lw.photo_url; return (
{/* Avatar */}
{avatar ? : }
{lw.display_name} {ap.label} {lw.profile_type === 'firm' && ( مكتب )} {lw.is_verified && ( موثّق )} {lw.is_premium && ( مميّز )} {lw.video_approval_status === 'pending' && ( ⏳ فيديو ينتظر مراجعة )} {lw.video_approval_status === 'approved' && lw.intro_video_url && ( 🎥 فيديو معتمد )} {lw.video_approval_status === 'rejected' && ( ✕ فيديو مرفوض )}
{lw.specialty}
{lw.bio && (
{lw.bio.length > 200 ? lw.bio.slice(0, 200) + '…' : lw.bio}
)}
السعر: {lw.price_per_session} ر.س الخبرة: {lw.years_experience} سنة {(lw.cities || []).length > 0 && المدن: {lw.cities.join('، ')}} سُجّل: {fmtAdminDate(lw.created_at)}
{/* Actions */}
{/* Profile approval row */}
{lw.approval_status !== 'approved' && ( )} {lw.approval_status !== 'rejected' && ( )} {lw.approval_status === 'rejected' && ( )}
{/* Video review row */} {lw.intro_video_url && (
{lw.video_approval_status !== 'approved' && ( )} {lw.video_approval_status !== 'rejected' && ( )}
)} {/* Verified/premium/delete row */}
); })}
)} {totalPages > 1 && (
صفحة {page} من {totalPages}
)} {editing && ( setEditing(null)} onDone={() => { setEditing(null); refresh(); }} /> )} {videoPreview && ( setVideoPreview(null)} onApprove={() => { setVideoStatus(videoPreview, 'approved'); setVideoPreview(null); }} onReject={() => { handleVideoReject(videoPreview); setVideoPreview(null); }} /> )} {docsModal && ( setDocsModal(null)} onChanged={refresh} /> )}
); } const DOC_TYPE_LABEL = { license: { label: 'رخصة محاماة', color: sanadColors.brand }, national_id: { label: 'هوية وطنية', color: '#4a7c6f' }, degree: { label: 'شهادة جامعية', color: '#5b6fa4' }, cr: { label: 'سجل تجاري', color: '#7a5c8a' }, other: { label: 'أخرى', color: sanadColors.textMuted }, }; function AdminLawyerDocumentsModal({ lawyer, onClose, onChanged }) { const [docs, setDocs] = React.useState([]); const [loading, setLoading] = React.useState(true); const [previewDoc, setPreviewDoc] = React.useState(null); const refresh = React.useCallback(async () => { setLoading(true); try { const data = await window.api.admin.documents.forLawyer(lawyer.id); setDocs(data || []); } catch (e) { alert(e.message); } finally { setLoading(false); } }, [lawyer.id]); React.useEffect(() => { refresh(); }, [refresh]); const handleApprove = async (d) => { try { await window.api.admin.documents.review(d.id, 'approved'); refresh(); onChanged && onChanged(); } catch (e) { alert(e.message); } }; const handleReject = async (d) => { const reason = window.prompt('سبب الرفض (يظهر للمحامي):', 'صورة المستند غير واضحة، نرجو إعادة الرفع.'); if (!reason) return; try { await window.api.admin.documents.review(d.id, 'rejected', reason); refresh(); onChanged && onChanged(); } catch (e) { alert(e.message); } }; const handleDelete = async (d) => { if (!window.confirm(`حذف "${d.title}" نهائياً؟`)) return; try { await window.api.admin.documents.delete(d.id); refresh(); onChanged && onChanged(); } catch (e) { alert(e.message); } }; const pending = docs.filter(d => d.approval_status === 'pending').length; const approved = docs.filter(d => d.approval_status === 'approved').length; const rejected = docs.filter(d => d.approval_status === 'rejected').length; return (
هذه الوثائق المرفوعة للتحقق من المحامي. اعتمد المستندات الصحيحة، ارفض الناقصة مع كتابة السبب.
{/* KPI row */}
الإجمالي
{docs.length}
{pending > 0 && (
ينتظر مراجعة
{pending}
)}
معتمد
{approved}
{rejected > 0 && (
مرفوض
{rejected}
)}
{loading ? (
جارٍ التحميل…
) : docs.length === 0 ? (
لم يرفع المحامي أى مستندات بعد
) : (
{docs.map(d => { const typeMeta = DOC_TYPE_LABEL[d.doc_type] || DOC_TYPE_LABEL.other; const statusMeta = { pending: { label: 'ينتظر مراجعة', bg: 'rgba(224,160,82,0.14)', color: '#e0a052' }, approved: { label: 'معتمد', bg: 'rgba(95,191,138,0.16)', color: sanadColors.success }, rejected: { label: 'مرفوض', bg: 'rgba(255,138,138,0.12)', color: sanadColors.error }, }[d.approval_status] || { label: d.approval_status, bg: sanadColors.bgSubtle, color: sanadColors.textSecondary }; return (
{typeMeta.label} {statusMeta.label} رُفع: {fmtAdminDate(d.uploaded_at, true)} {d.reviewed_at && ` · روجِع: ${fmtAdminDate(d.reviewed_at, true)}`}
{d.title}
{d.mime_type || 'ملف'} · {Math.round(d.file_size / 1024)} KB
{d.rejection_reason && (
سبب الرفض: {d.rejection_reason}
)}
{d.approval_status !== 'approved' && ( )} {d.approval_status !== 'rejected' && ( )}
); })}
)}
{previewDoc && ( setPreviewDoc(null)} /> )}
); } function AdminDocPreviewModal({ doc, onClose }) { const base = window.api?.baseUrl || 'http://localhost:8000'; const fileUrl = `${base}${doc.file_url}`; const isImage = (doc.mime_type || '').startsWith('image/'); return (
{doc.mime_type || 'ملف'} · {Math.round(doc.file_size / 1024)} KB
{isImage ? ( {doc.title} ) : (