Initial project upload

This commit is contained in:
Mohamed Mathar Irfan
2026-07-28 17:57:02 +05:30
commit ed6610d5d8
23919 changed files with 3003316 additions and 0 deletions

View File

@@ -0,0 +1,952 @@
"use client";
import { useEffect,useRef, useState } from "react";
import { UserPlus, X, Users as UsersIcon } from "lucide-react";
import { api } from "@/lib/api";
import { useLanguage } from "@/contexts/LanguageContext";
const ROLES = [
{ role: "admin", label: "Admin", desc: "Full access: products, QRs, batches, compliance, users, billing." },
{ role: "finance", label: "Finance", desc: "Billing, invoices, payment methods, subscription only." },
{ role: "user", label: "User", desc: "Create products, generate QRs, manage batches, view analytics." },
];
const ROLE_BADGE: Record<string, string> = {
admin: "bg-red-100 text-red-700 border border-red-200",
finance: "bg-violet-100 text-violet-700 border border-violet-200",
user: "bg-blue-100 text-blue-700 border border-blue-200",
manager: "bg-green-100 text-green-700 border border-green-200",
quality: "bg-orange-100 text-orange-700 border border-orange-200",
production: "bg-cyan-100 text-cyan-700 border border-cyan-200",
};
const STATUS_BADGE: Record<string, string> = {
active: "bg-emerald-50 text-emerald-700",
pending: "bg-amber-50 text-amber-700",
disabled: "bg-rose-50 text-rose-700",
};
export default function UsersPanel() {
const [tab, setTab] = useState<"users" | "roles">("users");
const [data, setData] = useState<any>(null);
const [org, setOrg] = useState<any>(null);
const [open, setOpen] = useState(false);
const [invite, setInvite] = useState({ email: "", role: "user", department: "",password: "",name: "", });
const [err, setErr] = useState("");
const { t } = useLanguage();
const [openMenu, setOpenMenu] = useState<string | null>(null);
const [editingUser, setEditingUser] = useState<any>(null);
const menuRef = useRef<HTMLDivElement | null>(null);
const [search, setSearch] = useState("");
const [filters, setFilters] = useState({
role: "",
status: "",
department: "",
joinedFrom: "",
joinedTo: "",
});
const [detailTab, setDetailTab] = useState<
"profile" | "permissions" | "activity"
>("profile");
const [activityLogs, setActivityLogs] = useState<any[]>([]);
const [selectedUser, setSelectedUser] = useState<any>(null);
const filteredUsers = (data?.items ?? []).filter((m: any) => {
const keyword = search.toLowerCase();
const matchesSearch =
!search ||
m.name?.toLowerCase().includes(keyword) ||
m.email?.toLowerCase().includes(keyword);
const matchesRole =
!filters.role || m.role === filters.role;
const matchesStatus =
!filters.status || m.status === filters.status;
const matchesDepartment =
!filters.department ||
m.department === filters.department;
const joined = m.joined_at
? new Date(m.joined_at)
: null;
const from =
!filters.joinedFrom ||
(joined &&
joined >= new Date(filters.joinedFrom));
const to =
!filters.joinedTo ||
(joined &&
joined <= new Date(filters.joinedTo + "T23:59:59"));
return (
matchesSearch &&
matchesRole &&
matchesStatus &&
matchesDepartment &&
from &&
to
);
});
const [showFilters, setShowFilters] = useState(false);
const [selected, setSelected] = useState<string[]>([]);
function toggleSelectAll(checked: boolean) {
if (checked) {
setSelected((data?.items ?? []).map((m: any) => m.member_id));
} else {
setSelected([]);
}
}
const toggleSelect = (memberId: string, checked: boolean) => {
if (checked) {
setSelected((prev) =>
prev.includes(memberId) ? prev : [...prev, memberId]
);
} else {
setSelected((prev) => prev.filter((id) => id !== memberId));
}
};async function updateUser() {
try {
await api(`/users/${editingUser.member_id}`, {
method: "PUT",
body: JSON.stringify({
role: invite.role,
department: invite.department,
}),
});
setOpen(false);
setEditingUser(null);
setInvite({
email: "",
role: "user",
department: "",
name: "",password: "",
});
load();
} catch (e) {
console.error(e);
}
}
function load() {
api("/users").then(setData).catch(() => setData({ items: [], kpis: {} }));
api("/organization").then(setOrg).catch(() => {});
}
useEffect(() => { load(); }, []);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
menuRef.current &&
!menuRef.current.contains(event.target as Node)
) {
setOpenMenu(null);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
async function sendInvite() {
setErr("");
try {
await api("/users/invite", { method: "POST", body: JSON.stringify(invite) });
setOpen(false);
setInvite({ email: "", role: "user", department: "" ,password: "",name: "",});
load();
} catch (e: any) {
setErr(typeof e.detail === "object" ? e.detail.message : e.detail || "Invite failed");
}
}
async function setRole(member_id: string, role: string) {
await api(`/users/${member_id}`, { method: "PUT", body: JSON.stringify({ role }) });
load();
}
async function setStatus(member_id: string, status: string) {
await api(`/users/${member_id}`, { method: "PUT", body: JSON.stringify({ status }) });
load();
}
const k = data?.kpis ?? {};
const roleSummary = data?.role_summary ?? {};
function Permission({ text }: { text: string }) {
return (
<div className="flex items-center gap-2 text-sm">
<div className="h-2 w-2 rounded-full bg-emerald-500"></div>
{text}
</div>
);
}
return (
<div className="space-y-5">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold">{t.users_management}</h2>
<div className="flex items-center gap-3">
<button
onClick={() => setOpen(true)}
className="flex items-center gap-2 bg-brand-600 text-white px-4 py-2 rounded-lg text-sm font-semibold"
>
<UserPlus className="h-4 w-4" />
{t.invite_user}
</button>
<button
onClick={async () => {
const token = localStorage.getItem("vp_token");
const res = await fetch(
"http://localhost:8000/users/export",
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "users.csv";
a.click();
window.URL.revokeObjectURL(url);
}}
className="flex items-center gap-2 border border-brand-600 text-brand-600 hover:bg-brand-50 px-4 py-2 rounded-lg text-sm font-semibold"
>
📥 Export Users
</button>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<Kpi label="Total Users" value={`${k.total ?? 0}${k.user_limit ? ` / ${k.user_limit}` : ""}`} />
<Kpi label="Active" value={k.active ?? 0} />
<Kpi label="Pending Invites" value={k.pending ?? 0} />
<Kpi label="Disabled" value={k.disabled ?? 0} />
</div>
<div className="flex gap-1 border-b border-slate-200">
{(["users", "roles"] as const).map((t) => (
<button key={t} onClick={() => setTab(t)}
className={`px-4 py-2 text-sm font-medium capitalize border-b-2 -mb-px ${tab === t ? "border-brand-600 text-brand-700" : "border-transparent text-slate-500"}`}>
{t === "roles" ? "Roles & Permissions" : "Users"}
</button>
))}
</div>
{tab === "users" ? (
<div className="card overflow-visible">
<div className="mb-4 flex flex-wrap items-center gap-3">
<input
type="text"
placeholder="Search user or email..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-72 rounded-lg border border-slate-300 px-3 py-2 text-sm"
/>
<select
value={filters.role}
onChange={(e)=>
setFilters({...filters,role:e.target.value})
}
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
>
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="finance">Finance</option>
<option value="user">User</option>
</select>
<select
value={filters.status}
onChange={(e)=>
setFilters({...filters,status:e.target.value})
}
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
>
<option value="">All Status</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="disabled">Disabled</option>
</select>
<select
value={filters.department}
onChange={(e)=>
setFilters({...filters,department:e.target.value})
}
className="rounded-lg border border-slate-300 px-3 py-2 text-sm"
>
<option value="">All Departments</option>
<option>Administration</option>
<option>Finance</option>
<option>Production</option>
<option>Quality</option>
<option>Warehouse</option>
<option>Sales</option>
<option>HR</option>
<option>Marketing</option>
</select>
<button
onClick={()=>{
setSearch("");
setFilters({
role:"",
status:"",
department:"",
joinedFrom:"",
joinedTo:"",
});
}}
className="rounded-lg border border-slate-300 px-4 py-2 text-sm"
>
Reset
</button>
</div>
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-500 text-left">
<tr>
<th className="w-12 px-4 py-3 text-center">
<div className="flex justify-center">
<input
type="checkbox"
checked={
data?.items?.length > 0 &&
selected.length === data.items.length
}
onChange={(e) => toggleSelectAll(e.target.checked)}
className="h-4 w-4 rounded border-slate-300"
/>
</div>
</th> <th className="px-4 py-3 font-medium">{t.user}</th>
<th className="px-4 py-3 font-medium">{t.role}</th>
<th className="px-4 py-3 font-medium">{t.department}</th>
<th className="px-4 py-3 font-medium">{t.status}</th>
<th className="px-4 py-3 font-medium">{t.last_login}</th>
<th className="px-4 py-3 font-medium">Joined On</th>
<th className="px-4 py-3 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredUsers.map((m:any,index:number)=>(
<tr
key={m.member_id}
onClick={async () => {
setSelectedUser(m);
setDetailTab("profile");
try {
const logs = await api(`/users/${m.member_id}/activity`);
setActivityLogs(logs.items ?? []);
} catch {
setActivityLogs([]);
}
}}
className="hover:bg-slate-50 cursor-pointer"
>
<td className="w-12 px-4 py-3 text-center">
<div className="flex justify-center">
<input
type="checkbox"
checked={selected.includes(m.member_id)}
onChange={(e) =>
toggleSelect(m.member_id, e.target.checked)
}
className="h-4 w-4 rounded border-slate-300"
/>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-full bg-brand-500 text-white grid place-items-center text-sm font-semibold">
{(m.name?.[0] ?? "U").toUpperCase()}
</div>
<div>
<div className="font-medium text-slate-800">{m.name}</div>
<div className="text-xs text-slate-400">{m.email}</div>
</div>
</div>
</td>
<td className="px-4 py-3">
<span
className={`px-3 py-1 rounded-full text-xs font-semibold capitalize ${ROLE_BADGE[m.role]}`}
>
{m.role}
</span>
</td>
<td className="px-4 py-3 text-slate-500">{m.department ?? "—"}</td>
<td className="px-4 py-3">
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full capitalize ${STATUS_BADGE[m.status]}`}>{m.status}</span>
</td>
<td className="px-4 py-3 text-slate-500">
{m.last_login
? new Date(m.last_login).toLocaleString()
: "—"}
</td>
<td className="px-4 py-3 text-slate-500">
{m.joined_at
? new Date(m.joined_at).toLocaleString()
: "—"}
</td>
<td
className="px-4 py-3 text-right"
onClick={(e)=>e.stopPropagation()}
>
<div className="relative inline-block" ref={menuRef}>
<button
onClick={() =>
setOpenMenu(openMenu === m.member_id ? null : m.member_id)
}
className="rounded-md p-2 hover:bg-slate-100"
>
</button>
{openMenu === m.member_id && (
<div className="absolute bottom-full right-0 mb-2 w-44 rounded-lg border border-slate-200 bg-white shadow-xl z-50">
<button
onClick={() => {
setInvite({
email: m.email,
role: m.role,
department: m.department ?? "",
name: m.name ?? "",
password: m.password ?? "",
});
setEditingUser(m);
setOpen(true);
setOpenMenu(null);
}}
className="w-full px-4 py-2 text-left text-sm hover:bg-slate-50"
>
Edit User
</button>
<button
onClick={() => {
setStatus(
m.member_id,
m.status === "active" ? "disabled" : "active"
);
setOpenMenu(null);
}}
className="w-full px-4 py-2 text-left text-sm hover:bg-slate-50"
>
{m.status === "active"
? "🚫 Disable User"
: "✅ Enable User"}
</button>
</div>
)}
</div>
</td>
</tr>
))}
{data?.items?.length === 0 && (
<tr><td colSpan={6} className="px-4 py-12 text-center text-slate-400">
<UsersIcon className="h-8 w-8 mx-auto mb-2 opacity-50" /> No team members yet.
</td></tr>
)}
</tbody>
</table>
</div>
) : (
<>
<h3 className="text-lg font-semibold mb-4">
Role Summary
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{ROLES.map((r) => {
const count = (data?.items ?? []).filter(
(u: any) => u.role === r.role
).length;
return (
<div
key={r.role}
className="rounded-xl border bg-white p-5"
>
<div
className={`inline-flex px-3 py-1 rounded-full text-xs font-semibold ${ROLE_BADGE[r.role]}`}
>
{r.label}
</div>
<div className="text-3xl font-bold mt-4">
{count}
</div>
<div className="text-sm text-slate-500 mt-2">
{r.desc}
</div>
</div>
);
})}
</div>
</>
)}
{/* Org profile */}
{org && (
<div className="card p-5">
<h3 className="font-bold mb-3">{t.organization_profile}</h3>
<div className="grid sm:grid-cols-2 gap-3 text-sm">
<Info label="Name" value={org.name} />
<Info label="Org ID" value={org.slug} />
<Info label="GSTIN" value={org.gstin} />
<Info label="Timezone" value={org.timezone} />
<Info label="Plan" value={org.plan?.name} />
<Info label="Language" value={org.language} />
<Info label="Address" value={org.address} />
</div>
</div>
)}
{selectedUser && (
<div
className="fixed inset-0 bg-black/30 grid place-items-center z-50"
onClick={() => setSelectedUser(null)}
>
<div
className="bg-white rounded-2xl w-full max-w-sm p-5 shadow-xl"
onClick={(e)=>e.stopPropagation()}
>
{/* Header */}
<div className="flex justify-between items-start">
<h3 className="font-bold text-lg">
User Detail
</h3>
<button onClick={()=>setSelectedUser(null)}>
<X className="h-5 w-5 text-slate-400"/>
</button>
</div>
{/* Profile */}
{/* Profile */}
<div className="mt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="
h-12 w-12 rounded-full
bg-brand-100 text-brand-700
grid place-items-center
font-bold
"
>
{selectedUser.name?.[0]?.toUpperCase()}
</div>
<div>
<div className="font-semibold text-base">
{selectedUser.name}
</div>
<div className="text-xs text-slate-500 mt-1">
{selectedUser.role}
</div>
</div>
</div>
{/* Enable / Disable button */}
<span className={`
text-xs px-2 py-1 rounded-full
${STATUS_BADGE[selectedUser.status]}
`}>
{selectedUser.status}
</span>
</div>
{/* Email + Phone */}
<div className="mt-4 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">
Email
</span>
<span className="font-medium">
{selectedUser.email ?? "—"}
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">
Phone
</span>
<span className="font-medium">
{selectedUser.phone ?? "—"}
</span>
</div>
</div>
</div>
<div className="flex border-b mt-5">
<button
onClick={() => setDetailTab("profile")}
className={`px-4 py-2 text-sm ${
detailTab === "profile"
? "border-b-2 border-brand-600 font-semibold text-brand-700"
: "text-slate-500"
}`}
>
Profile
</button>
<button
onClick={() => setDetailTab("permissions")}
className={`px-4 py-2 text-sm ${
detailTab === "permissions"
? "border-b-2 border-brand-600 font-semibold text-brand-700"
: "text-slate-500"
}`}
>
Role & Permissions
</button>
<button
onClick={() => setDetailTab("activity")}
className={`px-4 py-2 text-sm ${
detailTab === "activity"
? "border-b-2 border-brand-600 font-semibold text-brand-700"
: "text-slate-500"
}`}
>
Activity Log
</button>
</div>
{/* Details */}
{detailTab === "profile" && (
<>
{/* Your existing Info components */}
<Info label="Department" value={selectedUser.department} />
<Info
label="Joined On"
value={
selectedUser.joined_at
? new Date(selectedUser.joined_at).toLocaleDateString()
: "—"
}
/>
<Info
label="Last Login"
value={
selectedUser.last_login
? new Date(selectedUser.last_login).toLocaleString()
: "—"
}
/>
<Info label="Email" value={selectedUser.email} />
<Info label="Phone" value={selectedUser.phone} />
<Info label="Timezone" value={org?.timezone} />
<Info label="Language" value={org?.language} />
</>
)}
{detailTab === "permissions" && (
<div className="space-y-4 mt-4">
<Info
label="Current Role"
value={selectedUser.role}
/>
<Info
label="Department"
value={selectedUser.department}
/>
<div>
<h4 className="font-semibold mb-2">
Permissions
</h4>
<div className="space-y-2">
{selectedUser.role === "admin" && (
<>
<Permission text="Manage Products" />
<Permission text="Manage Users" />
<Permission text="Billing" />
<Permission text="Generate QR Codes" />
<Permission text="Compliance" />
<Permission text="Organization Settings" />
</>
)}
{selectedUser.role === "finance" && (
<>
<Permission text="Invoices" />
<Permission text="Subscription" />
<Permission text="Payments" />
</>
)}
{selectedUser.role === "user" && (
<>
<Permission text="Products" />
<Permission text="QR Generation" />
<Permission text="Batch Management" />
<Permission text="Analytics" />
</>
)}
</div>
</div>
</div>
)}
{detailTab === "activity" && (
<div className="mt-4">
{activityLogs.length === 0 ? (
<div className="text-center text-slate-400 py-8">
No activity found
</div>
) : (
<div className="space-y-3 max-h-72 overflow-y-auto">
{activityLogs.map((log) => (
<div
key={log.id}
className="border rounded-lg p-3"
>
<div className="font-medium text-sm">
{log.action}
</div>
<div className="text-xs text-slate-500 mt-1">
{log.details}
</div>
<div className="text-xs text-slate-400 mt-2">
{new Date(log.created_at).toLocaleString()}
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Buttons */}
<div className="flex gap-2 mt-5">
<button
onClick={()=>{
setInvite({
email:selectedUser.email,
role:selectedUser.role,
department:selectedUser.department ?? "",
name:selectedUser.name?? "",
password:selectedUser.password ?? "",
});
setEditingUser(selectedUser);
setSelectedUser(null);
setOpen(true);
}}
className="
flex-1 border rounded-lg py-2 text-sm
"
>
Edit User
</button>
<button
onClick={()=>{
setStatus(
selectedUser.member_id,
selectedUser.status==="active"
?"disabled"
:"active"
);
setSelectedUser(null);
}}
className={`
flex-1 border rounded-lg py-2 text-sm
${
selectedUser.status==="disabled"
?"border-emerald-200 text-emerald-600"
:"border-red-200 text-red-600"
}
`}
>
{
selectedUser.status==="disabled"
? "✅ Enable User"
: "🚫 Disable User"
}
</button>
</div>
</div>
</div>
)}
{/* Invite modal */}
{open && (
<div className="fixed inset-0 bg-black/30 grid place-items-center z-50" onClick={() => setOpen(false)}>
<div className="bg-white rounded-2xl w-full max-w-md p-6" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="font-bold text-lg">
{editingUser ? "Edit User" : t.invite_user}
</h3>
<button onClick={() => setOpen(false)}><X className="h-5 w-5 text-slate-400" /></button>
</div>
{err && <p className="text-sm text-rose-600 mb-3">{err}</p>}
<label className="block mb-3">
<span className="text-sm font-medium">Name</span>
<input
value={invite.name}
onChange={(e) =>
setInvite({ ...invite, name: e.target.value })
}
className="mt-1 w-full rounded-lg border border-slate-200 px-3 py-2"
/>
</label>
<label className="block mb-3">
<span className="text-sm font-medium">Email</span>
<input
value={invite.email}
disabled={!!editingUser}
onChange={(e) =>
setInvite({ ...invite, email: e.target.value })
}
className="mt-1 w-full px-3 py-2 rounded-lg border border-slate-200 text-sm disabled:bg-slate-100"
/>
</label>
<label className="block mb-3">
<span className="text-sm font-medium">Role</span>
<select value={invite.role} onChange={(e) => setInvite({ ...invite, role: e.target.value })}
className="mt-1 w-full px-3 py-2 rounded-lg border border-slate-200 text-sm">
{ROLES.map((r) => <option key={r.role} value={r.role}>{r.label}</option>)}
</select>
</label>
{!editingUser && (
<label className="block mb-3">
<span>Password</span>
<input
type="password"
value={invite.password}
onChange={(e)=>
setInvite({...invite,password:e.target.value})
}
className="mt-1 w-full px-3 py-2 rounded-lg border"
/>
</label>
)}
<label className="block mb-4">
<span className="text-sm font-medium">Department (optional)</span>
<select
value={invite.department}
onChange={(e) =>
setInvite({ ...invite, department: e.target.value })
}
className="mt-1 w-full px-3 py-2 rounded-lg border border-slate-200 text-sm"
>
<option value="">Select Department</option>
<option value="Administration">Administration</option>
<option value="Production">Production</option>
<option value="Quality">Quality</option>
<option value="Warehouse">Warehouse</option>
<option value="Sales">Sales</option>
<option value="Finance">Finance</option>
<option value="HR">HR</option>
<option value="Marketing">Marketing</option>
</select>
</label>
<button
onClick={editingUser ? updateUser : sendInvite}
className="w-full bg-brand-600 text-white py-2.5 rounded-lg font-semibold"
>
{editingUser ? "Save Changes" : "Send Invite"}
</button>
</div>
</div>
)}
</div>
);
}
function Kpi({ label, value }: any) {
return (
<div className="card p-5">
<div className="text-sm text-slate-500">{label}</div>
<div className="text-2xl font-extrabold mt-1">{value}</div>
</div>
);
}
function Info({ 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">{value ?? "—"}</span>
</div>
);
}