257 lines
10 KiB
TypeScript
257 lines
10 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
|
import dayjs from "dayjs";
|
|
import { trpc } from "../trpc/client";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
const getStatusBadge = (status?: string) => {
|
|
if (status === "delivered") {
|
|
return "bg-emerald-50 text-emerald-700 border-emerald-200";
|
|
}
|
|
if (status === "cancelled") {
|
|
return "bg-red-50 text-red-700 border-red-200";
|
|
}
|
|
return "bg-amber-50 text-amber-700 border-amber-200";
|
|
};
|
|
|
|
export function UserDetailsRoute() {
|
|
const { id } = useParams({ from: "/user-management/$id" });
|
|
const navigate = useNavigate();
|
|
const [incidentComment, setIncidentComment] = useState("");
|
|
const [negativityScore, setNegativityScore] = useState("");
|
|
|
|
const userId = Number(id);
|
|
const { data, isLoading, error } = trpc.admin.user.getUserDetails.useQuery(
|
|
{ userId: Number.isFinite(userId) ? userId : 0 },
|
|
{ enabled: Number.isFinite(userId) }
|
|
);
|
|
|
|
const { data: incidentsData, refetch: refetchIncidents } =
|
|
trpc.admin.user.getUserIncidents.useQuery(
|
|
{ userId },
|
|
{ enabled: Number.isFinite(userId) }
|
|
);
|
|
|
|
const updateSuspension = trpc.admin.user.updateUserSuspension.useMutation();
|
|
const addIncidentMutation = trpc.admin.user.addUserIncident.useMutation({
|
|
onSuccess: () => {
|
|
setIncidentComment("");
|
|
setNegativityScore("");
|
|
refetchIncidents();
|
|
},
|
|
});
|
|
|
|
const user = data?.user as any;
|
|
const orders = (data?.orders ?? []) as any[];
|
|
const orderCount = orders.length;
|
|
|
|
const statusBadge = useMemo(
|
|
() =>
|
|
user?.isSuspended
|
|
? "bg-red-50 text-red-700 border-red-200"
|
|
: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
|
[user?.isSuspended]
|
|
);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-[60vh] flex items-center justify-center">
|
|
<div className="rounded-2xl border border-slate-200 bg-white px-6 py-8 text-sm text-slate-600 shadow-sm">
|
|
Loading user details...
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !data) {
|
|
return (
|
|
<div className="min-h-[60vh] flex items-center justify-center">
|
|
<div className="rounded-2xl border border-red-200 bg-red-50 px-6 py-8 text-sm text-red-700 shadow-sm">
|
|
{error?.message || "Failed to load user details"}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs font-medium text-slate-500">User Details</p>
|
|
<h1 className="text-2xl font-semibold text-slate-900">
|
|
{user?.name || "Unnamed User"}
|
|
</h1>
|
|
</div>
|
|
<Button variant="outline" onClick={() => navigate({ to: "/" })}>
|
|
Back
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
<div>
|
|
<p className="text-sm text-slate-500">Mobile</p>
|
|
<p className="text-lg font-semibold text-slate-900">{user?.mobile || "No Mobile"}</p>
|
|
<p className="text-sm text-slate-500 mt-1">{user?.name || "Unnamed User"}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-slate-500">Status</p>
|
|
<div
|
|
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold uppercase ${statusBadge}`}
|
|
>
|
|
{user?.isSuspended ? "Suspended" : "Active"}
|
|
</div>
|
|
</div>
|
|
<div className="text-sm text-slate-600">
|
|
<p className="font-medium text-slate-700">Registered</p>
|
|
<p>{user?.createdAt ? dayjs(user.createdAt).format("MMM DD, YYYY • h:mm A") : "-"}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 border-t border-slate-100 pt-4 flex flex-wrap items-center justify-between gap-4">
|
|
<div>
|
|
<p className="text-sm font-semibold text-slate-900">Suspend User</p>
|
|
<p className="text-xs text-slate-500">Prevent user from placing orders</p>
|
|
</div>
|
|
<label className="flex items-center gap-2 text-sm text-slate-700">
|
|
<input
|
|
type="checkbox"
|
|
checked={Boolean(user?.isSuspended)}
|
|
onChange={() =>
|
|
updateSuspension.mutate({
|
|
userId,
|
|
isSuspended: !user?.isSuspended,
|
|
})
|
|
}
|
|
disabled={updateSuspension.isPending}
|
|
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
{updateSuspension.isPending ? "Updating..." : "Suspended"}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-6 shadow-sm">
|
|
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
|
<h2 className="text-lg font-semibold text-amber-900">User Incidents</h2>
|
|
</div>
|
|
|
|
{incidentsData?.incidents?.length ? (
|
|
<div className="space-y-3">
|
|
{incidentsData.incidents.map((incident: any) => (
|
|
<div key={incident.id} className="rounded-xl border border-amber-200 bg-white p-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-slate-500">
|
|
<span>{dayjs(incident.dateAdded).format("MMM DD, YYYY • h:mm A")}</span>
|
|
{incident.negativityScore ? (
|
|
<span className="rounded-md bg-red-100 px-2 py-1 text-red-700 font-semibold">
|
|
Score: {incident.negativityScore}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{incident.adminComment && (
|
|
<p className="mt-2 text-sm text-slate-800">{incident.adminComment}</p>
|
|
)}
|
|
<div className="mt-3 border-t border-slate-100 pt-2 text-xs text-slate-500">
|
|
Added by {incident.addedBy}
|
|
{incident.orderId ? ` • Order #${incident.orderId}` : ""}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-xl border border-amber-100 bg-white p-4 text-sm text-amber-800">
|
|
No incidents recorded for this user
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6 rounded-xl border border-amber-100 bg-white p-4">
|
|
<p className="text-sm font-semibold text-slate-900">Add Incident</p>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
Record an incident for this user. Higher negativity scores indicate more serious incidents.
|
|
</p>
|
|
<div className="mt-3 grid gap-3">
|
|
<textarea
|
|
value={incidentComment}
|
|
onChange={(event) => setIncidentComment(event.target.value)}
|
|
placeholder="Enter details about the incident..."
|
|
className="min-h-[96px] w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-blue-400 focus:outline-none"
|
|
/>
|
|
<input
|
|
type="number"
|
|
value={negativityScore}
|
|
onChange={(event) => setNegativityScore(event.target.value)}
|
|
placeholder="Negativity score (optional)"
|
|
className="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-blue-400 focus:outline-none"
|
|
/>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
onClick={() => {
|
|
addIncidentMutation.mutate({
|
|
userId,
|
|
orderId: undefined,
|
|
adminComment: incidentComment || undefined,
|
|
negativityScore: negativityScore ? Number(negativityScore) : undefined,
|
|
});
|
|
}}
|
|
disabled={addIncidentMutation.isPending}
|
|
>
|
|
{addIncidentMutation.isPending ? "Adding..." : "Add Incident"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-slate-900">Order History</h2>
|
|
<span className="text-sm text-slate-500">
|
|
{orderCount} {orderCount === 1 ? "order" : "orders"}
|
|
</span>
|
|
</div>
|
|
|
|
{orders.length === 0 ? (
|
|
<div className="rounded-xl border border-slate-100 bg-slate-50 p-6 text-sm text-slate-600">
|
|
No orders yet
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{orders.map((order) => (
|
|
<div
|
|
key={order.id}
|
|
className="rounded-xl border border-slate-100 bg-white p-4 shadow-sm"
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg font-semibold text-slate-900">#{order.readableId}</span>
|
|
{order.isFlashDelivery && (
|
|
<span className="rounded-full border border-amber-200 bg-amber-100 px-2 py-0.5 text-[10px] font-black uppercase text-amber-700">
|
|
⚡
|
|
</span>
|
|
)}
|
|
</div>
|
|
<span className={`rounded-full border px-2 py-1 text-xs font-semibold uppercase ${getStatusBadge(order.status)}`}>
|
|
{order.status}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-wrap items-center justify-between gap-3 text-sm text-slate-600">
|
|
<span>{dayjs(order.createdAt).format("MMM DD, YYYY • h:mm A")}</span>
|
|
<span>{order.itemCount} {order.itemCount === 1 ? "item" : "items"}</span>
|
|
<span className="text-base font-semibold text-slate-900">₹{order.totalAmount}</span>
|
|
</div>
|
|
<div className="mt-3">
|
|
<Button
|
|
variant="link"
|
|
onClick={() => navigate({ to: "/manage-orders/order-details/$id", params: { id: String(order.id) } })}
|
|
>
|
|
View Order Details
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|