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() {