1dcfa0bfa5
- 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.
155 lines
5.9 KiB
TypeScript
155 lines
5.9 KiB
TypeScript
import { useLanguage } from "../contexts/LanguageContext";
|
|
import {
|
|
useListWhatsappLog, useSendWhatsapp,
|
|
getListWhatsappLogQueryKey, getListRequestsQueryKey, useListRequests,
|
|
} from "@workspace/api-client-react";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { MessageSquare, Phone, Send, CheckCircle, Clock, XCircle } from "lucide-react";
|
|
|
|
export default function WhatsappLog() {
|
|
const { t } = useLanguage();
|
|
const queryClient = useQueryClient();
|
|
const { data: logs, isLoading } = useListWhatsappLog();
|
|
const { data: allRequests } = useListRequests();
|
|
const sendWhatsapp = useSendWhatsapp();
|
|
|
|
const getRequestByCase = (caseId: string) =>
|
|
allRequests?.find((r) => r.caseId === caseId);
|
|
|
|
const handleSend = (caseId: string) => {
|
|
const req = getRequestByCase(caseId);
|
|
if (!req) return;
|
|
sendWhatsapp.mutate(
|
|
{ id: req.id },
|
|
{
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: getListWhatsappLogQueryKey() });
|
|
queryClient.invalidateQueries({ queryKey: getListRequestsQueryKey() });
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
const statusConfig = {
|
|
pending: {
|
|
icon: Clock,
|
|
className: "bg-amber-100 text-amber-700 border-amber-200",
|
|
label: t.whatsapp.pending,
|
|
},
|
|
sent: {
|
|
icon: CheckCircle,
|
|
className: "bg-green-100 text-green-700 border-green-200",
|
|
label: t.whatsapp.sent,
|
|
},
|
|
failed: {
|
|
icon: XCircle,
|
|
className: "bg-red-100 text-red-700 border-red-200",
|
|
label: t.whatsapp.failed,
|
|
},
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-12">
|
|
<div className="mb-8 flex items-center gap-3">
|
|
<MessageSquare className="w-7 h-7 text-primary" />
|
|
<h1 className="text-3xl font-bold text-foreground">{t.whatsapp.title}</h1>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="space-y-4">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-48 w-full" />
|
|
))}
|
|
</div>
|
|
) : !logs || logs.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="py-16 text-center text-muted-foreground">
|
|
{t.whatsapp.noEntries}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{logs.map((log) => {
|
|
const status = statusConfig[log.status as keyof typeof statusConfig] || statusConfig.pending;
|
|
const StatusIcon = status.icon;
|
|
|
|
return (
|
|
<Card key={log.id} className="overflow-hidden" data-testid={`card-whatsapp-${log.id}`}>
|
|
<CardHeader className="pb-3 bg-muted/20">
|
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<CardTitle className="text-base font-mono">{log.caseId}</CardTitle>
|
|
<Badge variant="outline" className={status.className}>
|
|
<StatusIcon className="w-3 h-3 me-1" />
|
|
{status.label}
|
|
</Badge>
|
|
</div>
|
|
{log.status === "pending" && (
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleSend(log.caseId)}
|
|
disabled={sendWhatsapp.isPending}
|
|
className="gap-2"
|
|
data-testid={`button-sendWhatsapp-${log.id}`}
|
|
>
|
|
<Send className="w-3.5 h-3.5" />
|
|
{t.whatsapp.sendViaOpenClaw}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Donor Info */}
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase text-muted-foreground tracking-wide mb-2">
|
|
{t.whatsapp.donor}
|
|
</p>
|
|
<p className="font-medium">{log.donorName}</p>
|
|
<div className="flex items-center gap-1.5 text-sm text-muted-foreground mt-1">
|
|
<Phone className="w-3.5 h-3.5" />
|
|
<span>{log.donorPhone}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Beneficiary Message */}
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase text-muted-foreground tracking-wide mb-2">
|
|
{t.thankYou.beneficiaryMessageLabel}
|
|
</p>
|
|
<p className="text-sm text-foreground italic">"{log.beneficiaryMessage}"</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
{/* WhatsApp Message Preview */}
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase text-muted-foreground tracking-wide mb-2">
|
|
{t.whatsapp.message}
|
|
</p>
|
|
<div className="bg-[#dcf8c6] dark:bg-green-900/30 rounded-xl rounded-tl-sm p-4 max-w-lg text-sm whitespace-pre-line text-gray-800 dark:text-gray-200 border border-green-200 dark:border-green-700">
|
|
{log.whatsappMessage}
|
|
</div>
|
|
</div>
|
|
|
|
{log.sentAt && (
|
|
<p className="text-xs text-muted-foreground mt-3">
|
|
{t.whatsapp.sentAt}: {new Date(log.sentAt).toLocaleString()}
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|