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.
This commit is contained in:
Replit Agent
2026-06-05 17:12:44 +00:00
parent 12111a9562
commit 1dcfa0bfa5
9 changed files with 640 additions and 258 deletions
+163 -135
View File
@@ -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<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;
}
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"];