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:
@@ -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"];
|
||||
|
||||
Reference in New Issue
Block a user