Build EHSAN Closed Donation Loop POC — full bilingual Arabic/English app

- 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.
This commit is contained in:
Replit Agent
2026-06-05 17:05:27 +00:00
parent 2da838bb66
commit 12111a9562
117 changed files with 12366 additions and 81 deletions
+442
View File
@@ -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<RequestStatus, number> = {
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,
};
+10
View File
@@ -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;
+9 -1
View File
@@ -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;
+359
View File
@@ -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;
+53
View File
@@ -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<string, number> = {};
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<string, { count: number; totalAmount: number }> = {};
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;
@@ -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;