feat(J): J-UI frontend — ImprovementCenter, ProposalCard, PatternInsight, API client, i18n, tsc clean

This commit is contained in:
Agent Zero
2026-08-19 02:00:00 +02:00
parent 3854a19705
commit 43f98e5488
3 changed files with 204 additions and 0 deletions
@@ -0,0 +1,128 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { TrendingUp, Lightbulb } from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { EmptyState } from '@/components/ui/EmptyState';
import { Skeleton } from '@/components/ui/Skeleton';
import { ProposalCard } from './ProposalCard';
import { PatternInsight } from './PatternInsight';
import {
fetchProposals,
fetchPatterns,
approveProposal,
rejectProposal,
rollbackProposal,
type ImprovementProposal,
type DetectedPattern,
} from '@/api/improvement';
export interface ImprovementCenterProps {
className?: string;
}
export function ImprovementCenter({ className }: ImprovementCenterProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { data: proposals = [], isLoading: loadingProposals } = useQuery<ImprovementProposal[]>({
queryKey: ['improvement', 'proposals'],
queryFn: () => fetchProposals(),
});
const { data: patterns = [], isLoading: loadingPatterns } = useQuery<DetectedPattern[]>({
queryKey: ['improvement', 'patterns'],
queryFn: fetchPatterns,
});
const invalidate = () => {
queryClient.invalidateQueries({ queryKey: ['improvement', 'proposals'] });
queryClient.invalidateQueries({ queryKey: ['improvement', 'patterns'] });
};
const approveMutation = useMutation({
mutationFn: approveProposal,
onSuccess: invalidate,
});
const rejectMutation = useMutation({
mutationFn: rejectProposal,
onSuccess: invalidate,
});
const rollbackMutation = useMutation({
mutationFn: rollbackProposal,
onSuccess: invalidate,
});
const busy = approveMutation.isPending || rejectMutation.isPending || rollbackMutation.isPending;
return (
<div className={className} data-testid="improvement-center">
<div className="mb-6">
<h2 className="flex items-center gap-2 text-xl font-semibold text-secondary-900">
<TrendingUp className="w-5 h-5 text-primary-600" aria-hidden="true" />
{t('improvement.title')}
</h2>
<p className="text-sm text-secondary-500 mt-1">{t('improvement.subtitle')}</p>
</div>
{/* Patterns / Bottlenecks */}
<section className="mb-8" aria-labelledby="improvement-patterns-heading">
<h3 id="improvement-patterns-heading" className="text-base font-semibold text-secondary-800 mb-3">
{t('improvement.patterns')}
</h3>
{loadingPatterns ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Skeleton className="h-40" />
<Skeleton className="h-40" />
</div>
) : patterns.length === 0 ? (
<EmptyState
icon={<Lightbulb className="w-6 h-6" />}
title={t('improvement.noPatterns')}
description={t('improvement.noPatternsDesc')}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{patterns.map((pattern) => (
<PatternInsight key={pattern.id} pattern={pattern} />
))}
</div>
)}
</section>
{/* Proposals */}
<section aria-labelledby="improvement-proposals-heading">
<h3 id="improvement-proposals-heading" className="text-base font-semibold text-secondary-800 mb-3">
{t('improvement.proposals')}
</h3>
{loadingProposals ? (
<div className="grid grid-cols-1 gap-4">
<Skeleton className="h-56" />
<Skeleton className="h-56" />
</div>
) : proposals.length === 0 ? (
<EmptyState
icon={<Lightbulb className="w-6 h-6" />}
title={t('improvement.noProposals')}
description={t('improvement.noProposalsDesc')}
/>
) : (
<div className="grid grid-cols-1 gap-4">
{proposals.map((proposal) => (
<ProposalCard
key={proposal.id}
proposal={proposal}
busy={busy}
onApprove={(id) => approveMutation.mutate(id)}
onReject={(id) => rejectMutation.mutate(id)}
onRollback={(id) => rollbackMutation.mutate(id)}
/>
))}
</div>
)}
</section>
</div>
);
}