|
@@ -30,7 +30,7 @@ import {
|
|
|
CheckCircle2, AlertTriangle, Crown, Mail, Send,
|
|
CheckCircle2, AlertTriangle, Crown, Mail, Send,
|
|
|
Clock, XCircle, RefreshCw, Download, History,
|
|
Clock, XCircle, RefreshCw, Download, History,
|
|
|
MoreHorizontal, UserPlus, CheckSquare, Square,
|
|
MoreHorizontal, UserPlus, CheckSquare, Square,
|
|
|
- ArrowUpDown, Eye, Copy, ExternalLink,
|
|
|
|
|
|
|
+ ArrowUpDown, Eye, Copy, ExternalLink, Pencil, Link2Off,
|
|
|
} from "lucide-react";
|
|
} from "lucide-react";
|
|
|
|
|
|
|
|
/* ─── Role config ─── */
|
|
/* ─── Role config ─── */
|
|
@@ -286,6 +286,9 @@ function UsersTab() {
|
|
|
open: boolean; type: "role" | "delete" | "bulkRole" | "bulkDelete";
|
|
open: boolean; type: "role" | "delete" | "bulkRole" | "bulkDelete";
|
|
|
userId?: number; userName?: string; currentRole?: string; newRole?: string;
|
|
userId?: number; userName?: string; currentRole?: string; newRole?: string;
|
|
|
}>({ open: false, type: "role" });
|
|
}>({ open: false, type: "role" });
|
|
|
|
|
+ const [erpEditState, setErpEditState] = useState<{
|
|
|
|
|
+ userId: number | null; value: string;
|
|
|
|
|
+ }>({ userId: null, value: "" });
|
|
|
|
|
|
|
|
const utils = trpc.useUtils();
|
|
const utils = trpc.useUtils();
|
|
|
const { data: usersList, isLoading } = trpc.users.list.useQuery();
|
|
const { data: usersList, isLoading } = trpc.users.list.useQuery();
|
|
@@ -334,6 +337,15 @@ function UsersTab() {
|
|
|
onError: (error) => toast.error("Bulk delete failed", { description: error.message }),
|
|
onError: (error) => toast.error("Bulk delete failed", { description: error.message }),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ const updateErpCidMutation = trpc.users.updateErpContactCid.useMutation({
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ toast.success("ERP Contact ID updated");
|
|
|
|
|
+ utils.users.list.invalidate();
|
|
|
|
|
+ setErpEditState({ userId: null, value: "" });
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error) => toast.error("Failed to update ERP Contact ID", { description: error.message }),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
const exportCsvQuery = trpc.users.exportCsv.useQuery(undefined, { enabled: false });
|
|
const exportCsvQuery = trpc.users.exportCsv.useQuery(undefined, { enabled: false });
|
|
|
|
|
|
|
|
const filteredUsers = useMemo(() => {
|
|
const filteredUsers = useMemo(() => {
|
|
@@ -472,6 +484,7 @@ function UsersTab() {
|
|
|
</th>
|
|
</th>
|
|
|
<th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>User</th>
|
|
<th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>User</th>
|
|
|
<th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Role</th>
|
|
<th className="text-left text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Role</th>
|
|
|
|
|
+ <th className="text-left text-xs font-semibold px-4 py-3 hidden lg:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>ERP Contact ID</th>
|
|
|
<th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Joined</th>
|
|
<th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Joined</th>
|
|
|
<th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Last Active</th>
|
|
<th className="text-left text-xs font-semibold px-4 py-3 hidden md:table-cell" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Last Active</th>
|
|
|
<th className="text-right text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Actions</th>
|
|
<th className="text-right text-xs font-semibold px-4 py-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>Actions</th>
|
|
@@ -480,14 +493,14 @@ function UsersTab() {
|
|
|
<tbody>
|
|
<tbody>
|
|
|
{isLoading ? (
|
|
{isLoading ? (
|
|
|
<tr>
|
|
<tr>
|
|
|
- <td colSpan={6} className="text-center py-12">
|
|
|
|
|
|
|
+ <td colSpan={7} className="text-center py-12">
|
|
|
<div className="animate-spin w-6 h-6 border-2 rounded-full mx-auto" style={{ borderColor: "#14532D", borderTopColor: "transparent" }} />
|
|
<div className="animate-spin w-6 h-6 border-2 rounded-full mx-auto" style={{ borderColor: "#14532D", borderTopColor: "transparent" }} />
|
|
|
<p className="text-sm mt-2" style={{ color: "#a8a29e" }}>Loading users...</p>
|
|
<p className="text-sm mt-2" style={{ color: "#a8a29e" }}>Loading users...</p>
|
|
|
</td>
|
|
</td>
|
|
|
</tr>
|
|
</tr>
|
|
|
) : filteredUsers.length === 0 ? (
|
|
) : filteredUsers.length === 0 ? (
|
|
|
<tr>
|
|
<tr>
|
|
|
- <td colSpan={6} className="text-center py-12">
|
|
|
|
|
|
|
+ <td colSpan={7} className="text-center py-12">
|
|
|
<Users className="w-8 h-8 mx-auto mb-2" style={{ color: "#d6d3d1" }} />
|
|
<Users className="w-8 h-8 mx-auto mb-2" style={{ color: "#d6d3d1" }} />
|
|
|
<p className="text-sm" style={{ color: "#a8a29e" }}>No users found</p>
|
|
<p className="text-sm" style={{ color: "#a8a29e" }}>No users found</p>
|
|
|
</td>
|
|
</td>
|
|
@@ -533,6 +546,58 @@ function UsersTab() {
|
|
|
</div>
|
|
</div>
|
|
|
</td>
|
|
</td>
|
|
|
<td className="px-4 py-3"><RoleBadge role={u.role} /></td>
|
|
<td className="px-4 py-3"><RoleBadge role={u.role} /></td>
|
|
|
|
|
+ <td className="px-4 py-3 hidden lg:table-cell">
|
|
|
|
|
+ {erpEditState.userId === u.id ? (
|
|
|
|
|
+ <div className="flex items-center gap-1">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ value={erpEditState.value}
|
|
|
|
|
+ onChange={(e) => setErpEditState(s => ({ ...s, value: e.target.value }))}
|
|
|
|
|
+ placeholder="e.g. C001234"
|
|
|
|
|
+ className="px-2 py-1 text-xs rounded border w-28"
|
|
|
|
|
+ style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif" }}
|
|
|
|
|
+ onKeyDown={(e) => {
|
|
|
|
|
+ if (e.key === "Enter") {
|
|
|
|
|
+ updateErpCidMutation.mutate({ userId: u.id, erpContactCid: erpEditState.value.trim() || null });
|
|
|
|
|
+ } else if (e.key === "Escape") {
|
|
|
|
|
+ setErpEditState({ userId: null, value: "" });
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ autoFocus
|
|
|
|
|
+ />
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => updateErpCidMutation.mutate({ userId: u.id, erpContactCid: erpEditState.value.trim() || null })}
|
|
|
|
|
+ disabled={updateErpCidMutation.isPending}
|
|
|
|
|
+ className="text-xs px-2 py-1 rounded text-white"
|
|
|
|
|
+ style={{ background: "#14532D" }}
|
|
|
|
|
+ >
|
|
|
|
|
+ Save
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setErpEditState({ userId: null, value: "" })}
|
|
|
|
|
+ className="text-xs px-2 py-1 rounded border"
|
|
|
|
|
+ style={{ borderColor: "#e7e0d5" }}
|
|
|
|
|
+ >
|
|
|
|
|
+ Cancel
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="flex items-center gap-1.5 group">
|
|
|
|
|
+ <span className="text-xs font-mono" style={{ color: u.erpContactCid ? "#292524" : "#a8a29e" }}>
|
|
|
|
|
+ {u.erpContactCid || "—"}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {!isSelf && (
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setErpEditState({ userId: u.id, value: u.erpContactCid ?? "" })}
|
|
|
|
|
+ className="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
|
|
|
+ title="Edit ERP Contact ID"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Pencil className="w-3 h-3" style={{ color: "#78716C" }} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </td>
|
|
|
<td className="px-4 py-3 hidden md:table-cell">
|
|
<td className="px-4 py-3 hidden md:table-cell">
|
|
|
<span className="text-xs" style={{ color: "#78716C" }}>
|
|
<span className="text-xs" style={{ color: "#78716C" }}>
|
|
|
{u.createdAt ? new Date(u.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "—"}
|
|
{u.createdAt ? new Date(u.createdAt).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "—"}
|