"use client"; import * as React from "react"; import type { AuditEvent, Invite, Member, Role } from "@agy/contracts"; import { Badge, Button, Card, CardHint, CardTitle, ConfirmDialog, EmptyState, ErrorState, Field, LoadingState, Select, Timeline, } from "@agy/ui"; import { ApiError, api } from "@/lib/api"; import { CAN_MANAGE_MEMBERS, CAN_VIEW_AUDIT, roleHasCapability, } from "@/lib/permissions"; import { useSession } from "@/lib/session"; const ROLE_LABELS: Record = { owner: "مالک", admin: "مدیر", manager: "سرپرست", member: "عضو", viewer: "بیننده", }; const AUDIT_LABELS: Record = { "auth.login": "ورود به حساب", "auth.logout": "خروج از حساب", "auth.signup": "ثبت‌نام", "organization.create": "ساخت سازمان", "organization.delete": "حذف سازمان", "member.invite": "دعوت عضو", "member.join": "پیوستن عضو", "member.role_change": "تغییر نقش", "member.remove": "حذف عضو", "employee.create": "ساخت کارمند", "employee.update": "ویرایش کارمند", "repo.create": "ساخت مخزن", "repo.delete": "حذف مخزن", "domain.create": "ساخت دامنه", "domain.delete": "حذف دامنه", "sandbox.command": "اجرای دستور سندباکس", "sandbox.destroy": "نابودی سندباکس", "approval.decision": "تصمیم تأیید", "billing.credit": "تغییر اعتبار", "billing.payment": "پرداخت", "provider.change": "تغییر ارائه‌دهنده", "model.change": "تغییر مدل", "routing.change": "تغییر مسیریابی", "project.create": "ساخت پروژه", "project.delete": "حذف پروژه", "task.create": "ساخت کار", "run.cancel": "لغو اجرا", }; type Tab = "members" | "audit"; /** Matches ``INVITE_TTL_DAYS`` in the API so the hint cannot drift. */ const INVITE_TTL_DAYS = 14; /** * The API returns an absolute invite URL. Older invites (created before the API * started building one) carry a bare path, so fall back to the current origin — * that keeps the anchor clickable either way. */ function absoluteInviteUrl(url: string): string { if (/^https?:\/\//i.test(url)) return url; if (typeof window === "undefined") return url; return `${window.location.origin}${url.startsWith("/") ? url : `/${url}`}`; } export default function SettingsPage() { const { activeOrganization } = useSession(); const orgId = activeOrganization?.id ?? null; const [tab, setTab] = React.useState("members"); const [members, setMembers] = React.useState([]); const [audit, setAudit] = React.useState([]); const [loading, setLoading] = React.useState(true); const [auditLoading, setAuditLoading] = React.useState(false); const [error, setError] = React.useState(null); const [email, setEmail] = React.useState(""); const [inviteRole, setInviteRole] = React.useState("member"); const [inviting, setInviting] = React.useState(false); const [inviteError, setInviteError] = React.useState(null); const [lastInvite, setLastInvite] = React.useState(null); const [copied, setCopied] = React.useState(false); const [busyMemberId, setBusyMemberId] = React.useState(null); const [pendingRemove, setPendingRemove] = React.useState(null); const load = React.useCallback(async () => { if (!orgId) { setLoading(false); return; } setLoading(true); setError(null); try { setMembers(await api.listMembers(orgId)); } catch (err) { setError(err instanceof ApiError ? err.message : "دریافت اعضا ناموفق بود."); } finally { setLoading(false); } }, [orgId]); const loadAudit = React.useCallback(async () => { if (!orgId) return; setAuditLoading(true); try { setAudit(await api.listAudit(orgId)); } catch (err) { setError(err instanceof ApiError ? err.message : "دریافت رویدادهای حسابرسی ناموفق بود."); } finally { setAuditLoading(false); } }, [orgId]); React.useEffect(() => { void load(); }, [load]); React.useEffect(() => { if (tab === "audit") void loadAudit(); }, [tab, loadAudit]); const onInvite = async (event: React.FormEvent) => { event.preventDefault(); if (!orgId) return; setInviteError(null); setLastInvite(null); setCopied(false); setInviting(true); try { const invite = await api.createInvite(orgId, { email: email.trim(), role: inviteRole }); setLastInvite(invite); setEmail(""); } catch (err) { setInviteError(err instanceof ApiError ? err.message : "ارسال دعوت ناموفق بود."); } finally { setInviting(false); } }; const copyInviteLink = async (url: string) => { try { await navigator.clipboard.writeText(url); setCopied(true); window.setTimeout(() => setCopied(false), 2000); } catch { // Clipboard can be blocked (insecure context, denied permission). The // link is rendered as a real anchor above, so the user can still copy it // by hand — no need to surface an error for this. setCopied(false); } }; const onChangeRole = async (member: Member, role: Role) => { if (!orgId) return; setBusyMemberId(member.id); setError(null); try { const updated = await api.updateMember(orgId, member.id, { role }); setMembers((current) => current.map((m) => (m.id === updated.id ? updated : m))); } catch (err) { setError(err instanceof ApiError ? err.message : "تغییر نقش ناموفق بود."); } finally { setBusyMemberId(null); } }; const onConfirmRemove = async () => { if (!orgId || !pendingRemove) return; setBusyMemberId(pendingRemove.id); try { await api.removeMember(orgId, pendingRemove.id); setMembers((current) => current.filter((m) => m.id !== pendingRemove.id)); setPendingRemove(null); } catch (err) { setError(err instanceof ApiError ? err.message : "حذف عضو ناموفق بود."); setPendingRemove(null); } finally { setBusyMemberId(null); } }; if (!activeOrganization) { return ( ); } const canManageMembers = roleHasCapability(activeOrganization.role, CAN_MANAGE_MEMBERS); const canViewAudit = roleHasCapability(activeOrganization.role, CAN_VIEW_AUDIT); return (

تنظیمات سازمان

{activeOrganization.name}

{( [ ["members", "اعضا و دعوت"], ["audit", "رویدادهای حسابرسی"], ] as const ).map(([value, label]) => ( ))}
{error && } {tab === "members" ? ( <> اعضای سازمان نقش‌ها به ترتیب قدرت: مالک، مدیر، سرپرست، عضو، بیننده. آخرین مالک قابل حذف نیست.
{loading ? ( ) : members.length === 0 ? ( ) : (
    {members.map((member) => (
  • {member.name || member.email} {member.email}
    {member.status} {canManageMembers ? ( <>
    setInviteRole(e.target.value as Role)} options={Object.entries(ROLE_LABELS).map(([value, label]) => ({ value, label, }))} />
    {inviteError && (
    )} {lastInvite && (

    دعوت برای {lastInvite.email} ساخته شد.

    {absoluteInviteUrl(lastInvite.invite_url)}
    لینک یک‌بارمصرف است و تا {INVITE_TTL_DAYS} روز اعتبار دارد.
    )} )} ) : ( رویدادهای حسابرسی این فهرست فقط-افزودنی است. هر عملیات حساس — ورود، تغییر نقش، ساخت کارمند، تغییر اعتبار و تصمیم تأیید — اینجا ثبت می‌شود.
    {!canViewAudit ? (

    برای دیدن رویدادهای حسابرسی به سطح دسترسی مدیر یا بالاتر نیاز دارید.

    ) : auditLoading ? ( ) : audit.length === 0 ? (

    رویدادی ثبت نشده است.

    ) : ( ({ id: event.id, title: AUDIT_LABELS[event.action] ?? event.action, meta: `${event.target_type || "—"} · ${new Date(event.created_at).toLocaleString("fa-IR")}`, tone: event.action.includes("delete") || event.action.includes("remove") ? ("danger" as const) : event.action.includes("create") || event.action.includes("payment") ? ("success" as const) : ("info" as const), }))} /> )}
    )} setPendingRemove(null)} />
    ); }