Files
Ehsan/artifacts/api-server/src/routes/requests.ts
T
Replit Agent 1dcfa0bfa5 Complete EHSAN POC: fix all code review findings
- API routes: explicit return types on all Express handlers (fixes TS7030),
  `beneficiaryName` now accepted and stored in /thank-you route per OpenAPI spec.

- Home page: added search bar (filters by case ID, description, name) +
  Featured Opportunities section with live cards, progress bars, and Donate buttons.

- Opportunities page: added need-type filter pill bar (all 8 types + "All Types")
  with active state highlighting; empty state respects selected filter.

- i18n: expanded translations with all previously hardcoded strings
  (trackCase, notFound, noData, currentStep, search, searchPlaceholder,
  featuredTitle, noResults, donate.caseSummary, donate.caseNotFound,
  admin.noRequests, admin.needType, admin.amount, admin.track, admin.whatsapp,
  track.caseInfo, track.rejected, track.currentStepLabel, track.submitThankYou,
  thankYou.successNote, thankYou.beneficiaryMessageLabel,
  whatsapp.donorPhone, whatsapp.beneficiaryMessage, whatsapp.noEntries,
  opportunities.noOpportunities, opportunities.verified).
  All pages now use t.* — zero hardcoded English UI strings.

- TypeScript: both frontend (tsc --noEmit) and API server build are clean.
2026-06-05 17:12:44 +00:00

388 lines
12 KiB
TypeScript

import { Router, Request, Response } 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: Request, res: Response): void => {
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: Request, res: Response): void => {
res.json(requests.filter((r) => r.status === "new"));
});
// ─── 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: Request, res: Response): void => {
const {
beneficiaryName,
nationalId,
phone,
source,
sourceName,
needType,
requestedAmount,
description,
} = req.body;
if (
!beneficiaryName ||
!nationalId ||
!phone ||
!source ||
!sourceName ||
!needType ||
!requestedAmount ||
!description
) {
res.status(400).json({ error: "Missing required fields" });
return;
}
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);
res.status(201).json(newReq);
});
// ─── 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 ────────────────────────────────────────────────────
function findAndUpdate(
id: string,
updater: (r: DonationRequest) => void,
res: Response
): void {
const item = requests.find((r) => r.id === id || r.caseId === id);
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: Request, res: Response): void => {
findAndUpdate(
req.params.id,
(r) => {
r.status = "verified";
r.currentStep = STATUS_STEP["verified"];
},
res
);
});
// ─── POST /requests/:id/publish ───────────────────────────────────────────────
router.post("/requests/:id/publish", (req: Request, res: Response): void => {
findAndUpdate(
req.params.id,
(r) => {
r.status = "published";
r.currentStep = STATUS_STEP["published"];
},
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) {
res.status(404).json({ error: "Not found" });
return;
}
const { donorName, donorPhone, donorEmail, amount } = req.body;
if (!donorName || !donorPhone || !amount) {
res.status(400).json({ error: "Missing donor details" });
return;
}
const donorId = uuidv4();
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: Request, res: Response): void => {
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: 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: 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;
}
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";
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: Request, res: Response): Promise<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;
}
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,
});
}
}
);
// ─── POST /requests/:id/close ─────────────────────────────────────────────────
router.post("/requests/:id/close", (req: Request, res: Response): void => {
findAndUpdate(
req.params.id,
(r) => {
r.status = "closed";
r.currentStep = STATUS_STEP["closed"];
},
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) {
res.status(404).json({ error: "Not found" });
return;
}
item.status = "rejected";
item.currentStep = STATUS_STEP["rejected"];
item.rejectionReason = req.body?.reason || "تم الرفض من قبل المشرف";
item.updatedAt = new Date().toISOString();
res.json(item);
});
export default router;