feat(J): J-UI frontend — ImprovementCenter, ProposalCard, PatternInsight, API client, i18n, tsc clean
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user