enh
This commit is contained in:
parent
55bfd1aafa
commit
dc21636b3f
17 changed files with 57006 additions and 9 deletions
|
|
@ -63,7 +63,21 @@
|
|||
"backgroundColor": "#fff0f6"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "in.freshyo.adminui"
|
||||
"package": "in.freshyo.adminui",
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "VIEW",
|
||||
"autoVerify": true,
|
||||
"data": [
|
||||
{
|
||||
"scheme": "https",
|
||||
"host": "ui.freshyo.in",
|
||||
"pathPrefix": "/manage-orders/order-details"
|
||||
}
|
||||
],
|
||||
"category": ["BROWSABLE", "DEFAULT"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
"distribution": "internal",
|
||||
"channel": "development"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal",
|
||||
|
|
|
|||
Binary file not shown.
56362
apps/backend/dumps/latest.sql
Normal file
56362
apps/backend/dumps/latest.sql
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@
|
|||
"deploy": "wrangler deploy --config wrangler.prod.toml",
|
||||
"wrangler:dev": "wrangler dev worker.ts --config wrangler.toml",
|
||||
"wrangler:deploy": "wrangler deploy worker.ts --config wrangler.toml",
|
||||
"pull_db": "wrangler d1 export freshyo-dev --config wrangler.prod.toml --remote --output ./dumps/latest.sql && bash ./scripts/populate_localdb.sh",
|
||||
"docker:build": "cd .. && docker buildx build --platform linux/amd64 -t mohdshafiuddin54/health_petal:latest --progress=plain -f backend/Dockerfile .",
|
||||
"docker:push": "docker push mohdshafiuddin54/health_petal:latest"
|
||||
},
|
||||
|
|
|
|||
21
apps/backend/scripts/populate_localdb.sh
Executable file
21
apps/backend/scripts/populate_localdb.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DUMP_FILE="$ROOT_DIR/dumps/latest.sql"
|
||||
WRANGLER_CONFIG="$ROOT_DIR/wrangler.dev.toml"
|
||||
DB_NAME="freshyo-dev"
|
||||
|
||||
if [ ! -f "$DUMP_FILE" ]; then
|
||||
echo "Dump file not found: $DUMP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$WRANGLER_CONFIG" ]; then
|
||||
echo "Wrangler config not found: $WRANGLER_CONFIG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wrangler d1 execute "$DB_NAME" --local --file "$DUMP_FILE" --config "$WRANGLER_CONFIG"
|
||||
|
||||
echo "Local D1 database populated from $DUMP_FILE"
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
import { sendTelegramMessage } from '@/src/lib/telegram-service'
|
||||
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
|
||||
import { ensureWorkerInit } from './worker-init';
|
||||
import { getAppUrl } from '@/src/lib/env-exporter'
|
||||
|
||||
interface OrderIdMessage {
|
||||
orderIds: number[];
|
||||
|
|
@ -26,6 +27,18 @@ const formatDateTime = (dateStr: string | null | undefined): string => {
|
|||
});
|
||||
};
|
||||
|
||||
const buildTelegramLinks = (orderId: number, userId?: number | null): string => {
|
||||
const baseUrl = getAppUrl() || 'https://ui.freshyo.in'
|
||||
const orderUrl = `${baseUrl}/manage-orders/order-details/${orderId}`
|
||||
const orderLink = `↪ <a href="${orderUrl}">Order</a>`
|
||||
if (!userId) {
|
||||
return orderLink
|
||||
}
|
||||
const userUrl = `${baseUrl}/user-management/${userId}`
|
||||
const userLink = `↪ <a href="${userUrl}">User</a>`
|
||||
return `${orderLink} | ${userLink}`
|
||||
}
|
||||
|
||||
const formatOrderMessageWithFullData = (ordersData: any[]): string => {
|
||||
console.log('formatting the msg')
|
||||
let message = '🛒 <b>New Order Placed</b>\n\n';
|
||||
|
|
@ -55,6 +68,8 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
|
|||
message += ` 📞 ${order.address.phone}\n`;
|
||||
}
|
||||
|
||||
message += `\n${buildTelegramLinks(order.id, order.userId)}\n`
|
||||
|
||||
if (index < ordersData.length - 1) {
|
||||
message += '\n---\n\n';
|
||||
}
|
||||
|
|
@ -80,6 +95,8 @@ ${orderData.orderItems?.map((item: any) => ` • ${item.product?.name || 'Unkno
|
|||
❓ <b>Reason:</b> ${cancellationData.reason}
|
||||
👤 <b>Cancelled by:</b> ${cancellationData.cancelledBy === 'admin' ? 'Admin' : 'User'}
|
||||
⏰ <b>Time:</b> ${formatDateTime(cancellationData.cancelledAt)}
|
||||
|
||||
${buildTelegramLinks(orderData.id, orderData.userId)}
|
||||
`;
|
||||
|
||||
return message;
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ export const scheduleStoreInitialization = async (): Promise<void> => {
|
|||
}
|
||||
|
||||
const id = env.CACHE_CREATOR.idFromName('store-init')
|
||||
// const stub = env.CACHE_CREATOR.get(id)
|
||||
const stub = env.CACHE_CREATOR.getByName('store-init')
|
||||
const stub = env.CACHE_CREATOR.get(id)
|
||||
try {
|
||||
await stub.fetch('https://cache-creator/schedule', { method: 'POST' })
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export default {
|
|||
},
|
||||
ctx: ExecutionContext
|
||||
) {
|
||||
console.log(env)
|
||||
ensureWorkerInit(env)
|
||||
const app = createApp()
|
||||
return app.fetch(request, env, ctx)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"deploy": "wrangler pages deploy dist --project-name=freshyo-fallbackui",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\""
|
||||
|
|
|
|||
12
apps/fallback-ui/public/.well-known/assetlinks.json
Normal file
12
apps/fallback-ui/public/.well-known/assetlinks.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "in.freshyo.adminui",
|
||||
"sha256_cert_fingerprints": [
|
||||
"49:65:C6:4F:DE:31:95:1B:69:B1:15:1E:71:26:39:33:56:37:9E:A6:4B:29:59:F4:16:24:18:62:7A:9A:60:A3"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -12,6 +12,8 @@ import { LocationMarkerRoute } from './routes/location-marker'
|
|||
import { UserConnectRoute } from './routes/user-connect'
|
||||
import Inauguration from './routes/inauguration'
|
||||
import { DemoRoute } from './routes/demo'
|
||||
import { OrderDetailsRoute } from './routes/order-details'
|
||||
import { UserDetailsRoute } from './routes/user-details'
|
||||
import { AuthWrapper } from './components/AuthWrapper'
|
||||
import { SuperAdminGuard } from './components/SuperAdminGuard'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -135,6 +137,30 @@ const demoRoute = new Route({
|
|||
)
|
||||
})
|
||||
|
||||
const orderDetailsRoute = new Route({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/manage-orders/order-details/$id',
|
||||
component: () => (
|
||||
<Suspense fallback={<p>Loading order details…</p>}>
|
||||
<AuthWrapper>
|
||||
<OrderDetailsRoute />
|
||||
</AuthWrapper>
|
||||
</Suspense>
|
||||
)
|
||||
})
|
||||
|
||||
const userDetailsRoute = new Route({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/user-management/$id',
|
||||
component: () => (
|
||||
<Suspense fallback={<p>Loading user details…</p>}>
|
||||
<AuthWrapper>
|
||||
<UserDetailsRoute />
|
||||
</AuthWrapper>
|
||||
</Suspense>
|
||||
)
|
||||
})
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
dashboardRoute,
|
||||
vendorOrderListRoute,
|
||||
|
|
@ -145,7 +171,9 @@ const routeTree = rootRoute.addChildren([
|
|||
userConnectRoute,
|
||||
locationMarkerRoute,
|
||||
inaugurationRoute,
|
||||
demoRoute
|
||||
demoRoute,
|
||||
orderDetailsRoute,
|
||||
userDetailsRoute
|
||||
])
|
||||
|
||||
export function createAppRouter() {
|
||||
|
|
|
|||
282
apps/fallback-ui/src/routes/order-details.tsx
Normal file
282
apps/fallback-ui/src/routes/order-details.tsx
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { useParams, useNavigate, Link } 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 OrderDetailsRoute() {
|
||||
const { id } = useParams({ from: "/manage-orders/order-details/$id" });
|
||||
const navigate = useNavigate();
|
||||
const [incidentComment, setIncidentComment] = useState("");
|
||||
const [negativityScore, setNegativityScore] = useState("");
|
||||
|
||||
const orderId = Number(id);
|
||||
const {
|
||||
data: orderData,
|
||||
isLoading,
|
||||
error,
|
||||
} = trpc.admin.order.getOrderDetails.useQuery(
|
||||
{ orderId: Number.isFinite(orderId) ? orderId : 0 },
|
||||
{ enabled: Number.isFinite(orderId) }
|
||||
);
|
||||
|
||||
const order = orderData as any;
|
||||
|
||||
const { data: incidentsData, refetch: refetchIncidents } =
|
||||
trpc.admin.user.getUserIncidents.useQuery(
|
||||
{ userId: order?.userId ?? 0 },
|
||||
{ enabled: Boolean(order?.userId) }
|
||||
);
|
||||
|
||||
const addIncidentMutation = trpc.admin.user.addUserIncident.useMutation({
|
||||
onSuccess: () => {
|
||||
setIncidentComment("");
|
||||
setNegativityScore("");
|
||||
refetchIncidents();
|
||||
},
|
||||
});
|
||||
|
||||
const subtotal = useMemo(() => {
|
||||
if (!order?.items?.length) return 0;
|
||||
return order.items.reduce((sum: number, item: any) => sum + item.amount, 0);
|
||||
}, [order]);
|
||||
|
||||
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 order details...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !order) {
|
||||
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 order details"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statusBadge = getStatusBadge(order.status);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-500">Order Details</p>
|
||||
<h1 className="text-2xl font-semibold text-slate-900">#{order.readableId}</h1>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate({ to: "/vendor-order-list" })}
|
||||
>
|
||||
Back to Orders
|
||||
</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">Status</p>
|
||||
<div className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold uppercase ${statusBadge}`}>
|
||||
{order.status}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
<p className="font-medium text-slate-700">Placed on</p>
|
||||
<p>{dayjs(order.createdAt).format("MMM DD, YYYY • h:mm A")}</p>
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
<p className="font-medium text-slate-700">Delivery</p>
|
||||
<p>
|
||||
{order.isFlashDelivery
|
||||
? dayjs(order.createdAt).add(30, "minutes").format("MMM DD, YYYY • h:mm A")
|
||||
: order.slotInfo?.time
|
||||
? dayjs(order.slotInfo.time).format("MMM DD, YYYY • h:mm A")
|
||||
: "Not scheduled"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.status === "cancelled" && order.cancelReason && (
|
||||
<div className="rounded-2xl border border-red-200 bg-red-50 p-5 text-sm text-red-700">
|
||||
<p className="font-semibold">Cancellation Reason</p>
|
||||
<p className="mt-1 text-red-800">{order.cancelReason}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Customer Details</h2>
|
||||
<div className="grid gap-2 text-sm text-slate-600">
|
||||
{order.userId ? (
|
||||
<Link
|
||||
to="/user-management/$id"
|
||||
params={{ id: String(order.userId) }}
|
||||
className="font-semibold text-blue-700 hover:text-blue-800"
|
||||
>
|
||||
{order.customerName || "Unknown User"}
|
||||
</Link>
|
||||
) : (
|
||||
<p className="font-medium text-slate-900">{order.customerName || "Unknown User"}</p>
|
||||
)}
|
||||
<p>{order.customerMobile}</p>
|
||||
{order.customerEmail && <p>{order.customerEmail}</p>}
|
||||
<p className="whitespace-pre-line">
|
||||
{order.address?.name} {order.address?.line1}
|
||||
{order.address?.line2 ? `, ${order.address.line2}` : ""}
|
||||
{`\n${order.address?.city}, ${order.address?.state} - ${order.address?.pincode}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.userId && (
|
||||
<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: order.userId,
|
||||
orderId: order.id,
|
||||
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">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Items Ordered</h2>
|
||||
<div className="space-y-3">
|
||||
{order.items?.map((item: any, index: number) => (
|
||||
<div
|
||||
key={`${item.id}-${index}`}
|
||||
className="flex items-center justify-between border-b border-slate-100 pb-3"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-900">{item.name}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{Number(item.quantity) * item.productSize} {item.unit} × ₹{item.price}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-slate-900">₹{item.amount}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 border-t border-slate-100 pt-4 text-sm text-slate-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span className="font-semibold text-slate-900">₹{subtotal}</span>
|
||||
</div>
|
||||
{order.discountAmount > 0 && (
|
||||
<div className="flex items-center justify-between mt-2 text-emerald-600">
|
||||
<span>Discount</span>
|
||||
<span className="font-semibold">-₹{order.discountAmount}</span>
|
||||
</div>
|
||||
)}
|
||||
{order.deliveryCharge > 0 && (
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span>Delivery Charge</span>
|
||||
<span className="font-semibold text-slate-900">₹{order.deliveryCharge}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-4 text-base font-semibold text-slate-900">
|
||||
<span>Total Amount</span>
|
||||
<span>₹{order.totalAmount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.adminNotes && (
|
||||
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900">
|
||||
<p className="font-semibold">Admin Notes</p>
|
||||
<p className="mt-1">{order.adminNotes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{order.couponCode && (
|
||||
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 p-5">
|
||||
<p className="text-sm font-semibold text-emerald-800">Coupon Applied</p>
|
||||
<p className="mt-1 text-sm text-emerald-700">{order.couponCode}</p>
|
||||
{order.couponDescription && (
|
||||
<p className="mt-1 text-xs text-emerald-600">{order.couponDescription}</p>
|
||||
)}
|
||||
<p className="mt-2 text-sm font-semibold text-emerald-800">-₹{order.discountAmount}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
257
apps/fallback-ui/src/routes/user-details.tsx
Normal file
257
apps/fallback-ui/src/routes/user-details.tsx
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -629,6 +629,7 @@ export async function getProductsForRecentOrders(
|
|||
|
||||
export interface OrderWithFullData {
|
||||
id: number
|
||||
userId: number
|
||||
totalAmount: string
|
||||
isFlashDelivery: boolean
|
||||
address: {
|
||||
|
|
|
|||
|
|
@ -667,6 +667,7 @@ export async function getProductsForRecentOrders(
|
|||
|
||||
export interface OrderWithFullData {
|
||||
id: number
|
||||
userId: number
|
||||
totalAmount: string
|
||||
isFlashDelivery: boolean
|
||||
address: {
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
|
|||
// const BASE_API_URL = 'http://10.0.2.2:4000';
|
||||
// const BASE_API_URL = 'http://192.168.100.101:4000';
|
||||
// const BASE_API_URL = 'http://192.168.1.5:4000';
|
||||
// const BASE_API_URL = 'http://192.168.1.5:8787';
|
||||
const BASE_API_URL = 'http://192.168.1.5:8787';
|
||||
// let BASE_API_URL = "https://raw.freshyo.in";
|
||||
let BASE_API_URL = "https://worker.freshyo.in";
|
||||
// let BASE_API_URL = "https://worker.freshyo.in";
|
||||
// let BASE_API_URL = "https://freshyo.technocracy.ovh";
|
||||
// let BASE_API_URL = 'http://192.168.100.109:8787';
|
||||
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue