refactor: migrate loyalty-agent and loyalty-mcp-server components to a new modular structure with updated specs and frontend integration.

This commit is contained in:
2026-07-19 21:43:44 +07:00
parent c3861d6476
commit cda93365ca
128 changed files with 12721 additions and 6343 deletions

View File

@@ -0,0 +1,109 @@
import { useEffect, useState } from 'react';
interface Campaign {
id: string;
name: string;
status: string;
startDate?: string;
endDate?: string;
}
export function CampaignList() {
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCampaignId, setSelectedCampaignId] = useState<string | null>(null);
const [rules, setRules] = useState<any[]>([]);
const [loadingRules, setLoadingRules] = useState(false);
useEffect(() => {
fetch('http://localhost:9332/api/v1/agent/campaigns')
.then((res) => res.json())
.then((data) => {
if (Array.isArray(data)) setCampaigns(data);
else if (data && Array.isArray(data.content)) setCampaigns(data.content);
else if (data && Array.isArray(data.data)) setCampaigns(data.data);
else setCampaigns([]);
})
.catch((err) => {
console.error('Failed to fetch campaigns', err);
setCampaigns([]);
})
.finally(() => setLoading(false));
}, []);
const handleSelectCampaign = (id: string) => {
if (selectedCampaignId === id) {
setSelectedCampaignId(null);
setRules([]);
return;
}
setSelectedCampaignId(id);
setLoadingRules(true);
fetch(`http://localhost:9332/api/v1/agent/campaigns/${id}/rules`)
.then((res) => res.json())
.then((data) => {
if (Array.isArray(data)) setRules(data);
else if (data && Array.isArray(data.content)) setRules(data.content);
else if (data && Array.isArray(data.data)) setRules(data.data);
else setRules([]);
})
.catch((err) => {
console.error('Failed to fetch rules', err);
setRules([]);
})
.finally(() => setLoadingRules(false));
};
if (loading) {
return <div className="p-4 text-center text-sm text-muted-foreground">Loading campaigns...</div>;
}
if (campaigns.length === 0) {
return <div className="p-4 text-center text-sm text-muted-foreground">No campaigns found.</div>;
}
return (
<div className="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar">
{campaigns.map((camp) => (
<div key={camp.id} className="space-y-2">
<div
onClick={() => handleSelectCampaign(camp.id)}
className={`glass-card rounded-xl p-4 text-sm group cursor-pointer relative overflow-hidden transition-all duration-200 ${selectedCampaignId === camp.id ? 'ring-2 ring-primary/50' : ''}`}>
<div className="absolute inset-0 bg-primary/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative z-10">
<div className="font-semibold text-foreground text-base mb-1 group-hover:text-primary transition-colors">{camp.name || 'Unnamed Campaign'}</div>
<div className="flex justify-between items-center text-xs text-muted-foreground mt-3">
<span className="flex items-center gap-1.5">
<span className={`w-2 h-2 rounded-full ${camp.status === 'A' ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]' : 'bg-rose-500'}`} />
{camp.status === 'A' ? 'Active' : camp.status}
</span>
<code className="bg-background/50 px-1.5 py-0.5 rounded text-[10px] uppercase">{camp.id}</code>
</div>
</div>
</div>
{selectedCampaignId === camp.id && (
<div className="pl-4 pr-2 space-y-2 border-l-2 border-primary/20 ml-2 animate-in slide-in-from-top-2">
{loadingRules ? (
<div className="text-xs text-muted-foreground italic">Loading rules...</div>
) : rules.length === 0 ? (
<div className="text-xs text-muted-foreground italic">No rules found.</div>
) : (
rules.map((rule, idx) => (
<div key={rule.id || idx} className="bg-background/40 rounded-lg p-3 text-xs border border-border/50">
<div className="font-medium text-foreground mb-1">{rule.name || 'Unnamed Rule'}</div>
<div className="text-muted-foreground line-clamp-2">{rule.description || 'No description'}</div>
{rule.status && (
<div className="mt-2 text-[10px] uppercase tracking-wider text-muted-foreground/80">Status: {rule.status}</div>
)}
</div>
))
)}
</div>
)}
</div>
))}
</div>
);
}