Frontend — consistent HSL token usage across remaining pages: - Users: shared Card, Badge with success/error tokens, h2 typography, animate-fadeIn - Audit: shared Card, muted-foreground text, animate-fadeIn - Clients: shared Card, Badge active/inactive, hsl(--primary) icon color - Storage: shared Card, StatusBadge for status pills, hsl warning/primary bars replacing hardcoded amber/blue, all gray text → muted-foreground - Login: hsl(--surface) bg, hsl(--primary) submit button, brand mark icon, animate-scaleIn card entry, hsl(--warning) dev notice Backend tests — convert print-only stubs to real assertions: - test_pptx_creator: mkdir, deterministic save path, assert file exists + slide count - test_gemini_schema_support: direct google.genai client, skipif guard on GOOGLE_API_KEY, JSON parse + Pydantic model validation assertions - test_openai_schema_support: clean skip (OpenAI removed in Phase 6) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import { AppDispatch, RootState } from '@/store/store';
|
|
import { fetchClients } from '@/store/slices/adminSlice';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/shared/Card';
|
|
import { Plus, Building2 } from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import CreateClientDialog from '../components/CreateClientDialog';
|
|
|
|
export default function ClientsPage() {
|
|
const dispatch = useDispatch<AppDispatch>();
|
|
const { clients, isLoading } = useSelector((state: RootState) => state.admin);
|
|
const user = useSelector((state: RootState) => state.auth.user);
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
dispatch(fetchClients());
|
|
}, [dispatch]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-4 animate-fadeIn">
|
|
<h1 className="h2">Clients</h1>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div key={i} className="h-36 bg-[hsl(var(--surface-hover))] rounded-lg animate-pulse" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4 animate-fadeIn">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="h2">Clients</h1>
|
|
{user?.role === 'super_admin' && (
|
|
<Button onClick={() => setCreateOpen(true)}>
|
|
<Plus className="w-4 h-4 mr-1" />
|
|
New Client
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{clients.length === 0 ? (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<Building2 className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
|
<p>No clients yet.</p>
|
|
{user?.role === 'super_admin' && (
|
|
<p className="text-sm mt-1">Create your first client to get started.</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{clients.map((client) => (
|
|
<Link key={client.id} href={`/admin/clients/${client.id}`}>
|
|
<Card hover className="p-5">
|
|
<CardHeader className="mb-2 pb-0">
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Building2 className="w-5 h-5 text-[hsl(var(--primary))]" />
|
|
{client.name}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
<span>/{client.slug}</span>
|
|
<Badge
|
|
className={
|
|
client.is_active
|
|
? 'bg-[hsl(var(--success)/0.1)] text-[hsl(var(--success))] border-[hsl(var(--success)/0.2)]'
|
|
: 'bg-[hsl(var(--error)/0.1)] text-[hsl(var(--error))] border-[hsl(var(--error)/0.2)]'
|
|
}
|
|
variant="outline"
|
|
>
|
|
{client.is_active ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
Policy: {client.review_policy}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<CreateClientDialog open={createOpen} onOpenChange={setCreateOpen} />
|
|
</div>
|
|
);
|
|
}
|