From 12111a9562c09e2e255fd48464fc16908784ef86 Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Fri, 5 Jun 2026 17:05:27 +0000 Subject: [PATCH] =?UTF-8?q?Build=20EHSAN=20Closed=20Donation=20Loop=20POC?= =?UTF-8?q?=20=E2=80=94=20full=20bilingual=20Arabic/English=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend (api-server): Complete in-memory mock DB with 11 seed cases, 5 eligible beneficiaries, 3 donors, and WhatsApp log. All 14 API routes implemented across requests, donors, stats, and whatsapp-log. OpenClaw integration with OPENCLAW_SIMULATE toggle. UUID-based IDs. Full status machine (new → closed, 10 steps). - Frontend (ehsan-poc): 8 pages fully implemented using all generated API hooks: Home (stats counters, 10-step workflow diagram), Request (form with eligibility result), Opportunities (card grid with progress bars), Donate (case summary + donor form), Admin (full data table with contextual action buttons), Track (10-step visual timeline in green), ThankYou (message form), WhatsApp Log (WhatsApp bubble preview + OpenClaw send button). - Bilingual LanguageContext (AR/EN) with RTL/LTR toggle, localStorage persistence. EHSAN green palette (HSL 143), Tajawal font, fully responsive. TypeScript clean — zero errors. --- .replit | 4 + artifacts/api-server/package.json | 4 +- artifacts/api-server/src/lib/mockDb.ts | 442 ++++++ artifacts/api-server/src/routes/donors.ts | 10 + artifacts/api-server/src/routes/index.ts | 10 +- artifacts/api-server/src/routes/requests.ts | 359 +++++ artifacts/api-server/src/routes/stats.ts | 53 + .../api-server/src/routes/whatsappLog.ts | 10 + .../ehsan-poc/.replit-artifact/artifact.toml | 31 + artifacts/ehsan-poc/components.json | 20 + artifacts/ehsan-poc/index.html | 24 + artifacts/ehsan-poc/package.json | 77 + artifacts/ehsan-poc/public/favicon.svg | 3 + artifacts/ehsan-poc/public/opengraph.jpg | Bin 0 -> 38142 bytes artifacts/ehsan-poc/public/robots.txt | 2 + artifacts/ehsan-poc/src/App.tsx | 54 + .../src/components/layout/AppLayout.tsx | 18 + .../src/components/layout/Header.tsx | 57 + .../ehsan-poc/src/components/ui/accordion.tsx | 55 + .../src/components/ui/alert-dialog.tsx | 139 ++ .../ehsan-poc/src/components/ui/alert.tsx | 59 + .../src/components/ui/aspect-ratio.tsx | 5 + .../ehsan-poc/src/components/ui/avatar.tsx | 50 + .../ehsan-poc/src/components/ui/badge.tsx | 43 + .../src/components/ui/breadcrumb.tsx | 115 ++ .../src/components/ui/button-group.tsx | 83 + .../ehsan-poc/src/components/ui/button.tsx | 65 + .../ehsan-poc/src/components/ui/calendar.tsx | 213 +++ .../ehsan-poc/src/components/ui/card.tsx | 76 + .../ehsan-poc/src/components/ui/carousel.tsx | 260 +++ .../ehsan-poc/src/components/ui/chart.tsx | 367 +++++ .../ehsan-poc/src/components/ui/checkbox.tsx | 28 + .../src/components/ui/collapsible.tsx | 11 + .../ehsan-poc/src/components/ui/command.tsx | 153 ++ .../src/components/ui/context-menu.tsx | 198 +++ .../ehsan-poc/src/components/ui/dialog.tsx | 120 ++ .../ehsan-poc/src/components/ui/drawer.tsx | 116 ++ .../src/components/ui/dropdown-menu.tsx | 201 +++ .../ehsan-poc/src/components/ui/empty.tsx | 104 ++ .../ehsan-poc/src/components/ui/field.tsx | 244 +++ .../ehsan-poc/src/components/ui/form.tsx | 176 +++ .../src/components/ui/hover-card.tsx | 27 + .../src/components/ui/input-group.tsx | 168 ++ .../ehsan-poc/src/components/ui/input-otp.tsx | 69 + .../ehsan-poc/src/components/ui/input.tsx | 22 + .../ehsan-poc/src/components/ui/item.tsx | 193 +++ artifacts/ehsan-poc/src/components/ui/kbd.tsx | 28 + .../ehsan-poc/src/components/ui/label.tsx | 26 + .../ehsan-poc/src/components/ui/menubar.tsx | 254 +++ .../src/components/ui/navigation-menu.tsx | 128 ++ .../src/components/ui/pagination.tsx | 117 ++ .../ehsan-poc/src/components/ui/popover.tsx | 31 + .../ehsan-poc/src/components/ui/progress.tsx | 28 + .../src/components/ui/radio-group.tsx | 42 + .../ehsan-poc/src/components/ui/resizable.tsx | 45 + .../src/components/ui/scroll-area.tsx | 46 + .../ehsan-poc/src/components/ui/select.tsx | 159 ++ .../ehsan-poc/src/components/ui/separator.tsx | 29 + .../ehsan-poc/src/components/ui/sheet.tsx | 140 ++ .../ehsan-poc/src/components/ui/sidebar.tsx | 727 +++++++++ .../ehsan-poc/src/components/ui/skeleton.tsx | 15 + .../ehsan-poc/src/components/ui/slider.tsx | 26 + .../ehsan-poc/src/components/ui/sonner.tsx | 31 + .../ehsan-poc/src/components/ui/spinner.tsx | 16 + .../ehsan-poc/src/components/ui/switch.tsx | 27 + .../ehsan-poc/src/components/ui/table.tsx | 120 ++ .../ehsan-poc/src/components/ui/tabs.tsx | 53 + .../ehsan-poc/src/components/ui/textarea.tsx | 22 + .../ehsan-poc/src/components/ui/toast.tsx | 127 ++ .../ehsan-poc/src/components/ui/toaster.tsx | 33 + .../src/components/ui/toggle-group.tsx | 61 + .../ehsan-poc/src/components/ui/toggle.tsx | 43 + .../ehsan-poc/src/components/ui/tooltip.tsx | 32 + .../src/contexts/LanguageContext.tsx | 48 + artifacts/ehsan-poc/src/hooks/use-mobile.tsx | 19 + artifacts/ehsan-poc/src/hooks/use-toast.ts | 191 +++ artifacts/ehsan-poc/src/index.css | 291 ++++ .../ehsan-poc/src/lib/i18n/translations.ts | 267 ++++ artifacts/ehsan-poc/src/lib/utils.ts | 6 + artifacts/ehsan-poc/src/main.tsx | 5 + artifacts/ehsan-poc/src/pages/admin.tsx | 111 ++ artifacts/ehsan-poc/src/pages/donate.tsx | 210 +++ artifacts/ehsan-poc/src/pages/home.tsx | 86 + artifacts/ehsan-poc/src/pages/not-found.tsx | 21 + .../ehsan-poc/src/pages/opportunities.tsx | 77 + artifacts/ehsan-poc/src/pages/request.tsx | 268 ++++ artifacts/ehsan-poc/src/pages/thank-you.tsx | 162 ++ artifacts/ehsan-poc/src/pages/track.tsx | 193 +++ .../ehsan-poc/src/pages/whatsapp-log.tsx | 152 ++ artifacts/ehsan-poc/tsconfig.json | 24 + artifacts/ehsan-poc/vite.config.ts | 75 + .../src/generated/api.schemas.ts | 205 ++- lib/api-client-react/src/generated/api.ts | 1402 ++++++++++++++++- lib/api-spec/openapi.yaml | 537 ++++++- lib/api-zod/src/generated/api.ts | 493 +++++- .../src/generated/types/donationInput.ts | 15 + .../src/generated/types/donationRequest.ts | 41 + .../generated/types/donationRequestInput.ts | 20 + .../types/donationRequestInputNeedType.ts | 21 + .../types/donationRequestInputSource.ts | 16 + .../types/donationRequestNeedType.ts | 21 + .../generated/types/donationRequestSource.ts | 16 + .../generated/types/donationRequestStatus.ts | 24 + .../types/donationRequestWhatsappStatus.ts | 19 + lib/api-zod/src/generated/types/donor.ts | 17 + .../src/generated/types/healthStatus.ts | 4 +- lib/api-zod/src/generated/types/index.ts | 25 +- .../src/generated/types/listRequestsParams.ts | 12 + .../src/generated/types/needTypeCount.ts | 13 + .../src/generated/types/rejectInput.ts | 11 + lib/api-zod/src/generated/types/stats.ts | 20 + .../src/generated/types/statusCount.ts | 12 + .../src/generated/types/thankYouInput.ts | 12 + .../src/generated/types/whatsappLogEntry.ts | 21 + .../generated/types/whatsappLogEntryStatus.ts | 16 + .../src/generated/types/whatsappResult.ts | 15 + pnpm-lock.yaml | 289 +++- 117 files changed, 12366 insertions(+), 81 deletions(-) create mode 100644 artifacts/api-server/src/lib/mockDb.ts create mode 100644 artifacts/api-server/src/routes/donors.ts create mode 100644 artifacts/api-server/src/routes/requests.ts create mode 100644 artifacts/api-server/src/routes/stats.ts create mode 100644 artifacts/api-server/src/routes/whatsappLog.ts create mode 100644 artifacts/ehsan-poc/.replit-artifact/artifact.toml create mode 100644 artifacts/ehsan-poc/components.json create mode 100644 artifacts/ehsan-poc/index.html create mode 100644 artifacts/ehsan-poc/package.json create mode 100644 artifacts/ehsan-poc/public/favicon.svg create mode 100644 artifacts/ehsan-poc/public/opengraph.jpg create mode 100644 artifacts/ehsan-poc/public/robots.txt create mode 100644 artifacts/ehsan-poc/src/App.tsx create mode 100644 artifacts/ehsan-poc/src/components/layout/AppLayout.tsx create mode 100644 artifacts/ehsan-poc/src/components/layout/Header.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/accordion.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/alert-dialog.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/alert.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/aspect-ratio.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/avatar.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/badge.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/breadcrumb.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/button-group.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/button.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/calendar.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/card.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/carousel.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/chart.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/checkbox.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/collapsible.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/command.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/context-menu.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/dialog.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/drawer.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/dropdown-menu.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/empty.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/field.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/form.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/hover-card.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/input-group.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/input-otp.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/input.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/item.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/kbd.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/label.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/menubar.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/navigation-menu.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/pagination.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/popover.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/progress.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/radio-group.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/resizable.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/scroll-area.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/select.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/separator.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/sheet.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/sidebar.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/skeleton.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/slider.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/sonner.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/spinner.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/switch.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/table.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/tabs.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/textarea.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/toast.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/toaster.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/toggle-group.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/toggle.tsx create mode 100644 artifacts/ehsan-poc/src/components/ui/tooltip.tsx create mode 100644 artifacts/ehsan-poc/src/contexts/LanguageContext.tsx create mode 100644 artifacts/ehsan-poc/src/hooks/use-mobile.tsx create mode 100644 artifacts/ehsan-poc/src/hooks/use-toast.ts create mode 100644 artifacts/ehsan-poc/src/index.css create mode 100644 artifacts/ehsan-poc/src/lib/i18n/translations.ts create mode 100644 artifacts/ehsan-poc/src/lib/utils.ts create mode 100644 artifacts/ehsan-poc/src/main.tsx create mode 100644 artifacts/ehsan-poc/src/pages/admin.tsx create mode 100644 artifacts/ehsan-poc/src/pages/donate.tsx create mode 100644 artifacts/ehsan-poc/src/pages/home.tsx create mode 100644 artifacts/ehsan-poc/src/pages/not-found.tsx create mode 100644 artifacts/ehsan-poc/src/pages/opportunities.tsx create mode 100644 artifacts/ehsan-poc/src/pages/request.tsx create mode 100644 artifacts/ehsan-poc/src/pages/thank-you.tsx create mode 100644 artifacts/ehsan-poc/src/pages/track.tsx create mode 100644 artifacts/ehsan-poc/src/pages/whatsapp-log.tsx create mode 100644 artifacts/ehsan-poc/tsconfig.json create mode 100644 artifacts/ehsan-poc/vite.config.ts create mode 100644 lib/api-zod/src/generated/types/donationInput.ts create mode 100644 lib/api-zod/src/generated/types/donationRequest.ts create mode 100644 lib/api-zod/src/generated/types/donationRequestInput.ts create mode 100644 lib/api-zod/src/generated/types/donationRequestInputNeedType.ts create mode 100644 lib/api-zod/src/generated/types/donationRequestInputSource.ts create mode 100644 lib/api-zod/src/generated/types/donationRequestNeedType.ts create mode 100644 lib/api-zod/src/generated/types/donationRequestSource.ts create mode 100644 lib/api-zod/src/generated/types/donationRequestStatus.ts create mode 100644 lib/api-zod/src/generated/types/donationRequestWhatsappStatus.ts create mode 100644 lib/api-zod/src/generated/types/donor.ts create mode 100644 lib/api-zod/src/generated/types/listRequestsParams.ts create mode 100644 lib/api-zod/src/generated/types/needTypeCount.ts create mode 100644 lib/api-zod/src/generated/types/rejectInput.ts create mode 100644 lib/api-zod/src/generated/types/stats.ts create mode 100644 lib/api-zod/src/generated/types/statusCount.ts create mode 100644 lib/api-zod/src/generated/types/thankYouInput.ts create mode 100644 lib/api-zod/src/generated/types/whatsappLogEntry.ts create mode 100644 lib/api-zod/src/generated/types/whatsappLogEntryStatus.ts create mode 100644 lib/api-zod/src/generated/types/whatsappResult.ts diff --git a/.replit b/.replit index df803ae..ade20b3 100644 --- a/.replit +++ b/.replit @@ -26,3 +26,7 @@ externalPort = 80 [[ports]] localPort = 8081 externalPort = 8081 + +[[ports]] +localPort = 18312 +externalPort = 3000 diff --git a/artifacts/api-server/package.json b/artifacts/api-server/package.json index 6916f27..d12a3b2 100644 --- a/artifacts/api-server/package.json +++ b/artifacts/api-server/package.json @@ -17,13 +17,15 @@ "drizzle-orm": "catalog:", "express": "^5.2.1", "pino": "^9.14.0", - "pino-http": "^10.5.0" + "pino-http": "^10.5.0", + "uuid": "^14.0.0" }, "devDependencies": { "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "catalog:", + "@types/uuid": "^11.0.0", "esbuild": "0.27.3", "esbuild-plugin-pino": "^2.3.3", "pino-pretty": "^13.1.3", diff --git a/artifacts/api-server/src/lib/mockDb.ts b/artifacts/api-server/src/lib/mockDb.ts new file mode 100644 index 0000000..5ab357e --- /dev/null +++ b/artifacts/api-server/src/lib/mockDb.ts @@ -0,0 +1,442 @@ +import { v4 as uuidv4 } from "uuid"; + +export type NeedType = + | "electricity" + | "water" + | "food" + | "health" + | "housing" + | "refrigerator" + | "air_conditioner" + | "court_order"; + +export type RequestSource = "beneficiary" | "charity" | "official"; + +export type RequestStatus = + | "new" + | "pending_review" + | "verified" + | "published" + | "donated" + | "delivered" + | "receipt_confirmed" + | "thank_you_submitted" + | "whatsapp_sent" + | "closed" + | "rejected"; + +export type WhatsappStatus = "pending" | "sent" | "failed"; + +export interface DonationRequest { + id: string; + caseId: string; + beneficiaryName: string; + nationalId: string; + phone: string; + source: RequestSource; + sourceName: string; + needType: NeedType; + requestedAmount: number; + collectedAmount: number; + description: string; + status: RequestStatus; + currentStep: number; + donorId: string | null; + donorName: string | null; + thankYouMessage: string | null; + whatsappStatus: WhatsappStatus | null; + whatsappSentAt: string | null; + rejectionReason: string | null; + createdAt: string; + updatedAt: string; +} + +export interface Donor { + id: string; + name: string; + phone: string; + email: string | null; + totalDonated: number; + donationCount: number; +} + +export interface EligibilityRecord { + nationalId: string; + eligible: boolean; +} + +export interface WhatsappLogEntry { + id: string; + caseId: string; + donorName: string; + donorPhone: string; + beneficiaryMessage: string; + whatsappMessage: string; + status: WhatsappStatus; + sentAt: string | null; + createdAt: string; +} + +// ─── Eligibility Database ─────────────────────────────────────────────────── +export const eligibilityDb: EligibilityRecord[] = [ + { nationalId: "1090512345", eligible: true }, + { nationalId: "1023456789", eligible: true }, + { nationalId: "2098765432", eligible: true }, + { nationalId: "1056789012", eligible: true }, + { nationalId: "2034567890", eligible: true }, + { nationalId: "1099999999", eligible: false }, +]; + +// ─── Donors ───────────────────────────────────────────────────────────────── +export const donors: Donor[] = [ + { + id: "donor-001", + name: "عبدالله المنصور", + phone: "0501234567", + email: "abdullah@example.com", + totalDonated: 5000, + donationCount: 2, + }, + { + id: "donor-002", + name: "سارة الأحمد", + phone: "0556789012", + email: "sara@example.com", + totalDonated: 3000, + donationCount: 1, + }, + { + id: "donor-003", + name: "محمد الشمري", + phone: "0589012345", + email: null, + totalDonated: 2500, + donationCount: 1, + }, +]; + +// ─── Mock Requests ─────────────────────────────────────────────────────────── +const now = new Date(); +const d = (daysAgo: number) => + new Date(now.getTime() - daysAgo * 86400000).toISOString(); + +export const requests: DonationRequest[] = [ + { + id: "req-001", + caseId: "CASE-001", + beneficiaryName: "أحمد إبراهيم الحربي", + nationalId: "1090512345", + phone: "0501111111", + source: "beneficiary", + sourceName: "مستفيد مباشر", + needType: "electricity", + requestedAmount: 2400, + collectedAmount: 2400, + description: "فاتورة كهرباء متراكمة لمدة 6 أشهر، أب لأربعة أطفال", + status: "closed", + currentStep: 10, + donorId: "donor-001", + donorName: "عبدالله المنصور", + thankYouMessage: "جزاكم الله خيراً، وصلني الدعم وكان له أثر كبير عليّ وعلى أسرتي.", + whatsappStatus: "sent", + whatsappSentAt: d(1), + rejectionReason: null, + createdAt: d(30), + updatedAt: d(1), + }, + { + id: "req-002", + caseId: "CASE-002", + beneficiaryName: "فاطمة علي السلمي", + nationalId: "1023456789", + phone: "0502222222", + source: "charity", + sourceName: "جمعية البر الخيرية", + needType: "water", + requestedAmount: 1800, + collectedAmount: 1800, + description: "فاتورة مياه متأخرة، أرملة تعيل ثلاثة أطفال", + status: "whatsapp_sent", + currentStep: 9, + donorId: "donor-002", + donorName: "سارة الأحمد", + thankYouMessage: "بارك الله فيكم، وصل الدعم في الوقت المناسب جداً.", + whatsappStatus: "sent", + whatsappSentAt: d(2), + rejectionReason: null, + createdAt: d(25), + updatedAt: d(2), + }, + { + id: "req-003", + caseId: "CASE-003", + beneficiaryName: "خالد محمد الغامدي", + nationalId: "2098765432", + phone: "0503333333", + source: "official", + sourceName: "وزارة العدل", + needType: "food", + requestedAmount: 1200, + collectedAmount: 1200, + description: "سلة غذائية شهرية لعائلة من 6 أفراد", + status: "thank_you_submitted", + currentStep: 8, + donorId: "donor-003", + donorName: "محمد الشمري", + thankYouMessage: "شكراً جزيلاً، السلة الغذائية أفادتنا كثيراً.", + whatsappStatus: "pending", + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(20), + updatedAt: d(3), + }, + { + id: "req-004", + caseId: "CASE-004", + beneficiaryName: "مريم سالم العتيبي", + nationalId: "1056789012", + phone: "0504444444", + source: "charity", + sourceName: "الهلال الأحمر السعودي", + needType: "health", + requestedAmount: 5000, + collectedAmount: 5000, + description: "مصاريف علاج ومستلزمات طبية لمريضة مزمنة", + status: "receipt_confirmed", + currentStep: 7, + donorId: "donor-001", + donorName: "عبدالله المنصور", + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(18), + updatedAt: d(4), + }, + { + id: "req-005", + caseId: "CASE-005", + beneficiaryName: "عمر عبدالرحمن الدوسري", + nationalId: "2034567890", + phone: "0505555555", + source: "official", + sourceName: "شركة المياه الوطنية", + needType: "housing", + requestedAmount: 8000, + collectedAmount: 8000, + description: "إصلاحات طارئة في المسكن بعد تسرب المياه", + status: "delivered", + currentStep: 6, + donorId: "donor-002", + donorName: "سارة الأحمد", + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(15), + updatedAt: d(5), + }, + { + id: "req-006", + caseId: "CASE-006", + beneficiaryName: "نورة سعد القحطاني", + nationalId: "1090512345", + phone: "0506666666", + source: "beneficiary", + sourceName: "مستفيد مباشر", + needType: "refrigerator", + requestedAmount: 1500, + collectedAmount: 1500, + description: "ثلاجة منزلية لعائلة تفتقر لوسيلة حفظ الغذاء", + status: "donated", + currentStep: 5, + donorId: "donor-003", + donorName: "محمد الشمري", + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(10), + updatedAt: d(6), + }, + { + id: "req-007", + caseId: "CASE-007", + beneficiaryName: "سليمان ناصر الزهراني", + nationalId: "1023456789", + phone: "0507777777", + source: "charity", + sourceName: "جمعية التنمية الأسرية", + needType: "air_conditioner", + requestedAmount: 2000, + collectedAmount: 0, + description: "مكيف لمنزل في منطقة حارة، وجود أطفال ومسنين", + status: "published", + currentStep: 4, + donorId: null, + donorName: null, + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(8), + updatedAt: d(7), + }, + { + id: "req-008", + caseId: "CASE-008", + beneficiaryName: "حسن عبدالله الرشيدي", + nationalId: "2056789012", + phone: "0508888888", + source: "official", + sourceName: "وزارة العدل", + needType: "court_order", + requestedAmount: 3500, + collectedAmount: 0, + description: "سداد غرامة قضائية لتجنب الحجز على الممتلكات", + status: "verified", + currentStep: 3, + donorId: null, + donorName: null, + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(6), + updatedAt: d(5), + }, + { + id: "req-009", + caseId: "CASE-009", + beneficiaryName: "رنا طارق المالكي", + nationalId: "9999999999", + phone: "0509999999", + source: "beneficiary", + sourceName: "مستفيد مباشر", + needType: "food", + requestedAmount: 900, + collectedAmount: 0, + description: "سلة غذائية لأسرة محتاجة", + status: "pending_review", + currentStep: 2, + donorId: null, + donorName: null, + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(2), + updatedAt: d(1), + }, + { + id: "req-010", + caseId: "CASE-010", + beneficiaryName: "بدر محمد الجهني", + nationalId: "1099999999", + phone: "0511111111", + source: "charity", + sourceName: "الجمعية الخيرية", + needType: "electricity", + requestedAmount: 1800, + collectedAmount: 0, + description: "فاتورة كهرباء متراكمة", + status: "rejected", + currentStep: 2, + donorId: null, + donorName: null, + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: "المستفيد غير مؤهل وفق قاعدة بيانات الاستحقاق", + createdAt: d(5), + updatedAt: d(4), + }, + { + id: "req-011", + caseId: "CASE-011", + beneficiaryName: "أميرة خالد السبيعي", + nationalId: "1056789012", + phone: "0512222222", + source: "official", + sourceName: "شركة الكهرباء السعودية", + needType: "electricity", + requestedAmount: 3200, + collectedAmount: 0, + description: "فاتورة كهرباء لمنزل أرملة ذات أطفال صغار", + status: "new", + currentStep: 1, + donorId: null, + donorName: null, + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: null, + createdAt: d(1), + updatedAt: d(0), + }, +]; + +// ─── WhatsApp Log ──────────────────────────────────────────────────────────── +export const whatsappLog: WhatsappLogEntry[] = [ + { + id: "wa-001", + caseId: "CASE-001", + donorName: "عبدالله المنصور", + donorPhone: "0501234567", + beneficiaryMessage: + "جزاكم الله خيراً، وصلني الدعم وكان له أثر كبير عليّ وعلى أسرتي.", + whatsappMessage: + "السلام عليكم، نشكركم على تبرعكم عبر منصة إحسان.\nتم إيصال الدعم للمستفيد، وهذه رسالة الشكر من المستفيد:\n\"جزاكم الله خيراً، وصلني الدعم وكان له أثر كبير عليّ وعلى أسرتي.\"\nرقم الحالة: CASE-001", + status: "sent", + sentAt: d(1), + createdAt: d(2), + }, + { + id: "wa-002", + caseId: "CASE-002", + donorName: "سارة الأحمد", + donorPhone: "0556789012", + beneficiaryMessage: "بارك الله فيكم، وصل الدعم في الوقت المناسب جداً.", + whatsappMessage: + "السلام عليكم، نشكركم على تبرعكم عبر منصة إحسان.\nتم إيصال الدعم للمستفيد، وهذه رسالة الشكر من المستفيد:\n\"بارك الله فيكم، وصل الدعم في الوقت المناسب جداً.\"\nرقم الحالة: CASE-002", + status: "sent", + sentAt: d(2), + createdAt: d(3), + }, + { + id: "wa-003", + caseId: "CASE-003", + donorName: "محمد الشمري", + donorPhone: "0589012345", + beneficiaryMessage: "شكراً جزيلاً، السلة الغذائية أفادتنا كثيراً.", + whatsappMessage: + "السلام عليكم، نشكركم على تبرعكم عبر منصة إحسان.\nتم إيصال الدعم للمستفيد، وهذه رسالة الشكر من المستفيد:\n\"شكراً جزيلاً، السلة الغذائية أفادتنا كثيراً.\"\nرقم الحالة: CASE-003", + status: "pending", + sentAt: null, + createdAt: d(3), + }, +]; + +// ─── Helper: Determine status after eligibility check ─────────────────────── +export function checkEligibility(nationalId: string): { + eligible: boolean | null; +} { + const record = eligibilityDb.find((r) => r.nationalId === nationalId); + if (!record) return { eligible: null }; + return { eligible: record.eligible }; +} + +// ─── Helper: Status → Step mapping ────────────────────────────────────────── +export const STATUS_STEP: Record = { + new: 1, + pending_review: 2, + verified: 3, + published: 4, + donated: 5, + delivered: 6, + receipt_confirmed: 7, + thank_you_submitted: 8, + whatsapp_sent: 9, + closed: 10, + rejected: 2, +}; diff --git a/artifacts/api-server/src/routes/donors.ts b/artifacts/api-server/src/routes/donors.ts new file mode 100644 index 0000000..6f9e60a --- /dev/null +++ b/artifacts/api-server/src/routes/donors.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { donors } from "../lib/mockDb.js"; + +const router = Router(); + +router.get("/donors", (_req, res) => { + res.json(donors); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts index 5a1f77a..42f716f 100644 --- a/artifacts/api-server/src/routes/index.ts +++ b/artifacts/api-server/src/routes/index.ts @@ -1,8 +1,16 @@ import { Router, type IRouter } from "express"; -import healthRouter from "./health"; +import healthRouter from "./health.js"; +import requestsRouter from "./requests.js"; +import donorsRouter from "./donors.js"; +import statsRouter from "./stats.js"; +import whatsappLogRouter from "./whatsappLog.js"; const router: IRouter = Router(); router.use(healthRouter); +router.use(requestsRouter); +router.use(donorsRouter); +router.use(statsRouter); +router.use(whatsappLogRouter); export default router; diff --git a/artifacts/api-server/src/routes/requests.ts b/artifacts/api-server/src/routes/requests.ts new file mode 100644 index 0000000..a21650c --- /dev/null +++ b/artifacts/api-server/src/routes/requests.ts @@ -0,0 +1,359 @@ +import { Router } from "express"; +import { + requests, + donors, + whatsappLog, + checkEligibility, + STATUS_STEP, + DonationRequest, + WhatsappLogEntry, +} from "../lib/mockDb.js"; +import { v4 as uuidv4 } from "uuid"; + +const router = Router(); + +// ─── GET /requests ─────────────────────────────────────────────────────────── +router.get("/requests", (req, res) => { + let result = [...requests]; + const { status, needType } = req.query; + if (status) result = result.filter((r) => r.status === status); + if (needType) result = result.filter((r) => r.needType === needType); + res.json(result); +}); + +// ─── GET /requests/new ────────────────────────────────────────────────────── +router.get("/requests/new", (_req, res) => { + res.json(requests.filter((r) => r.status === "new")); +}); + +// ─── GET /requests/published ──────────────────────────────────────────────── +router.get("/requests/published", (_req, res) => { + res.json(requests.filter((r) => r.status === "published")); +}); + +// ─── POST /requests ────────────────────────────────────────────────────────── +router.post("/requests", (req, res) => { + const { + beneficiaryName, + nationalId, + phone, + source, + sourceName, + needType, + requestedAmount, + description, + } = req.body; + + if ( + !beneficiaryName || + !nationalId || + !phone || + !source || + !sourceName || + !needType || + !requestedAmount || + !description + ) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const { eligible } = checkEligibility(nationalId); + + let status: DonationRequest["status"]; + if (eligible === true) { + status = "verified"; + } else if (eligible === false) { + status = "rejected"; + } else { + status = "pending_review"; + } + + const now = new Date().toISOString(); + const caseNum = String(requests.length + 1).padStart(3, "0"); + const newReq: DonationRequest = { + id: uuidv4(), + caseId: `CASE-${caseNum}`, + beneficiaryName, + nationalId, + phone, + source, + sourceName, + needType, + requestedAmount: Number(requestedAmount), + collectedAmount: 0, + description, + status, + currentStep: STATUS_STEP[status], + donorId: null, + donorName: null, + thankYouMessage: null, + whatsappStatus: null, + whatsappSentAt: null, + rejectionReason: + status === "rejected" + ? "المستفيد غير مؤهل وفق قاعدة بيانات الاستحقاق" + : null, + createdAt: now, + updatedAt: now, + }; + + requests.push(newReq); + return 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" }); + res.json(item); +}); + +// ─── Helper: find & update ─────────────────────────────────────────────────── +function findAndUpdate( + id: string, + updater: (r: DonationRequest) => void, + res: any +) { + const item = requests.find((r) => r.id === id || r.caseId === id); + if (!item) return res.status(404).json({ error: "Not found" }); + updater(item); + item.updatedAt = new Date().toISOString(); + res.json(item); +} + +// ─── POST /requests/:id/verify ─────────────────────────────────────────────── +router.post("/requests/:id/verify", (req, res) => { + findAndUpdate( + req.params.id, + (r) => { + r.status = "verified"; + r.currentStep = STATUS_STEP["verified"]; + }, + res + ); +}); + +// ─── POST /requests/:id/publish ────────────────────────────────────────────── +router.post("/requests/:id/publish", (req, res) => { + findAndUpdate( + req.params.id, + (r) => { + r.status = "published"; + r.currentStep = STATUS_STEP["published"]; + }, + res + ); +}); + +// ─── POST /requests/:id/donate ─────────────────────────────────────────────── +router.post("/requests/:id/donate", (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 { donorName, donorPhone, donorEmail, amount } = req.body; + if (!donorName || !donorPhone || !amount) { + return res.status(400).json({ error: "Missing donor details" }); + } + + const donorId = uuidv4(); + // add/update donor record + const existingDonor = donors.find((d) => d.phone === donorPhone); + if (existingDonor) { + existingDonor.totalDonated += Number(amount); + existingDonor.donationCount += 1; + item.donorId = existingDonor.id; + } else { + const newDonor = { + id: donorId, + name: donorName, + phone: donorPhone, + email: donorEmail || null, + totalDonated: Number(amount), + donationCount: 1, + }; + donors.push(newDonor); + item.donorId = donorId; + } + + item.donorName = donorName; + item.collectedAmount = Number(amount); + item.status = "donated"; + item.currentStep = STATUS_STEP["donated"]; + item.updatedAt = new Date().toISOString(); + res.json(item); +}); + +// ─── POST /requests/:id/deliver ────────────────────────────────────────────── +router.post("/requests/:id/deliver", (req, res) => { + findAndUpdate( + req.params.id, + (r) => { + r.status = "delivered"; + r.currentStep = STATUS_STEP["delivered"]; + }, + 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/thank-you ──────────────────────────────────────────── +router.post("/requests/:id/thank-you", (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 { message } = req.body; + if (!message) return res.status(400).json({ error: "Message is required" }); + + item.thankYouMessage = message; + item.status = "thank_you_submitted"; + item.currentStep = STATUS_STEP["thank_you_submitted"]; + item.whatsappStatus = "pending"; + item.updatedAt = new Date().toISOString(); + + // Create whatsapp log entry + const logEntry: WhatsappLogEntry = { + id: uuidv4(), + caseId: item.caseId, + donorName: item.donorName || "متبرع", + donorPhone: donors.find((d) => d.id === item.donorId)?.phone || "", + beneficiaryMessage: message, + whatsappMessage: `السلام عليكم، نشكركم على تبرعكم عبر منصة إحسان.\nتم إيصال الدعم للمستفيد، وهذه رسالة الشكر من المستفيد:\n"${message}"\nرقم الحالة: ${item.caseId}`, + status: "pending", + sentAt: null, + createdAt: new Date().toISOString(), + }; + whatsappLog.push(logEntry); + + 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; + } + + return res.json({ + success: true, + message: "WhatsApp sent (simulated)", + simulated: true, + sentAt: now, + }); + } + + // 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) => { + findAndUpdate( + req.params.id, + (r) => { + r.status = "closed"; + r.currentStep = STATUS_STEP["closed"]; + }, + res + ); +}); + +// ─── POST /requests/:id/reject ─────────────────────────────────────────────── +router.post("/requests/:id/reject", (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" }); + + item.status = "rejected"; + item.currentStep = STATUS_STEP["rejected"]; + item.rejectionReason = req.body?.reason || "تم الرفض من قبل المشرف"; + item.updatedAt = new Date().toISOString(); + res.json(item); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/stats.ts b/artifacts/api-server/src/routes/stats.ts new file mode 100644 index 0000000..5583468 --- /dev/null +++ b/artifacts/api-server/src/routes/stats.ts @@ -0,0 +1,53 @@ +import { Router } from "express"; +import { requests } from "../lib/mockDb.js"; + +const router = Router(); + +router.get("/stats", (_req, res) => { + const totalRequests = requests.length; + const totalDonated = requests.filter((r) => + ["donated", "delivered", "receipt_confirmed", "thank_you_submitted", "whatsapp_sent", "closed"].includes(r.status) + ).length; + const totalCollected = requests.reduce((sum, r) => sum + r.collectedAmount, 0); + const totalClosed = requests.filter((r) => r.status === "closed").length; + const pendingVerification = requests.filter((r) => + ["new", "pending_review"].includes(r.status) + ).length; + const activeOpportunities = requests.filter((r) => r.status === "published").length; + + const statusCounts: Record = {}; + for (const r of requests) { + statusCounts[r.status] = (statusCounts[r.status] || 0) + 1; + } + const byStatus = Object.entries(statusCounts).map(([status, count]) => ({ + status, + count, + })); + + const needTypeCounts: Record = {}; + for (const r of requests) { + if (!needTypeCounts[r.needType]) { + needTypeCounts[r.needType] = { count: 0, totalAmount: 0 }; + } + needTypeCounts[r.needType].count += 1; + needTypeCounts[r.needType].totalAmount += r.requestedAmount; + } + const byNeedType = Object.entries(needTypeCounts).map(([needType, v]) => ({ + needType, + count: v.count, + totalAmount: v.totalAmount, + })); + + res.json({ + totalRequests, + totalDonated, + totalCollected, + totalClosed, + pendingVerification, + activeOpportunities, + byStatus, + byNeedType, + }); +}); + +export default router; diff --git a/artifacts/api-server/src/routes/whatsappLog.ts b/artifacts/api-server/src/routes/whatsappLog.ts new file mode 100644 index 0000000..7032fc0 --- /dev/null +++ b/artifacts/api-server/src/routes/whatsappLog.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { whatsappLog } from "../lib/mockDb.js"; + +const router = Router(); + +router.get("/whatsapp-log", (_req, res) => { + res.json(whatsappLog); +}); + +export default router; diff --git a/artifacts/ehsan-poc/.replit-artifact/artifact.toml b/artifacts/ehsan-poc/.replit-artifact/artifact.toml new file mode 100644 index 0000000..9ad1756 --- /dev/null +++ b/artifacts/ehsan-poc/.replit-artifact/artifact.toml @@ -0,0 +1,31 @@ +kind = "web" +previewPath = "/" +title = "EHSAN Closed Donation Loop" +version = "1.0.0" +id = "artifacts/ehsan-poc" +router = "path" + +[[integratedSkills]] +name = "react-vite" +version = "1.0.0" + +[[services]] +name = "web" +paths = [ "/" ] +localPort = 18312 + +[services.development] +run = "pnpm --filter @workspace/ehsan-poc run dev" + +[services.production] +build = [ "pnpm", "--filter", "@workspace/ehsan-poc", "run", "build" ] +publicDir = "artifacts/ehsan-poc/dist/public" +serve = "static" + +[[services.production.rewrites]] +from = "/*" +to = "/index.html" + +[services.env] +PORT = "18312" +BASE_PATH = "/" diff --git a/artifacts/ehsan-poc/components.json b/artifacts/ehsan-poc/components.json new file mode 100644 index 0000000..3ff62cf --- /dev/null +++ b/artifacts/ehsan-poc/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/artifacts/ehsan-poc/index.html b/artifacts/ehsan-poc/index.html new file mode 100644 index 0000000..22123dd --- /dev/null +++ b/artifacts/ehsan-poc/index.html @@ -0,0 +1,24 @@ + + + + + + EHSAN Closed Donation Loop + + + + + + + + + + + + + + +
+ + + diff --git a/artifacts/ehsan-poc/package.json b/artifacts/ehsan-poc/package.json new file mode 100644 index 0000000..63cc7ec --- /dev/null +++ b/artifacts/ehsan-poc/package.json @@ -0,0 +1,77 @@ +{ + "name": "@workspace/ehsan-poc", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --config vite.config.ts --host 0.0.0.0", + "build": "vite build --config vite.config.ts", + "serve": "vite preview --config vite.config.ts --host 0.0.0.0", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "devDependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-alert-dialog": "^1.1.7", + "@radix-ui/react-aspect-ratio": "^1.1.3", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-collapsible": "^1.1.4", + "@radix-ui/react-context-menu": "^2.2.7", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-hover-card": "^1.1.7", + "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-menubar": "^1.1.7", + "@radix-ui/react-navigation-menu": "^1.2.6", + "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-progress": "^1.1.3", + "@radix-ui/react-radio-group": "^1.2.4", + "@radix-ui/react-scroll-area": "^1.2.4", + "@radix-ui/react-select": "^2.1.7", + "@radix-ui/react-separator": "^1.1.3", + "@radix-ui/react-slider": "^1.2.4", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-toast": "^1.2.7", + "@radix-ui/react-toggle": "^1.1.3", + "@radix-ui/react-toggle-group": "^1.1.3", + "@radix-ui/react-tooltip": "^1.2.0", + "@replit/vite-plugin-cartographer": "catalog:", + "@replit/vite-plugin-dev-banner": "catalog:", + "@replit/vite-plugin-runtime-error-modal": "catalog:", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "catalog:", + "@tanstack/react-query": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "@workspace/api-client-react": "workspace:*", + "class-variance-authority": "catalog:", + "clsx": "catalog:", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "framer-motion": "catalog:", + "input-otp": "^1.4.2", + "lucide-react": "catalog:", + "next-themes": "^0.4.6", + "react": "catalog:", + "react-day-picker": "^9.11.1", + "react-dom": "catalog:", + "react-hook-form": "^7.55.0", + "react-icons": "^5.4.0", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.15.2", + "sonner": "^2.0.7", + "tailwind-merge": "catalog:", + "tailwindcss": "catalog:", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2", + "vite": "catalog:", + "wouter": "^3.3.5", + "zod": "catalog:" + } +} diff --git a/artifacts/ehsan-poc/public/favicon.svg b/artifacts/ehsan-poc/public/favicon.svg new file mode 100644 index 0000000..4373d3c --- /dev/null +++ b/artifacts/ehsan-poc/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/artifacts/ehsan-poc/public/opengraph.jpg b/artifacts/ehsan-poc/public/opengraph.jpg new file mode 100644 index 0000000000000000000000000000000000000000..558299cbfb600890b1afecd38b2d76136ed3a463 GIT binary patch literal 38142 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'_ysEW3Doax~ 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 JfaY(L{{abn(y0Id literal 0 HcmV?d00001 diff --git a/artifacts/ehsan-poc/public/robots.txt b/artifacts/ehsan-poc/public/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/artifacts/ehsan-poc/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/artifacts/ehsan-poc/src/App.tsx b/artifacts/ehsan-poc/src/App.tsx new file mode 100644 index 0000000..15fec76 --- /dev/null +++ b/artifacts/ehsan-poc/src/App.tsx @@ -0,0 +1,54 @@ +import { Switch, Route, Router as WouterRouter } from "wouter"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { LanguageProvider } from "./contexts/LanguageContext"; +import { AppLayout } from "./components/layout/AppLayout"; +import NotFound from "@/pages/not-found"; + +// Page imports +import Home from "./pages/home"; +import RequestSupport from "./pages/request"; +import Opportunities from "./pages/opportunities"; +import Donate from "./pages/donate"; +import Admin from "./pages/admin"; +import Track from "./pages/track"; +import ThankYou from "./pages/thank-you"; +import WhatsappLog from "./pages/whatsapp-log"; + +const queryClient = new QueryClient(); + +function Router() { + return ( + + + + + + + + + + + + + + ); +} + +function App() { + return ( + + + + + + + + + + + ); +} + +export default App; diff --git a/artifacts/ehsan-poc/src/components/layout/AppLayout.tsx b/artifacts/ehsan-poc/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..99ea593 --- /dev/null +++ b/artifacts/ehsan-poc/src/components/layout/AppLayout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from "react"; +import { Header } from "./Header"; + +export function AppLayout({ children }: { children: ReactNode }) { + return ( +
+
+
+ {children} +
+
+
+ EHSAN POC © {new Date().getFullYear()} +
+
+
+ ); +} diff --git a/artifacts/ehsan-poc/src/components/layout/Header.tsx b/artifacts/ehsan-poc/src/components/layout/Header.tsx new file mode 100644 index 0000000..60bb752 --- /dev/null +++ b/artifacts/ehsan-poc/src/components/layout/Header.tsx @@ -0,0 +1,57 @@ +import { Link, useLocation } from "wouter"; +import { useLanguage } from "../../contexts/LanguageContext"; +import { Button } from "../ui/button"; + +export function Header() { + const { language, setLanguage, t } = useLanguage(); + const [location] = useLocation(); + + const toggleLanguage = () => { + setLanguage(language === "ar" ? "en" : "ar"); + }; + + const navItems = [ + { path: "/", label: t.common.home }, + { path: "/opportunities", label: t.common.opportunities }, + { path: "/request", label: t.common.requestSupport }, + { path: "/admin", label: t.common.adminDashboard }, + { path: "/whatsapp-log", label: t.common.whatsappLog }, + ]; + + return ( +
+
+
+ +
+ إ +
+ {t.common.ehsan} + + + +
+ +
+ +
+
+
+ ); +} diff --git a/artifacts/ehsan-poc/src/components/ui/accordion.tsx b/artifacts/ehsan-poc/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e1797c9 --- /dev/null +++ b/artifacts/ehsan-poc/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/artifacts/ehsan-poc/src/components/ui/alert-dialog.tsx b/artifacts/ehsan-poc/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..fa2b442 --- /dev/null +++ b/artifacts/ehsan-poc/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/artifacts/ehsan-poc/src/components/ui/alert.tsx b/artifacts/ehsan-poc/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/artifacts/ehsan-poc/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/artifacts/ehsan-poc/src/components/ui/aspect-ratio.tsx b/artifacts/ehsan-poc/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/artifacts/ehsan-poc/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/artifacts/ehsan-poc/src/components/ui/avatar.tsx b/artifacts/ehsan-poc/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/artifacts/ehsan-poc/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/artifacts/ehsan-poc/src/components/ui/badge.tsx b/artifacts/ehsan-poc/src/components/ui/badge.tsx new file mode 100644 index 0000000..3f03665 --- /dev/null +++ b/artifacts/ehsan-poc/src/components/ui/badge.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + // @replit + // Whitespace-nowrap: Badges should never wrap. + "whitespace-nowrap inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + + " hover-elevate ", + { + variants: { + variant: { + default: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-primary text-primary-foreground shadow-xs", + secondary: + // @replit no hover because we use hover-elevate + "border-transparent bg-secondary text-secondary-foreground", + destructive: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-destructive text-destructive-foreground shadow-xs", + // @replit shadow-xs" - use badge outline variable + outline: "text-foreground border [border-color:var(--badge-outline)]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/artifacts/ehsan-poc/src/components/ui/breadcrumb.tsx b/artifacts/ehsan-poc/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/artifacts/ehsan-poc/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>