"use client"; import React, { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Settings2, User, Building2, ShieldCheck, Bell, Palette, KeyRound, Save, Plus, Loader2, FolderKanban, CheckCircle2, XCircle, Calendar, Pencil, Trash2, AlertTriangle, } from "lucide-react"; import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/Card"; import { Button } from "@/components/ui/Button"; import { Badge } from "@/components/ui/Badge"; import { EmptyState } from "@/components/ui/EmptyState"; import { orgs, projects as projectsApi } from "@/lib/api"; import type { Organization, Project } from "@/lib/types"; import { cn, fmtDate, shortId } from "@/lib/utils"; const SECTIONS = [ { key: "profile", label: "Profile", icon: User }, { key: "org", label: "Organization", icon: Building2 }, { key: "security", label: "Security", icon: ShieldCheck }, { key: "ai", label: "AI Policy", icon: KeyRound }, { key: "notifs", label: "Notifications", icon: Bell }, { key: "theme", label: "Appearance", icon: Palette }, ] as const; type SecKey = (typeof SECTIONS)[number]["key"]; const PLAN_TONES: Record = { Free: "neutral", Team: "blue", Business: "brand", Enterprise: "violet", }; const STATUS_TONES: Record = { true: "green", false: "red", active: "green", inactive: "red", }; export default function SettingsPage() { const queryClient = useQueryClient(); const [sec, setSec] = useState("org"); const [name, setName] = useState("Mario Rossi"); const [email, setEmail] = useState("mario.rossi@azurian.example"); const [role, setRole] = useState("Owner"); const [mfa, setMfa] = useState(true); const [ceiling, setCeiling] = useState("50"); /* ---------------- Organizations ---------------- */ const orgsQ = useQuery({ queryKey: ["orgs", "list"], queryFn: () => orgs.list({ limit: 200 }), retry: false, refetchOnWindowFocus: false, }); const [showNewOrg, setShowNewOrg] = useState(false); const [newOrgName, setNewOrgName] = useState(""); const [newOrgSlug, setNewOrgSlug] = useState(""); const [newOrgPlan, setNewOrgPlan] = useState<"Free" | "Team" | "Business" | "Enterprise">("Team"); const [newOrgActive, setNewOrgActive] = useState(true); const createOrgM = useMutation({ mutationFn: async () => { const slugRgx = /^[a-z0-9][a-z0-9-]{1,58}[a-z0-9]$/; if (!newOrgName.trim()) throw new Error("Organization name required"); if (!slugRgx.test(newOrgSlug.trim())) throw new Error("Slug must be 3-60 chars: lowercase letters, digits, dashes (first+last alphanumeric)"); return orgs.create({ name: newOrgName.trim(), slug: newOrgSlug.trim().toLowerCase(), plan: newOrgPlan, is_active: newOrgActive, }); }, onSuccess: () => { setShowNewOrg(false); setNewOrgName(""); setNewOrgSlug(""); setNewOrgPlan("Team"); setNewOrgActive(true); queryClient.invalidateQueries({ queryKey: ["orgs", "list"] }).catch(() => undefined); }, }); const slugHint = useMemo(() => { if (!newOrgSlug) return null; const ok = /^[a-z0-9][a-z0-9-]{1,58}[a-z0-9]$/.test(newOrgSlug); return ok ? "Valid slug format" : "⚠ Must be 3-60 a-z 0-9 -, first+last alphanumeric"; }, [newOrgSlug]); /* ---------------- Projects ---------------- */ const projectsQ = useQuery({ queryKey: ["projects", "list"], queryFn: () => projectsApi.list({ limit: 500 }), retry: false, refetchOnWindowFocus: false, }); const [showNewProj, setShowNewProj] = useState(false); const [newProjName, setNewProjName] = useState(""); const [newProjKey, setNewProjKey] = useState(""); const [newProjDesc, setNewProjDesc] = useState(""); const [newProjBranch, setNewProjBranch] = useState("main"); const [newProjActive, setNewProjActive] = useState(true); const createProjM = useMutation({ mutationFn: async () => { const keyRgx = /^[A-Z][A-Z0-9]{1,19}$/; if (!newProjName.trim()) throw new Error("Project name required"); if (!keyRgx.test(newProjKey.trim())) throw new Error("Project key must be 2-20 uppercase letters/digits, starting with a letter (e.g. ASF, PROJ2)"); return projectsApi.create({ name: newProjName.trim(), key: newProjKey.trim().toUpperCase(), description: newProjDesc.trim() || undefined, default_branch: newProjBranch.trim() || undefined, is_active: newProjActive, }); }, onSuccess: () => { setShowNewProj(false); setNewProjName(""); setNewProjKey(""); setNewProjDesc(""); setNewProjBranch("main"); setNewProjActive(true); queryClient.invalidateQueries({ queryKey: ["projects", "list"] }).catch(() => undefined); }, }); const keyHint = useMemo(() => { if (!newProjKey) return null; const ok = /^[A-Z][A-Z0-9]{1,19}$/.test(newProjKey); return ok ? "Valid project key format" : "⚠ Must be 2-20 A-Z 0-9 starting with letter (e.g. ASF)"; }, [newProjKey]); /* ---------------- Edit Organization ---------------- */ const [showEditOrg, setShowEditOrg] = useState(false); const [editOrg, setEditOrg] = useState(null); const [eoName, setEoName] = useState(""); const [eoSlug, setEoSlug] = useState(""); const [eoPlan, setEoPlan] = useState<"Free" | "Team" | "Business" | "Enterprise">("Team"); const [eoActive, setEoActive] = useState(true); const openEditOrg = (o: Organization) => { setEditOrg(o); setEoName(o.name ?? ""); setEoSlug(o.slug ?? ""); setEoPlan((o.plan as typeof eoPlan) ?? "Team"); setEoActive(Boolean(o.is_active ?? true)); setShowEditOrg(true); }; const editOrgM = useMutation({ mutationFn: async () => { if (!editOrg) throw new Error("no organization"); const slugRgx = /^[a-z0-9][a-z0-9-]{1,58}[a-z0-9]$/; if (!eoName.trim()) throw new Error("Organization name required"); if (!slugRgx.test(eoSlug.trim())) throw new Error("Slug must be 3-60 chars: lowercase letters, digits, dashes (first+last alphanumeric)"); return orgs.update(String(editOrg.id), { name: eoName.trim(), slug: eoSlug.trim().toLowerCase(), plan: eoPlan, is_active: eoActive, }); }, onSuccess: () => { setShowEditOrg(false); setEditOrg(null); queryClient.invalidateQueries({ queryKey: ["orgs", "list"] }).catch(() => undefined); }, }); const eoSlugHint = useMemo(() => { if (!eoSlug) return null; const ok = /^[a-z0-9][a-z0-9-]{1,58}[a-z0-9]$/.test(eoSlug); return ok ? "Valid slug format" : "⚠ Must be 3-60 a-z 0-9 -, first+last alphanumeric"; }, [eoSlug]); /* ---------------- Delete Organization ---------------- */ const [delOrg, setDelOrg] = useState(null); const deleteOrgM = useMutation({ mutationFn: async (id: string) => orgs.remove(id), onSuccess: () => { setDelOrg(null); queryClient.invalidateQueries({ queryKey: ["orgs", "list"] }).catch(() => undefined); }, }); /* ---------------- Edit Project ---------------- */ const [showEditProj, setShowEditProj] = useState(false); const [editProj, setEditProj] = useState(null); const [epName, setEpName] = useState(""); const [epKey, setEpKey] = useState(""); const [epDesc, setEpDesc] = useState(""); const [epBranch, setEpBranch] = useState("main"); const [epActive, setEpActive] = useState(true); const openEditProj = (p: Project) => { setEditProj(p); setEpName(p.name ?? ""); setEpKey(p.key ?? ""); setEpDesc(p.description ?? ""); setEpBranch(p.default_branch ?? "main"); setEpActive(Boolean(p.is_active ?? true)); setShowEditProj(true); }; const editProjM = useMutation({ mutationFn: async () => { if (!editProj) throw new Error("no project"); const keyRgx = /^[A-Z][A-Z0-9]{1,19}$/; if (!epName.trim()) throw new Error("Project name required"); if (!keyRgx.test(epKey.trim())) throw new Error("Project key must be 2-20 uppercase letters/digits, starting with a letter (e.g. ASF, PROJ2)"); return projectsApi.update(String(editProj.id), { name: epName.trim(), key: epKey.trim().toUpperCase(), description: epDesc.trim() || undefined, default_branch: epBranch.trim() || undefined, is_active: epActive, }); }, onSuccess: () => { setShowEditProj(false); setEditProj(null); queryClient.invalidateQueries({ queryKey: ["projects", "list"] }).catch(() => undefined); }, }); const epKeyHint = useMemo(() => { if (!epKey) return null; const ok = /^[A-Z][A-Z0-9]{1,19}$/.test(epKey); return ok ? "Valid project key format" : "⚠ Must be 2-20 A-Z 0-9 starting with letter (e.g. ASF)"; }, [epKey]); /* ---------------- Delete Project ---------------- */ const [delProj, setDelProj] = useState(null); const deleteProjM = useMutation({ mutationFn: async (id: string) => projectsApi.remove(id), onSuccess: () => { setDelProj(null); queryClient.invalidateQueries({ queryKey: ["projects", "list"] }).catch(() => undefined); }, }); return (

Settings

User profile, organization, security, AI policy, notifications and appearance.

    {SECTIONS.map(({ key, label, icon: Icon }) => (
  • ))}
{sec === "profile" && <>User Profile You} {sec === "org" && "Organization & Projects"} {sec === "security" && "Security"} {sec === "ai" && "AI Policy"} {sec === "notifs" && "Notifications"} {sec === "theme" && "Appearance"} {sec === "profile" && (
setName(e.target.value)} /> setEmail(e.target.value)} />
)} {sec === "org" && (
{/* Organizations Card */}

Organizations

{orgsQ.data?.length ?? 0}
{orgsQ.isFetching && !orgsQ.data ? (
Loading organizations…
) : (orgsQ.data ?? []).length === 0 ? ( ) : (
{(orgsQ.data ?? []).map((o: Organization) => ( ))}
Name Slug Plan Status Created Actions
{o.name} {shortId(String(o.id))} {o.slug} {o.plan ?? "Free"} {o.is_active ?? true ? <>Active : <>Inactive} {o.created_at ? fmtDate(o.created_at) : "—"}
)}
{/* Projects Card */}

Projects

{projectsQ.data?.length ?? 0}
{projectsQ.isFetching && !projectsQ.data ? (
Loading projects…
) : (projectsQ.data ?? []).length === 0 ? ( ) : (
{(projectsQ.data ?? []).map((p: Project) => ( ))}
Key Name Default branch Status Updated Actions
{p.key} {p.name} {shortId(String(p.id))} {p.default_branch ?? "—"} {p.is_active ?? true ? <>Active : <>Archived} {p.updated_at ? fmtDate(p.updated_at) : "—"}
)}
)} {sec === "security" && (
{}} /> {}} />
)} {sec === "ai" && (
setCeiling(e.target.value)} /> {}} /> {}} />

AI Policy can be further refined per-project and per-team inside asf-ai.

)} {sec === "notifs" && (
{[ { l: "Job failures", h: "Email + in-app when any job fails." }, { l: "Security findings critical/high", h: "Instant via configured channel." }, { l: "Deployment completed", h: "Production deployments only." }, { l: "Weekly digest", h: "Health, coverage and cost snapshot." }, ].map((t) => ( {}} /> ))}
)} {sec === "theme" && (
{[ { k: "light", label: "Light", sw: "bg-white border-slate-300" }, { k: "system", label: "System", sw: "bg-gradient-to-r from-white to-slate-900 border-slate-400" }, { k: "dark", label: "Dark", sw: "bg-slate-900 border-slate-700" }, ].map((t) => ( ))}
{}} />
)}
{/* ---------- New Organization modal ---------- */} {showNewOrg && (
!createOrgM.isPending && setShowNewOrg(false)}>
e.stopPropagation()} >

New organization

{ e.preventDefault(); createOrgM.mutate(); }} > setNewOrgName(e.target.value)} /> Slug * (URL identifier)}> setNewOrgSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))} />
{slugHint ?? "3-60 chars, lowercase letters, digits, dashes. First and last must be alphanumeric."}
{createOrgM.isError && (
{createOrgM.error instanceof Error ? createOrgM.error.message : "Failed to create organization."}
)}
)} {/* ---------- New Project modal ---------- */} {showNewProj && (
!createProjM.isPending && setShowNewProj(false)}>
e.stopPropagation()} >

New project

{ e.preventDefault(); createProjM.mutate(); }} > setNewProjName(e.target.value)} /> Key * (short uppercase code)}> setNewProjKey(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ""))} />
{keyHint ?? "2-20 characters. Uppercase letters/digits only — starts with a letter."}
setNewProjBranch(e.target.value)} />