Files
verify/frontend/app/dashboard/settings/page.tsx
Mohamed Mathar Irfan ed6610d5d8 Initial project upload
2026-07-28 17:57:02 +05:30

545 lines
16 KiB
TypeScript

"use client";
import { Suspense, useEffect, useState } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import {
Building2, Bell, CreditCard, FileText, Save, CheckCircle2, Users as UsersIcon,
} from "lucide-react";
import { api } from "@/lib/api";
import { Package } from "lucide-react";
import UsersPanel from "@/components/UsersPanel";
import BrandsPanel from "@/components/BrandsPanel";
import countryList from "react-select-country-list";
import TimezoneSelect from "react-timezone-select";
import { T, detectLang } from "@/lib/i18n";
import { useLanguage } from "@/contexts/LanguageContext";
import { apiBase } from "@/lib/api";
import { Download } from "lucide-react";
const countries = countryList().getData();
export const LANGS = [
{ code: "en", label: "English" },
{ code: "hi", label: "हिन्दी" },
{ code: "kn", label: "ಕನ್ನಡ" },
{ code: "ta", label: "தமிழ்" },
{ code: "bn", label: "বাংলা" },
];
function SettingsInner() {
const sp = useSearchParams();
const initial = sp.get("section") ?? "org";
const [active, setActive] = useState(initial);
const [org, setOrg] = useState<any>(null);
const [form, setForm] = useState<any>({});
const [saved, setSaved] = useState(false);
const [sub, setSub] = useState<any>(null);
const [logs, setLogs] = useState<any[]>([]);
const [notif, setNotif] = useState({ email: true, system: true, compliance: true, security: true });
const { t, setLang } = useLanguage();
const SECTIONS = [
{ id: "org", label: t.organization, icon: Building2 },
{ id: "users", label: t.users, icon: UsersIcon },
{ id: "brands", label: t.brands, icon: Package },
{ id: "notifications", label: t.notifications, icon: Bell },
{ id: "subscription", label: t.subscription, icon: CreditCard },
{ id: "audit", label: t.audit, icon: FileText },
];
const [logoPreview, setLogoPreview] = useState("");
const [logoFileName, setLogoFileName] = useState("");
const [filters, setFilters] = useState({
user: "",
module: "",
from: "",
to: "",
});
function downloadAuditLogs() {
if (!logs.length) return;
const rows = logs.map((l) => ({
Time: l.at
? new Date(l.at).toLocaleString(form.language || "en", {
timeZone: form.timezone || "Asia/Kolkata",
})
: "",
User: l.user,
Role: l.role,
Action: l.action,
Module: l.module,
Details: l.details,
IP: l.ip ?? "",
}));
const headers = Object.keys(rows[0]);
const csv = [
headers.join(","),
...rows.map((r) =>
headers
.map((h) => `"${String((r as any)[h] ?? "").replace(/"/g, '""')}"`)
.join(",")
),
].join("\n");
const blob = new Blob([csv], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `audit_logs_${new Date()
.toISOString()
.slice(0, 10)}.csv`;
link.click();
URL.revokeObjectURL(url);
}
async function loadLogs() {
const params = new URLSearchParams();
if (filters.user) params.append("user", filters.user);
if (filters.module) params.append("module", filters.module);
if (filters.from) params.append("from_date", filters.from);
if (filters.to) params.append("to_date", filters.to);
const data = await api(`/audit-logs?${params.toString()}`);
setLogs(data);
}
useEffect(() => {
api("/organization").then((o) => {
setOrg(o);
setLogoPreview(o.logo_url || "");
localStorage.setItem("timezone", o.timezone);
setForm({ name: o.name, gstin: o.gstin ?? "", pan: o.pan ?? "",address: o.address ?? "", phone: o.phone ?? "",logo_url:o.logo_url ?? "",
website: o.website ?? "", timezone: o.timezone ?? "Asia/Kolkata", language: o.language ?? "en", contact_email: o.contact_email ?? "",country: o.country ?? "",});
}).catch(() => {});
api("/billing/subscription").then(setSub).catch(() => {});
loadLogs();
}, []);
async function uploadLogo(file: File) {
const fd = new FormData();
fd.append("file", file);
const token = localStorage.getItem("vp_token");
const res = await fetch(`${apiBase}/brands/upload-logo`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: fd,
});
const data = await res.json();
setLogoPreview(data.url);
setLogoFileName(file.name);
setForm({
...form,
logo_url: data.url,
});
}
async function saveOrg() {
await api("/organization", { method: "PUT", body: JSON.stringify(form) });
setLang(form.language);
localStorage.setItem("timezone", form.timezone);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}
useEffect(() => {
loadLogs();
}, [filters]);
return (
<div className="space-y-5">
<h2 className="text-xl font-bold">{t.settings}</h2>
<div className="grid md:grid-cols-[200px_1fr] gap-5">
<nav className="space-y-1">
{SECTIONS.map(({ id, label, icon: Icon }) => (
<button key={id} onClick={() => setActive(id)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium ${active === id ? "bg-brand-50 text-brand-700" : "text-slate-600 hover:bg-slate-50"}`}>
<Icon className="h-4 w-4" /> {label}
</button>
))}
</nav>
<div>
{active === "users" && <UsersPanel />}
{active === "brands" && <BrandsPanel />}
{active === "org" && (
<div className="card p-5 space-y-4 max-w-3xl">
<h3 className="font-bold">{t.organization}</h3>
{saved && <div className="text-sm text-emerald-600 flex items-center gap-1"><CheckCircle2 className="h-4 w-4" /> Saved</div>}
<div className="flex items-center gap-6 mb-6">
<div className="grid lg:grid-cols-[260px_1fr] gap-8">
{/* LEFT SIDE */}
<div className="border rounded-xl p-6 bg-slate-50 flex flex-col items-center">
<div className="w-40 h-40 rounded-xl border bg-white overflow-hidden flex items-center justify-center">
{logoPreview ? (
<img
src={logoPreview}
alt="Organization Logo"
className="w-full h-full object-contain"
/>
) : (
<span className="text-slate-400 text-sm">
No Logo
</span>
)}
</div>
<label className="mt-5 w-full">
<input
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
if (e.target.files?.[0]) {
uploadLogo(e.target.files[0]);
}
}}
/>
<div className="cursor-pointer text-center bg-brand-600 text-white rounded-lg py-2 font-medium hover:bg-brand-700">
Upload Logo
</div>
</label>
{logoFileName && (
<p className="mt-3 text-xs text-slate-500 break-all text-center">
{logoFileName}
</p>
)}
</div>
{/* RIGHT SIDE */}
<div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Field
label="Organization Name"
value={form.name}
onChange={(v:string)=>setForm({...form,name:v})}
/>
<Field
label="Website"
value={form.website}
onChange={(v:string)=>setForm({...form,website:v})}
/>
<Field
label="GSTIN"
value={form.gstin}
onChange={(v:string)=>setForm({...form,gstin:v})}
/>
<Field
label="PAN"
value={form.pan}
onChange={(v:string)=>setForm({...form,pan:v})}
/>
<Field
label="Phone"
value={form.phone}
onChange={(v:string)=>setForm({...form,phone:v})}
/>
<Field
label="Contact Email"
value={form.contact_email}
onChange={(v:string)=>setForm({...form,contact_email:v})}
/>
<label>
<span className="text-sm font-medium">Country</span>
<select
value={form.country}
onChange={(e)=>setForm({...form,country:e.target.value})}
className="mt-1 w-full border rounded-lg px-3 py-2"
>
{countries.map((c)=>(
<option key={c.value} value={c.label}>
{c.label}
</option>
))}
</select>
</label>
<label>
<span className="text-sm font-medium">Language</span>
<select
value={form.language}
onChange={(e)=>setForm({...form,language:e.target.value})}
className="mt-1 w-full border rounded-lg px-3 py-2"
>
{LANGS.map((l)=>(
<option key={l.code} value={l.code}>
{l.label}
</option>
))}
</select>
</label>
<div className="md:col-span-2">
<label>
<span className="text-sm font-medium">Timezone</span>
<TimezoneSelect
value={form.timezone}
onChange={(tz:any)=>
setForm({
...form,
timezone:tz.value,
})
}
/>
</label>
</div>
<div className="md:col-span-2">
<Field
label="Address"
value={form.address}
onChange={(v:string)=>setForm({...form,address:v})}
/>
</div>
</div>
</div>
</div>
</div>
<button onClick={saveOrg} className="flex items-center gap-2 bg-brand-600 text-white px-5 py-2 rounded-lg text-sm font-semibold">
<Save className="h-4 w-4" /> {t.save}
</button>
</div>
)}
{active === "notifications" && (
<div className="card p-5 space-y-4 max-w-xl">
<h3 className="font-bold">{t.notifications}</h3>
{[
["email", "Email notifications"],
["system", "System alerts"],
["compliance", "Compliance expiry alerts"],
["security", "Security alerts"],
].map(([key, label]) => (
<div key={key} className="flex items-center justify-between py-2 border-b border-slate-100">
<span className="text-sm text-slate-700">{label}</span>
<button onClick={() => setNotif({ ...notif, [key]: !(notif as any)[key] })}
className={`w-11 h-6 rounded-full relative transition ${(notif as any)[key] ? "bg-emerald-500" : "bg-slate-300"}`}>
<span className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition ${(notif as any)[key] ? "left-5" : "left-0.5"}`} />
</button>
</div>
))}
<p className="text-xs text-slate-400">Preferences are illustrative in this build.</p>
</div>
)}
{active === "subscription" && (
<div className="card p-5 max-w-xl">
<h3 className="font-bold mb-3">{t.subscription}</h3>
<div className="space-y-2 text-sm">
<Row label="Current plan" value={sub?.plan?.name ?? "—"} />
<Row label="Status" value={sub?.status ?? "—"} />
<Row label="Days remaining" value={sub?.days_remaining ?? "—"} />
<Row label="Auto-renewal" value={sub?.auto_renew ? "On" : "Off"} />
</div>
<Link href="/dashboard/billing" className="inline-block mt-4 bg-brand-600 text-white px-5 py-2 rounded-lg text-sm font-semibold">
Manage billing
</Link>
</div>
)}
{active === "audit" && (
<div className="card overflow-x-auto">
<div className="flex justify-between items-center p-5 pb-3">
<h3 className="font-bold">Recent Activity</h3>
<button
onClick={downloadAuditLogs}
className="flex items-center gap-2 bg-brand-600 text-white px-4 py-2 rounded-lg hover:bg-brand-700"
>
<Download className="h-4 w-4" />
Download CSV
</button>
</div>
<div className="flex flex-wrap gap-3 p-5 border-b">
<input
type="date"
value={filters.from}
onChange={(e) =>
setFilters({ ...filters, from: e.target.value })
}
className="border rounded-lg px-3 py-2"
/>
<input
type="date"
value={filters.to}
onChange={(e) =>
setFilters({ ...filters, to: e.target.value })
}
className="border rounded-lg px-3 py-2"
/>
<select
value={filters.module}
onChange={(e) =>
setFilters({ ...filters, module: e.target.value })
}
className="border rounded-lg px-3 py-2"
>
<option value="">All Modules</option>
<option value="organization">Organization</option>
<option value="products">Product</option>
<option value="branding">Branding</option>
<option value="users">Users</option>
<option value="compliance">Compliance</option>
</select>
<input
placeholder="Search User"
value={filters.user}
onChange={(e) =>
setFilters({ ...filters, user: e.target.value })
}
className="border rounded-lg px-3 py-2"
/>
<button
onClick={() =>
setFilters({
user: "",
module: "",
from: "",
to: "",
})
}
className="px-4 py-2 rounded-lg bg-slate-200"
>
Clear
</button>
</div>
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-500 text-left">
<tr>
<th className="px-4 py-3 font-medium">Time</th>
<th className="px-4 py-3 font-medium">User</th>
<th className="px-4 py-3 font-medium">Action</th>
<th className="px-4 py-3 font-medium">Details</th>
<th className="px-4 py-3 font-medium">Module</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{logs.map((l, i) => (
<tr key={i}>
<td className="px-4 py-3 text-slate-500">
{l.at
? new Date(l.at).toLocaleString(form.language || "en", {
timeZone: form.timezone || "Asia/Kolkata",
dateStyle: "medium",
timeStyle: "short",
})
: ""}
</td>
<td className="px-4 py-3">
<div className="font-medium">
{l.user ?? "—"}
</div>
<div className="text-xs text-slate-500 capitalize">
{l.role ?? "User"}
</div>
</td>
<td className="px-4 py-3 capitalize">{l.action?.replace(".", " ")}</td>
<td className="px-4 py-3 text-slate-500">{l.details}</td>
<td className="px-4 py-3">
<span
className={`px-2 py-1 rounded-full text-xs font-medium
${
l.module === "Product"
? "bg-blue-100 text-blue-700"
: l.module === "Organization"
? "bg-green-100 text-green-700"
: l.module === "Branding"
? "bg-purple-100 text-purple-700"
: l.module === "Notification"
? "bg-yellow-100 text-yellow-700"
: "bg-slate-100 text-slate-700"
}`}
>
{l.module}
</span>
</td>
</tr>
))}
{logs.length === 0 && <tr><td colSpan={4} className="px-4 py-10 text-center text-slate-400">No activity yet.</td></tr>}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
}
export default function Settings() {
return (
<Suspense fallback={<div className="text-slate-400">Loading</div>}>
<SettingsInner />
</Suspense>
);
}
function Field({ label, value, onChange }: any) {
return (
<label className="block">
<span className="text-sm font-medium">{label}</span>
<input value={value ?? ""} onChange={(e) => onChange(e.target.value)}
className="mt-1 w-full px-3 py-2 rounded-lg border border-slate-200 text-sm" />
</label>
);
}
function Row({ label, value }: any) {
return (
<div className="flex justify-between border-b border-slate-100 py-2">
<span className="text-slate-500">{label}</span>
<span className="font-medium text-slate-800 capitalize">{value}</span>
</div>
);
}