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"
|
"backgroundColor": "#fff0f6"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"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": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"development": {
|
"development": {
|
||||||
"developmentClient": true,
|
"distribution": "internal",
|
||||||
"distribution": "internal"
|
"channel": "development"
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"distribution": "internal",
|
"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",
|
"deploy": "wrangler deploy --config wrangler.prod.toml",
|
||||||
"wrangler:dev": "wrangler dev worker.ts --config wrangler.toml",
|
"wrangler:dev": "wrangler dev worker.ts --config wrangler.toml",
|
||||||
"wrangler:deploy": "wrangler deploy 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: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"
|
"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 { sendTelegramMessage } from '@/src/lib/telegram-service'
|
||||||
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
|
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
|
||||||
import { ensureWorkerInit } from './worker-init';
|
import { ensureWorkerInit } from './worker-init';
|
||||||
|
import { getAppUrl } from '@/src/lib/env-exporter'
|
||||||
|
|
||||||
interface OrderIdMessage {
|
interface OrderIdMessage {
|
||||||
orderIds: number[];
|
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 => {
|
const formatOrderMessageWithFullData = (ordersData: any[]): string => {
|
||||||
console.log('formatting the msg')
|
console.log('formatting the msg')
|
||||||
let message = '🛒 <b>New Order Placed</b>\n\n';
|
let message = '🛒 <b>New Order Placed</b>\n\n';
|
||||||
|
|
@ -55,6 +68,8 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
|
||||||
message += ` 📞 ${order.address.phone}\n`;
|
message += ` 📞 ${order.address.phone}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message += `\n${buildTelegramLinks(order.id, order.userId)}\n`
|
||||||
|
|
||||||
if (index < ordersData.length - 1) {
|
if (index < ordersData.length - 1) {
|
||||||
message += '\n---\n\n';
|
message += '\n---\n\n';
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +94,9 @@ ${orderData.orderItems?.map((item: any) => ` • ${item.product?.name || 'Unkno
|
||||||
|
|
||||||
❓ <b>Reason:</b> ${cancellationData.reason}
|
❓ <b>Reason:</b> ${cancellationData.reason}
|
||||||
👤 <b>Cancelled by:</b> ${cancellationData.cancelledBy === 'admin' ? 'Admin' : 'User'}
|
👤 <b>Cancelled by:</b> ${cancellationData.cancelledBy === 'admin' ? 'Admin' : 'User'}
|
||||||
⏰ <b>Time:</b> ${formatDateTime(cancellationData.cancelledAt)}
|
⏰ <b>Time:</b> ${formatDateTime(cancellationData.cancelledAt)}
|
||||||
|
|
||||||
|
${buildTelegramLinks(orderData.id, orderData.userId)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,7 @@ export const scheduleStoreInitialization = async (): Promise<void> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = env.CACHE_CREATOR.idFromName('store-init')
|
const id = env.CACHE_CREATOR.idFromName('store-init')
|
||||||
// const stub = env.CACHE_CREATOR.get(id)
|
const stub = env.CACHE_CREATOR.get(id)
|
||||||
const stub = env.CACHE_CREATOR.getByName('store-init')
|
|
||||||
try {
|
try {
|
||||||
await stub.fetch('https://cache-creator/schedule', { method: 'POST' })
|
await stub.fetch('https://cache-creator/schedule', { method: 'POST' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export default {
|
||||||
},
|
},
|
||||||
ctx: ExecutionContext
|
ctx: ExecutionContext
|
||||||
) {
|
) {
|
||||||
|
console.log(env)
|
||||||
ensureWorkerInit(env)
|
ensureWorkerInit(env)
|
||||||
const app = createApp()
|
const app = createApp()
|
||||||
return app.fetch(request, env, ctx)
|
return app.fetch(request, env, ctx)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"deploy": "wrangler pages deploy dist --project-name=freshyo-fallbackui",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint \"src/**/*.{ts,tsx}\""
|
"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 { UserConnectRoute } from './routes/user-connect'
|
||||||
import Inauguration from './routes/inauguration'
|
import Inauguration from './routes/inauguration'
|
||||||
import { DemoRoute } from './routes/demo'
|
import { DemoRoute } from './routes/demo'
|
||||||
|
import { OrderDetailsRoute } from './routes/order-details'
|
||||||
|
import { UserDetailsRoute } from './routes/user-details'
|
||||||
import { AuthWrapper } from './components/AuthWrapper'
|
import { AuthWrapper } from './components/AuthWrapper'
|
||||||
import { SuperAdminGuard } from './components/SuperAdminGuard'
|
import { SuperAdminGuard } from './components/SuperAdminGuard'
|
||||||
import { cn } from '@/lib/utils'
|
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([
|
const routeTree = rootRoute.addChildren([
|
||||||
dashboardRoute,
|
dashboardRoute,
|
||||||
vendorOrderListRoute,
|
vendorOrderListRoute,
|
||||||
|
|
@ -145,7 +171,9 @@ const routeTree = rootRoute.addChildren([
|
||||||
userConnectRoute,
|
userConnectRoute,
|
||||||
locationMarkerRoute,
|
locationMarkerRoute,
|
||||||
inaugurationRoute,
|
inaugurationRoute,
|
||||||
demoRoute
|
demoRoute,
|
||||||
|
orderDetailsRoute,
|
||||||
|
userDetailsRoute
|
||||||
])
|
])
|
||||||
|
|
||||||
export function createAppRouter() {
|
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 {
|
export interface OrderWithFullData {
|
||||||
id: number
|
id: number
|
||||||
|
userId: number
|
||||||
totalAmount: string
|
totalAmount: string
|
||||||
isFlashDelivery: boolean
|
isFlashDelivery: boolean
|
||||||
address: {
|
address: {
|
||||||
|
|
|
||||||
|
|
@ -667,6 +667,7 @@ export async function getProductsForRecentOrders(
|
||||||
|
|
||||||
export interface OrderWithFullData {
|
export interface OrderWithFullData {
|
||||||
id: number
|
id: number
|
||||||
|
userId: number
|
||||||
totalAmount: string
|
totalAmount: string
|
||||||
isFlashDelivery: boolean
|
isFlashDelivery: boolean
|
||||||
address: {
|
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://10.0.2.2:4000';
|
||||||
// const BASE_API_URL = 'http://192.168.100.101: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: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://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 = "https://freshyo.technocracy.ovh";
|
||||||
// let BASE_API_URL = 'http://192.168.100.109:8787';
|
// let BASE_API_URL = 'http://192.168.100.109:8787';
|
||||||
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue