From 1dcfa0bfa5287ee0ff3208bbe2d9a5c37f7e30b6 Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Fri, 5 Jun 2026 17:12:44 +0000 Subject: [PATCH] Complete EHSAN POC: fix all code review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API routes: explicit return types on all Express handlers (fixes TS7030), `beneficiaryName` now accepted and stored in /thank-you route per OpenAPI spec. - Home page: added search bar (filters by case ID, description, name) + Featured Opportunities section with live cards, progress bars, and Donate buttons. - Opportunities page: added need-type filter pill bar (all 8 types + "All Types") with active state highlighting; empty state respects selected filter. - i18n: expanded translations with all previously hardcoded strings (trackCase, notFound, noData, currentStep, search, searchPlaceholder, featuredTitle, noResults, donate.caseSummary, donate.caseNotFound, admin.noRequests, admin.needType, admin.amount, admin.track, admin.whatsapp, track.caseInfo, track.rejected, track.currentStepLabel, track.submitThankYou, thankYou.successNote, thankYou.beneficiaryMessageLabel, whatsapp.donorPhone, whatsapp.beneficiaryMessage, whatsapp.noEntries, opportunities.noOpportunities, opportunities.verified). All pages now use t.* — zero hardcoded English UI strings. - TypeScript: both frontend (tsc --noEmit) and API server build are clean. --- artifacts/api-server/src/routes/requests.ts | 298 ++++++++++-------- .../ehsan-poc/src/lib/i18n/translations.ts | 82 ++++- artifacts/ehsan-poc/src/pages/admin.tsx | 96 ++++-- artifacts/ehsan-poc/src/pages/donate.tsx | 52 ++- artifacts/ehsan-poc/src/pages/home.tsx | 144 ++++++++- .../ehsan-poc/src/pages/opportunities.tsx | 107 +++++-- artifacts/ehsan-poc/src/pages/thank-you.tsx | 52 ++- artifacts/ehsan-poc/src/pages/track.tsx | 57 ++-- .../ehsan-poc/src/pages/whatsapp-log.tsx | 10 +- 9 files changed, 640 insertions(+), 258 deletions(-) diff --git a/artifacts/api-server/src/routes/requests.ts b/artifacts/api-server/src/routes/requests.ts index a21650c..8465ffb 100644 --- a/artifacts/api-server/src/routes/requests.ts +++ b/artifacts/api-server/src/routes/requests.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Router, Request, Response } from "express"; import { requests, donors, @@ -12,8 +12,8 @@ import { v4 as uuidv4 } from "uuid"; const router = Router(); -// ─── GET /requests ─────────────────────────────────────────────────────────── -router.get("/requests", (req, res) => { +// ─── GET /requests ──────────────────────────────────────────────────────────── +router.get("/requests", (req: Request, res: Response): void => { let result = [...requests]; const { status, needType } = req.query; if (status) result = result.filter((r) => r.status === status); @@ -21,18 +21,18 @@ router.get("/requests", (req, res) => { res.json(result); }); -// ─── GET /requests/new ────────────────────────────────────────────────────── -router.get("/requests/new", (_req, res) => { +// ─── GET /requests/new ─────────────────────────────────────────────────────── +router.get("/requests/new", (_req: Request, res: Response): void => { res.json(requests.filter((r) => r.status === "new")); }); -// ─── GET /requests/published ──────────────────────────────────────────────── -router.get("/requests/published", (_req, res) => { +// ─── GET /requests/published ───────────────────────────────────────────────── +router.get("/requests/published", (_req: Request, res: Response): void => { res.json(requests.filter((r) => r.status === "published")); }); -// ─── POST /requests ────────────────────────────────────────────────────────── -router.post("/requests", (req, res) => { +// ─── POST /requests ─────────────────────────────────────────────────────────── +router.post("/requests", (req: Request, res: Response): void => { const { beneficiaryName, nationalId, @@ -54,7 +54,8 @@ router.post("/requests", (req, res) => { !requestedAmount || !description ) { - return res.status(400).json({ error: "Missing required fields" }); + res.status(400).json({ error: "Missing required fields" }); + return; } const { eligible } = checkEligibility(nationalId); @@ -98,31 +99,39 @@ router.post("/requests", (req, res) => { }; requests.push(newReq); - return res.status(201).json(newReq); + res.status(201).json(newReq); }); -// ─── GET /requests/:id ─────────────────────────────────────────────────────── -router.get("/requests/:id", (req, res) => { - const item = requests.find((r) => r.id === req.params.id || r.caseId === req.params.id); - if (!item) return res.status(404).json({ error: "Not found" }); +// ─── GET /requests/:id ──────────────────────────────────────────────────────── +router.get("/requests/:id", (req: Request, res: Response): void => { + const item = requests.find( + (r) => r.id === req.params.id || r.caseId === req.params.id + ); + if (!item) { + res.status(404).json({ error: "Not found" }); + return; + } res.json(item); }); -// ─── Helper: find & update ─────────────────────────────────────────────────── +// ─── Helper: find & update ──────────────────────────────────────────────────── function findAndUpdate( id: string, updater: (r: DonationRequest) => void, - res: any -) { + res: Response +): void { const item = requests.find((r) => r.id === id || r.caseId === id); - if (!item) return res.status(404).json({ error: "Not found" }); + if (!item) { + res.status(404).json({ error: "Not found" }); + return; + } updater(item); item.updatedAt = new Date().toISOString(); res.json(item); } -// ─── POST /requests/:id/verify ─────────────────────────────────────────────── -router.post("/requests/:id/verify", (req, res) => { +// ─── POST /requests/:id/verify ──────────────────────────────────────────────── +router.post("/requests/:id/verify", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { @@ -133,8 +142,8 @@ router.post("/requests/:id/verify", (req, res) => { ); }); -// ─── POST /requests/:id/publish ────────────────────────────────────────────── -router.post("/requests/:id/publish", (req, res) => { +// ─── POST /requests/:id/publish ─────────────────────────────────────────────── +router.post("/requests/:id/publish", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { @@ -145,20 +154,23 @@ router.post("/requests/:id/publish", (req, res) => { ); }); -// ─── POST /requests/:id/donate ─────────────────────────────────────────────── -router.post("/requests/:id/donate", (req, res) => { +// ─── POST /requests/:id/donate ──────────────────────────────────────────────── +router.post("/requests/:id/donate", (req: Request, res: Response): void => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); - if (!item) return res.status(404).json({ error: "Not found" }); + if (!item) { + res.status(404).json({ error: "Not found" }); + return; + } const { donorName, donorPhone, donorEmail, amount } = req.body; if (!donorName || !donorPhone || !amount) { - return res.status(400).json({ error: "Missing donor details" }); + res.status(400).json({ error: "Missing donor details" }); + return; } const donorId = uuidv4(); - // add/update donor record const existingDonor = donors.find((d) => d.phone === donorPhone); if (existingDonor) { existingDonor.totalDonated += Number(amount); @@ -185,8 +197,8 @@ router.post("/requests/:id/donate", (req, res) => { res.json(item); }); -// ─── POST /requests/:id/deliver ────────────────────────────────────────────── -router.post("/requests/:id/deliver", (req, res) => { +// ─── POST /requests/:id/deliver ─────────────────────────────────────────────── +router.post("/requests/:id/deliver", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { @@ -197,27 +209,41 @@ router.post("/requests/:id/deliver", (req, res) => { ); }); -// ─── POST /requests/:id/confirm-receipt ────────────────────────────────────── -router.post("/requests/:id/confirm-receipt", (req, res) => { - findAndUpdate( - req.params.id, - (r) => { - r.status = "receipt_confirmed"; - r.currentStep = STATUS_STEP["receipt_confirmed"]; - }, - res - ); -}); +// ─── POST /requests/:id/confirm-receipt ─────────────────────────────────────── +router.post( + "/requests/:id/confirm-receipt", + (req: Request, res: Response): void => { + findAndUpdate( + req.params.id, + (r) => { + r.status = "receipt_confirmed"; + r.currentStep = STATUS_STEP["receipt_confirmed"]; + }, + res + ); + } +); -// ─── POST /requests/:id/thank-you ──────────────────────────────────────────── -router.post("/requests/:id/thank-you", (req, res) => { +// ─── POST /requests/:id/thank-you ───────────────────────────────────────────── +router.post("/requests/:id/thank-you", (req: Request, res: Response): void => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); - if (!item) return res.status(404).json({ error: "Not found" }); + if (!item) { + res.status(404).json({ error: "Not found" }); + return; + } - const { message } = req.body; - if (!message) return res.status(400).json({ error: "Message is required" }); + const { message, beneficiaryName } = req.body; + if (!message) { + res.status(400).json({ error: "Message is required" }); + return; + } + + // Update beneficiary name if provided + if (beneficiaryName) { + item.beneficiaryName = beneficiaryName; + } item.thankYouMessage = message; item.status = "thank_you_submitted"; @@ -242,96 +268,95 @@ router.post("/requests/:id/thank-you", (req, res) => { res.json(item); }); -// ─── POST /requests/:id/send-whatsapp ──────────────────────────────────────── -router.post("/requests/:id/send-whatsapp", async (req, res) => { - const item = requests.find( - (r) => r.id === req.params.id || r.caseId === req.params.id - ); - if (!item) return res.status(404).json({ error: "Not found" }); - - const simulate = process.env.OPENCLAW_SIMULATE !== "false"; - const openclawUrl = - process.env.OPENCLAW_BASE_URL || "http://localhost:3100"; - - const logEntry = whatsappLog.find((w) => w.caseId === item.caseId); - const now = new Date().toISOString(); - - if (simulate) { - // Simulate mode — mark as sent without calling OpenClaw - item.whatsappStatus = "sent"; - item.whatsappSentAt = now; - item.status = "whatsapp_sent"; - item.currentStep = STATUS_STEP["whatsapp_sent"]; - item.updatedAt = now; - - if (logEntry) { - logEntry.status = "sent"; - logEntry.sentAt = now; +// ─── POST /requests/:id/send-whatsapp ───────────────────────────────────────── +router.post( + "/requests/:id/send-whatsapp", + async (req: Request, res: Response): Promise => { + const item = requests.find( + (r) => r.id === req.params.id || r.caseId === req.params.id + ); + if (!item) { + res.status(404).json({ error: "Not found" }); + return; } - return res.json({ - success: true, - message: "WhatsApp sent (simulated)", - simulated: true, - sentAt: now, - }); + const simulate = process.env.OPENCLAW_SIMULATE !== "false"; + const openclawUrl = process.env.OPENCLAW_BASE_URL || "http://localhost:3100"; + const logEntry = whatsappLog.find((w) => w.caseId === item.caseId); + const now = new Date().toISOString(); + + if (simulate) { + item.whatsappStatus = "sent"; + item.whatsappSentAt = now; + item.status = "whatsapp_sent"; + item.currentStep = STATUS_STEP["whatsapp_sent"]; + item.updatedAt = now; + if (logEntry) { + logEntry.status = "sent"; + logEntry.sentAt = now; + } + res.json({ + success: true, + message: "WhatsApp sent (simulated)", + simulated: true, + sentAt: now, + }); + return; + } + + // Live mode — call OpenClaw + try { + const donor = donors.find((d) => d.id === item.donorId); + const payload = { + to: donor?.phone || "", + message: logEntry?.whatsappMessage || "", + caseId: item.caseId, + }; + + const response = await fetch(`${openclawUrl}/api/whatsapp/send`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`OpenClaw returned ${response.status}`); + } + + item.whatsappStatus = "sent"; + item.whatsappSentAt = now; + item.status = "whatsapp_sent"; + item.currentStep = STATUS_STEP["whatsapp_sent"]; + item.updatedAt = now; + if (logEntry) { + logEntry.status = "sent"; + logEntry.sentAt = now; + } + res.json({ + success: true, + message: "WhatsApp sent via OpenClaw", + simulated: false, + sentAt: now, + }); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Unknown error"; + item.whatsappStatus = "failed"; + item.updatedAt = now; + if (logEntry) { + logEntry.status = "failed"; + } + res.json({ + success: false, + message: `Failed to send via OpenClaw: ${message}`, + simulated: false, + sentAt: null, + }); + } } +); - // Live mode — call OpenClaw - try { - const donor = donors.find((d) => d.id === item.donorId); - const payload = { - to: donor?.phone || "", - message: logEntry?.whatsappMessage || "", - caseId: item.caseId, - }; - - const response = await fetch(`${openclawUrl}/api/whatsapp/send`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error(`OpenClaw returned ${response.status}`); - } - - item.whatsappStatus = "sent"; - item.whatsappSentAt = now; - item.status = "whatsapp_sent"; - item.currentStep = STATUS_STEP["whatsapp_sent"]; - item.updatedAt = now; - - if (logEntry) { - logEntry.status = "sent"; - logEntry.sentAt = now; - } - - return res.json({ - success: true, - message: "WhatsApp sent via OpenClaw", - simulated: false, - sentAt: now, - }); - } catch (err: any) { - item.whatsappStatus = "failed"; - item.updatedAt = now; - - if (logEntry) { - logEntry.status = "failed"; - } - - return res.json({ - success: false, - message: `Failed to send via OpenClaw: ${err.message}`, - simulated: false, - sentAt: null, - }); - } -}); - -// ─── POST /requests/:id/close ──────────────────────────────────────────────── -router.post("/requests/:id/close", (req, res) => { +// ─── POST /requests/:id/close ───────────────────────────────────────────────── +router.post("/requests/:id/close", (req: Request, res: Response): void => { findAndUpdate( req.params.id, (r) => { @@ -342,12 +367,15 @@ router.post("/requests/:id/close", (req, res) => { ); }); -// ─── POST /requests/:id/reject ─────────────────────────────────────────────── -router.post("/requests/:id/reject", (req, res) => { +// ─── POST /requests/:id/reject ──────────────────────────────────────────────── +router.post("/requests/:id/reject", (req: Request, res: Response): void => { const item = requests.find( (r) => r.id === req.params.id || r.caseId === req.params.id ); - if (!item) return res.status(404).json({ error: "Not found" }); + if (!item) { + res.status(404).json({ error: "Not found" }); + return; + } item.status = "rejected"; item.currentStep = STATUS_STEP["rejected"]; diff --git a/artifacts/ehsan-poc/src/lib/i18n/translations.ts b/artifacts/ehsan-poc/src/lib/i18n/translations.ts index 53161ef..2678aef 100644 --- a/artifacts/ehsan-poc/src/lib/i18n/translations.ts +++ b/artifacts/ehsan-poc/src/lib/i18n/translations.ts @@ -15,6 +15,16 @@ export const en = { back: "Back", confirm: "Confirm", language: "العربية", + trackCase: "Track Case", + notFound: "Case not found.", + noData: "No data available.", + currentStep: "Current Step", + pleaseWait: "Please wait...", + search: "Search", + searchPlaceholder: "Search by name, case ID, or description...", + allOpportunities: "All Opportunities", + featuredCases: "Featured Cases", + donate: "Donate", }, home: { heroTitle: "Closed Donation Loop POC", @@ -22,8 +32,13 @@ export const en = { totalRequests: "Total Requests", totalCollected: "Total Collected", totalClosed: "Closed Cases", - viewOpportunities: "View Opportunities", + viewOpportunities: "View All Opportunities", workflowTitle: "Closed Donation Loop Workflow", + searchOpportunities: "Search Donation Opportunities", + searchLabel: "Find a cause to support", + searchButton: "Search", + featuredTitle: "Featured Opportunities", + noResults: "No opportunities match your search.", }, workflow: { step1: "Request Submitted", @@ -85,22 +100,26 @@ export const en = { collected: "Collected", remaining: "Remaining", target: "Target", + noOpportunities: "No opportunities are available right now.", + verified: "Verified", }, donate: { title: "Complete Donation", + caseSummary: "Case Summary", donorName: "Donor Name", donorPhone: "Phone Number", donorEmail: "Email (Optional)", amount: "Donation Amount", confirmDonation: "Confirm Donation", successMessage: "Thank you for your donation. May Allah reward you.", + caseNotFound: "Case not found or no longer available.", }, admin: { title: "Admin Dashboard", caseId: "Case ID", beneficiary: "Beneficiary", status: "Status", - currentStep: "Current Step", + currentStep: "Step", actions: "Actions", verify: "Verify", publish: "Publish", @@ -109,27 +128,41 @@ export const en = { close: "Close Case", reject: "Reject", rejectionReason: "Rejection Reason", + track: "Track", + whatsapp: "WhatsApp", + noRequests: "No requests found.", + needType: "Need Type", + amount: "Amount", }, track: { title: "Track Case", caseTimeline: "Case Timeline", + caseInfo: "Case Information", + rejected: "Case Rejected", + currentStepLabel: "Current", + submitThankYou: "Submit Thank-You Message", }, thankYou: { title: "Submit Thank You Message", message: "Thank You Message", submitLabel: "Send Message to Donor", + successNote: "Your thank-you message will be sent to the donor via WhatsApp through OpenClaw.", + beneficiaryMessageLabel: "Beneficiary Message", }, whatsapp: { title: "WhatsApp Log", donor: "Donor", - message: "Message", + donorPhone: "Phone", + message: "WhatsApp Message", + beneficiaryMessage: "Beneficiary Message", status: "Status", sentAt: "Sent At", sendViaOpenClaw: "Send via OpenClaw", pending: "Pending", sent: "Sent", failed: "Failed", - } + noEntries: "No WhatsApp log entries yet.", + }, }; export const ar = { @@ -149,6 +182,16 @@ export const ar = { back: "رجوع", confirm: "تأكيد", language: "English", + trackCase: "تتبع الحالة", + notFound: "الحالة غير موجودة.", + noData: "لا توجد بيانات.", + currentStep: "الخطوة الحالية", + pleaseWait: "يرجى الانتظار...", + search: "بحث", + searchPlaceholder: "ابحث بالاسم أو رقم الحالة أو الوصف...", + allOpportunities: "جميع الفرص", + featuredCases: "الحالات المميزة", + donate: "تبرع", }, home: { heroTitle: "إقفال دورة التبرع", @@ -156,8 +199,13 @@ export const ar = { totalRequests: "إجمالي الطلبات", totalCollected: "إجمالي التبرعات (ريال)", totalClosed: "الحالات المغلقة", - viewOpportunities: "استعراض الفرص", + viewOpportunities: "عرض جميع الفرص", workflowTitle: "خطوات إقفال دورة التبرع", + searchOpportunities: "ابحث في فرص التبرع", + searchLabel: "ابحث عن قضية لدعمها", + searchButton: "بحث", + featuredTitle: "الفرص المميزة", + noResults: "لا توجد فرص تطابق بحثك.", }, workflow: { step1: "مقدم الطلب", @@ -219,22 +267,26 @@ export const ar = { collected: "المجموع", remaining: "المتبقي", target: "الهدف", + noOpportunities: "لا توجد فرص متاحة حالياً.", + verified: "موثق", }, donate: { title: "إتمام التبرع", + caseSummary: "ملخص الحالة", donorName: "اسم المتبرع", donorPhone: "رقم الجوال", donorEmail: "البريد الإلكتروني (اختياري)", amount: "مبلغ التبرع", confirmDonation: "تأكيد التبرع", successMessage: "شكراً لتبرعك. جزاك الله خيراً.", + caseNotFound: "الحالة غير موجودة أو لم تعد متاحة.", }, admin: { title: "لوحة الإدارة", caseId: "رقم الحالة", beneficiary: "المستفيد", status: "الحالة", - currentStep: "الخطوة الحالية", + currentStep: "الخطوة", actions: "الإجراءات", verify: "توثيق", publish: "نشر", @@ -243,25 +295,39 @@ export const ar = { close: "إغلاق الحالة", reject: "رفض", rejectionReason: "سبب الرفض", + track: "تتبع", + whatsapp: "واتساب", + noRequests: "لا توجد طلبات.", + needType: "نوع الاحتياج", + amount: "المبلغ", }, track: { title: "تتبع الحالة", caseTimeline: "مسار الحالة", + caseInfo: "معلومات الحالة", + rejected: "تم رفض الحالة", + currentStepLabel: "الحالية", + submitThankYou: "تقديم رسالة الشكر", }, thankYou: { title: "تقديم رسالة الشكر", message: "رسالة الشكر", submitLabel: "إرسال الرسالة للمتبرع", + successNote: "سيتم إرسال رسالة شكرك إلى المتبرع عبر واتساب من خلال OpenClaw.", + beneficiaryMessageLabel: "رسالة المستفيد", }, whatsapp: { title: "سجل رسائل الواتساب", donor: "المتبرع", - message: "الرسالة", + donorPhone: "الجوال", + message: "رسالة الواتساب", + beneficiaryMessage: "رسالة المستفيد", status: "الحالة", sentAt: "وقت الإرسال", sendViaOpenClaw: "إرسال عبر OpenClaw", pending: "قيد الانتظار", sent: "مرسل", failed: "فشل", - } + noEntries: "لا توجد سجلات واتساب بعد.", + }, }; diff --git a/artifacts/ehsan-poc/src/pages/admin.tsx b/artifacts/ehsan-poc/src/pages/admin.tsx index 8547feb..4875cac 100644 --- a/artifacts/ehsan-poc/src/pages/admin.tsx +++ b/artifacts/ehsan-poc/src/pages/admin.tsx @@ -1,7 +1,19 @@ import { useLanguage } from "../contexts/LanguageContext"; -import { useListRequests, getListRequestsQueryKey, useVerifyRequest, usePublishRequest, useDeliverSupport, useConfirmReceipt, useCloseRequest, useRejectRequest } from "@workspace/api-client-react"; +import { + useListRequests, + getListRequestsQueryKey, + useVerifyRequest, + usePublishRequest, + useDeliverSupport, + useConfirmReceipt, + useCloseRequest, + useRejectRequest, +} from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Table, TableBody, TableCell, TableHead, + TableHeader, TableRow, +} from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Link } from "wouter"; @@ -10,7 +22,7 @@ export default function Admin() { const { t } = useLanguage(); const queryClient = useQueryClient(); const { data: requests, isLoading } = useListRequests(); - + const verifyRequest = useVerifyRequest(); const publishRequest = usePublishRequest(); const deliverSupport = useDeliverSupport(); @@ -30,15 +42,15 @@ export default function Admin() { return (

{t.admin.title}

- +
{t.admin.caseId} {t.admin.beneficiary} - {t.request.needType} - {t.request.amount} + {t.admin.needType} + {t.admin.amount} {t.admin.status} {t.admin.currentStep} {t.admin.actions} @@ -55,8 +67,10 @@ export default function Admin() { {req.caseId} {req.beneficiaryName} - {t.needTypes[req.needType as keyof typeof t.needTypes] || req.needType} - {req.requestedAmount} ﷼ + + {t.needTypes[req.needType as keyof typeof t.needTypes] || req.needType} + + {req.requestedAmount.toLocaleString()} ﷼ {t.statuses[req.status as keyof typeof t.statuses] || req.status} @@ -64,33 +78,61 @@ export default function Admin() { {req.currentStep}/10 -
+
- + - - {req.status === 'new' && ( + {req.status === "new" && ( <> - - + + )} - {req.status === 'verified' && ( - + {req.status === "pending_review" && ( + <> + + + )} - {req.status === 'donated' && ( - + {req.status === "verified" && ( + )} - {req.status === 'delivered' && ( - + {req.status === "donated" && ( + )} - {req.status === 'thank_you_submitted' && ( - - + {req.status === "delivered" && ( + + )} + {req.status === "receipt_confirmed" && ( + + )} - {req.status === 'whatsapp_sent' && ( - + {req.status === "thank_you_submitted" && ( + + + + )} + {req.status === "whatsapp_sent" && ( + )}
@@ -99,7 +141,7 @@ export default function Admin() { {(!requests || requests.length === 0) && !isLoading && ( - No requests found + {t.admin.noRequests} )} @@ -108,4 +150,4 @@ export default function Admin() {
); -} \ No newline at end of file +} diff --git a/artifacts/ehsan-poc/src/pages/donate.tsx b/artifacts/ehsan-poc/src/pages/donate.tsx index 7b22693..b810acc 100644 --- a/artifacts/ehsan-poc/src/pages/donate.tsx +++ b/artifacts/ehsan-poc/src/pages/donate.tsx @@ -4,7 +4,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useParams, useLocation } from "wouter"; import { useLanguage } from "../contexts/LanguageContext"; -import { useGetRequest, useDonateToRequest, getListRequestsQueryKey, getListPublishedRequestsQueryKey, getGetRequestQueryKey } from "@workspace/api-client-react"; +import { + useGetRequest, useDonateToRequest, + getListRequestsQueryKey, getListPublishedRequestsQueryKey, getGetRequestQueryKey, +} from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; @@ -14,6 +17,7 @@ import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { CheckCircle, Heart } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; +import { Link } from "wouter"; const schema = z.object({ donorName: z.string().min(2), @@ -43,7 +47,7 @@ export default function Donate() { donorName: "", donorPhone: "", donorEmail: "", - amount: request?.requestedAmount || 0, + amount: 0, }, }); @@ -80,7 +84,7 @@ export default function Donate() { if (!request) { return (
- Case not found. + {t.donate.caseNotFound}
); } @@ -94,10 +98,16 @@ export default function Donate() {

{t.common.success}

{t.donate.successMessage}

-

{request.caseId}

+

+ {request.caseId} +

- - + +
@@ -105,7 +115,12 @@ export default function Donate() { ); } - const progress = Math.min(100, Math.round((request.collectedAmount / request.requestedAmount) * 100)); + const progress = Math.min( + 100, + request.requestedAmount > 0 + ? Math.round((request.collectedAmount / request.requestedAmount) * 100) + : 0 + ); return (
@@ -115,7 +130,12 @@ export default function Donate() { {/* Case Summary */} - + + + {t.donate.caseSummary} + + +

{request.description}

@@ -126,8 +146,14 @@ export default function Donate() {
- {t.opportunities.collected}: {request.collectedAmount} ﷼ - {t.opportunities.target}: {request.requestedAmount} ﷼ + + {t.opportunities.collected}:{" "} + {request.collectedAmount.toLocaleString()} ﷼ + + + {t.opportunities.target}:{" "} + {request.requestedAmount.toLocaleString()} ﷼ +
@@ -205,6 +231,12 @@ export default function Donate() { + +
+ + + +
); } diff --git a/artifacts/ehsan-poc/src/pages/home.tsx b/artifacts/ehsan-poc/src/pages/home.tsx index a82afa5..3bfcb97 100644 --- a/artifacts/ehsan-poc/src/pages/home.tsx +++ b/artifacts/ehsan-poc/src/pages/home.tsx @@ -1,29 +1,65 @@ +import { useState } from "react"; import { useLanguage } from "../contexts/LanguageContext"; -import { useGetStats } from "@workspace/api-client-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useGetStats, useListPublishedRequests } from "@workspace/api-client-react"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Link } from "wouter"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; import { Skeleton } from "@/components/ui/skeleton"; +import { Link } from "wouter"; +import { Search } from "lucide-react"; export default function Home() { const { t } = useLanguage(); - const { data: stats, isLoading } = useGetStats(); + const { data: stats, isLoading: statsLoading } = useGetStats(); + const { data: published, isLoading: pubLoading } = useListPublishedRequests(); + const [query, setQuery] = useState(""); + + const filtered = (published || []).filter((r) => { + if (!query.trim()) return true; + const q = query.toLowerCase(); + return ( + r.caseId.toLowerCase().includes(q) || + r.description.toLowerCase().includes(q) || + r.beneficiaryName.toLowerCase().includes(q) + ); + }); return (
-
+ {/* Hero */} +

{t.home.heroTitle}

{t.home.heroSubtitle}

- - {t.home.viewOpportunities} - + + {/* Search Bar */} +
+ +
+
+ + setQuery(e.target.value)} + data-testid="input-search" + /> +
+ +
+
- {isLoading ? ( + {/* Stats */} + {statsLoading ? (
@@ -38,7 +74,9 @@ export default function Home() { -
{stats?.totalRequests || 0}
+
+ {stats?.totalRequests || 0} +
@@ -48,7 +86,9 @@ export default function Home() { -
{stats?.totalCollected?.toLocaleString() || 0} ﷼
+
+ {stats?.totalCollected?.toLocaleString() || 0} ﷼ +
@@ -58,19 +98,95 @@ export default function Home() { -
{stats?.totalClosed || 0}
+
+ {stats?.totalClosed || 0} +
)} + {/* Featured Opportunities */} +
+
+

{t.home.featuredTitle}

+ + + +
+ + {pubLoading ? ( +
+ {[1, 2, 3].map((i) => )} +
+ ) : filtered.length === 0 ? ( +
+ {query.trim() ? t.home.noResults : t.opportunities.noOpportunities} +
+ ) : ( +
+ {filtered.slice(0, 6).map((request) => { + const progress = Math.min( + 100, + request.requestedAmount > 0 + ? Math.round((request.collectedAmount / request.requestedAmount) * 100) + : 0 + ); + const remaining = Math.max(0, request.requestedAmount - request.collectedAmount); + return ( + + +
+ + {t.needTypes[request.needType as keyof typeof t.needTypes] || request.needType} + + + {t.opportunities.verified} + +
+ {request.description} +

{request.caseId}

+
+ +
+ + {t.opportunities.collected}:{" "} + {request.collectedAmount.toLocaleString()} ﷼ + + + {t.opportunities.target}:{" "} + {request.requestedAmount.toLocaleString()} ﷼ + +
+ +
{progress}%
+
+ {t.opportunities.remaining}: + {remaining.toLocaleString()} ﷼ +
+
+ + + + + +
+ ); + })} +
+ )} +
+ + {/* Workflow Steps */}

{t.home.workflowTitle}

{Array.from({ length: 10 }).map((_, i) => ( -
+
{i + 1}
@@ -83,4 +199,4 @@ export default function Home() {
); -} \ No newline at end of file +} diff --git a/artifacts/ehsan-poc/src/pages/opportunities.tsx b/artifacts/ehsan-poc/src/pages/opportunities.tsx index d53986b..c660f3d 100644 --- a/artifacts/ehsan-poc/src/pages/opportunities.tsx +++ b/artifacts/ehsan-poc/src/pages/opportunities.tsx @@ -1,32 +1,88 @@ +import { useState } from "react"; import { useLanguage } from "../contexts/LanguageContext"; -import { useListPublishedRequests, getListPublishedRequestsQueryKey } from "@workspace/api-client-react"; +import { useListPublishedRequests } from "@workspace/api-client-react"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Link } from "wouter"; import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { Skeleton } from "@/components/ui/skeleton"; +import { Link } from "wouter"; + +type NeedTypeKey = + | "electricity" | "water" | "food" | "health" + | "housing" | "refrigerator" | "air_conditioner" | "court_order"; + +const NEED_TYPES: NeedTypeKey[] = [ + "electricity", "water", "food", "health", + "housing", "refrigerator", "air_conditioner", "court_order", +]; export default function Opportunities() { const { t } = useLanguage(); const { data: requests, isLoading } = useListPublishedRequests(); + const [activeFilter, setActiveFilter] = useState("all"); + + const filtered = (requests || []).filter((r) => + activeFilter === "all" ? true : r.needType === activeFilter + ); return (
-

{t.opportunities.title}

- +

{t.opportunities.title}

+ + {/* Filter Bar */} +
+

{t.opportunities.filterByType}

+
+ + {NEED_TYPES.map((type) => ( + + ))} +
+
+ {isLoading ? (
{[1, 2, 3].map((i) => ( ))}
+ ) : filtered.length === 0 ? ( +
+ {t.opportunities.noOpportunities} +
) : (
- {requests?.map((request) => { - const progress = Math.min(100, Math.round((request.collectedAmount / request.requestedAmount) * 100)); - const remaining = request.requestedAmount - request.collectedAmount; - + {filtered.map((request) => { + const progress = Math.min( + 100, + request.requestedAmount > 0 + ? Math.round((request.collectedAmount / request.requestedAmount) * 100) + : 0 + ); + const remaining = Math.max(0, request.requestedAmount - request.collectedAmount); + return ( @@ -34,24 +90,29 @@ export default function Opportunities() { {t.needTypes[request.needType as keyof typeof t.needTypes] || request.needType} - - {t.statuses.verified} + + {t.opportunities.verified}
- {request.description} + {request.description} +

{request.caseId}

- +
- {t.opportunities.collected}: {request.collectedAmount} ﷼ - {t.opportunities.target}: {request.requestedAmount} ﷼ + + {t.opportunities.collected}:{" "} + {request.collectedAmount.toLocaleString()} ﷼ + + + {t.opportunities.target}:{" "} + {request.requestedAmount.toLocaleString()} ﷼ +
-
- {progress}% -
-
+
{progress}%
+
{t.opportunities.remaining}: - {remaining > 0 ? remaining : 0} ﷼ + {remaining.toLocaleString()} ﷼
@@ -64,14 +125,8 @@ export default function Opportunities() { ); })} - - {(!requests || requests.length === 0) && ( -
- No opportunities available right now. -
- )}
)}
); -} \ No newline at end of file +} diff --git a/artifacts/ehsan-poc/src/pages/thank-you.tsx b/artifacts/ehsan-poc/src/pages/thank-you.tsx index 2d984f3..c801784 100644 --- a/artifacts/ehsan-poc/src/pages/thank-you.tsx +++ b/artifacts/ehsan-poc/src/pages/thank-you.tsx @@ -4,8 +4,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useParams, useLocation } from "wouter"; import { useLanguage } from "../contexts/LanguageContext"; -import { useGetRequest, useSubmitThankYou, getListRequestsQueryKey, getGetRequestQueryKey } from "@workspace/api-client-react"; - +import { + useGetRequest, useSubmitThankYou, + getListRequestsQueryKey, getGetRequestQueryKey, +} from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; @@ -14,6 +16,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CheckCircle, Heart } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; +import { Link } from "wouter"; const schema = z.object({ beneficiaryName: z.string().min(2), @@ -38,11 +41,14 @@ export default function ThankYou() { const form = useForm({ resolver: zodResolver(schema), defaultValues: { - beneficiaryName: request?.beneficiaryName || "", + beneficiaryName: "", message: "", }, }); + // Pre-fill name once loaded + const beneficiaryName = request?.beneficiaryName || ""; + const onSubmit = (data: FormData) => { submitThankYou.mutate( { @@ -68,6 +74,14 @@ export default function ThankYou() { ); } + if (!request) { + return ( +
+ {t.common.notFound} +
+ ); + } + if (submitted) { return (
@@ -78,15 +92,15 @@ export default function ThankYou() {

{t.common.success}

-

- {t.thankYou.title} -

-

- Your thank-you message will be sent to the donor via WhatsApp through OpenClaw. -

+

{t.thankYou.title}

+

{t.thankYou.successNote}

- - + +
@@ -98,9 +112,9 @@ export default function ThankYou() {

{t.thankYou.title}

- {request && ( -

{request.caseId} — {request.beneficiaryName}

- )} +

+ {request.caseId} — {request.beneficiaryName} +

@@ -119,8 +133,9 @@ export default function ThankYou() { @@ -137,7 +152,6 @@ export default function ThankYou() {