freshyo/apps/fallback-ui/src/routes/user-details.tsx
2026-04-11 15:39:54 +05:30

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>
);
}