Improve frontend 5

This commit is contained in:
Urtzi Alfaro
2025-11-20 19:14:49 +01:00
parent 29e6ddcea9
commit 4433b66f25
30 changed files with 3649 additions and 600 deletions

View File

@@ -89,7 +89,8 @@ function ActionItemCard({
const config = urgencyConfig[action.urgency as keyof typeof urgencyConfig] || urgencyConfig.normal;
const UrgencyIcon = config.icon;
const { formatPOAction } = useReasoningFormatter();
const { t } = useTranslation('reasoning');
const { t: tReasoning } = useTranslation('reasoning');
const { t: tDashboard } = useTranslation('dashboard');
// Fetch PO details if this is a PO action and details are expanded
const { data: poDetail } = usePurchaseOrder(
@@ -98,18 +99,28 @@ function ActionItemCard({
{ enabled: !!tenantId && showDetails && action.type === 'po_approval' }
);
// Translate reasoning_data (or fallback to deprecated text fields)
// Memoize to prevent undefined values from being created on each render
const { reasoning, consequence, severity } = useMemo(() => {
if (action.reasoning_data) {
return formatPOAction(action.reasoning_data);
// Translate i18n fields (or fallback to deprecated text fields or reasoning_data for alerts)
const reasoning = useMemo(() => {
if (action.reasoning_i18n) {
return tDashboard(action.reasoning_i18n.key, action.reasoning_i18n.params);
}
return {
reasoning: action.reasoning || '',
consequence: action.consequence || '',
severity: ''
};
}, [action.reasoning_data, action.reasoning, action.consequence, formatPOAction]);
if (action.reasoning_data) {
const formatted = formatPOAction(action.reasoning_data);
return formatted.reasoning;
}
return action.reasoning || '';
}, [action.reasoning_i18n, action.reasoning_data, action.reasoning, tDashboard, formatPOAction]);
const consequence = useMemo(() => {
if (action.consequence_i18n) {
return tDashboard(action.consequence_i18n.key, action.consequence_i18n.params);
}
if (action.reasoning_data) {
const formatted = formatPOAction(action.reasoning_data);
return formatted.consequence;
}
return '';
}, [action.consequence_i18n, action.reasoning_data, tDashboard, formatPOAction]);
return (
<div
@@ -124,7 +135,9 @@ function ActionItemCard({
<UrgencyIcon className="w-6 h-6 flex-shrink-0" style={{ color: config.iconColor }} />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<h3 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}>{action.title || 'Action Required'}</h3>
<h3 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}>
{action.title_i18n ? tDashboard(action.title_i18n.key, action.title_i18n.params) : (action.title || 'Action Required')}
</h3>
<span
className="px-2 py-1 rounded text-xs font-semibold uppercase flex-shrink-0"
style={{
@@ -135,7 +148,9 @@ function ActionItemCard({
{action.urgency || 'normal'}
</span>
</div>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{action.subtitle || ''}</p>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{action.subtitle_i18n ? tDashboard(action.subtitle_i18n.key, action.subtitle_i18n.params) : (action.subtitle || '')}
</p>
</div>
</div>
@@ -152,7 +167,7 @@ function ActionItemCard({
{/* Reasoning (always visible) */}
<div className="rounded-md p-3 mb-3" style={{ backgroundColor: 'var(--bg-primary)' }}>
<p className="text-sm font-medium mb-1" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.action_queue.why_needed')}
{tReasoning('jtbd.action_queue.why_needed')}
</p>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>{reasoning}</p>
</div>
@@ -166,7 +181,7 @@ function ActionItemCard({
style={{ color: 'var(--text-secondary)' }}
>
{expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
<span className="font-medium">{t('jtbd.action_queue.what_if_not')}</span>
<span className="font-medium">{tReasoning('jtbd.action_queue.what_if_not')}</span>
</button>
{expanded && (
@@ -178,11 +193,6 @@ function ActionItemCard({
}}
>
<p className="text-sm" style={{ color: 'var(--color-warning-900)' }}>{consequence}</p>
{severity && (
<span className="text-xs font-semibold mt-1 block" style={{ color: 'var(--color-warning-900)' }}>
{severity}
</span>
)}
</div>
)}
</>
@@ -343,7 +353,7 @@ function ActionItemCard({
<div className="flex items-center gap-2 text-xs mb-4" style={{ color: 'var(--text-tertiary)' }}>
<Clock className="w-4 h-4" />
<span>
{t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes || 5} min
{tReasoning('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes || 5} min
</span>
</div>
@@ -483,7 +493,7 @@ function ActionItemCard({
{button.action === 'reject' && <X className="w-4 h-4" />}
{button.action === 'view_details' && <Eye className="w-4 h-4" />}
{button.action === 'modify' && <Edit className="w-4 h-4" />}
{button.label}
{tDashboard(button.label_i18n.key, button.label_i18n.params)}
</button>
);
})}
@@ -502,7 +512,7 @@ export function ActionQueueCard({
tenantId,
}: ActionQueueCardProps) {
const [showAll, setShowAll] = useState(false);
const { t } = useTranslation('reasoning');
const { t: tReasoning } = useTranslation('reasoning');
if (loading || !actionQueue) {
return (
@@ -527,9 +537,9 @@ export function ActionQueueCard({
>
<CheckCircle2 className="w-16 h-16 mx-auto mb-4" style={{ color: 'var(--color-success-600)' }} />
<h3 className="text-xl font-bold mb-2" style={{ color: 'var(--color-success-900)' }}>
{t('jtbd.action_queue.all_caught_up')}
{tReasoning('jtbd.action_queue.all_caught_up')}
</h3>
<p style={{ color: 'var(--color-success-700)' }}>{t('jtbd.action_queue.no_actions')}</p>
<p style={{ color: 'var(--color-success-700)' }}>{tReasoning('jtbd.action_queue.no_actions')}</p>
</div>
);
}
@@ -540,7 +550,7 @@ export function ActionQueueCard({
<div className="rounded-xl shadow-lg p-6 border" style={{ backgroundColor: 'var(--bg-primary)', borderColor: 'var(--border-primary)' }}>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('jtbd.action_queue.title')}</h2>
<h2 className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{tReasoning('jtbd.action_queue.title')}</h2>
{(actionQueue.totalActions || 0) > 3 && (
<span
className="px-3 py-1 rounded-full text-sm font-semibold"
@@ -549,7 +559,7 @@ export function ActionQueueCard({
color: 'var(--color-error-800)',
}}
>
{actionQueue.totalActions || 0} {t('jtbd.action_queue.total')}
{actionQueue.totalActions || 0} {tReasoning('jtbd.action_queue.total')}
</span>
)}
</div>
@@ -565,7 +575,7 @@ export function ActionQueueCard({
color: 'var(--color-error-800)',
}}
>
{actionQueue.criticalCount || 0} {t('jtbd.action_queue.critical')}
{actionQueue.criticalCount || 0} {tReasoning('jtbd.action_queue.critical')}
</span>
)}
{(actionQueue.importantCount || 0) > 0 && (
@@ -576,7 +586,7 @@ export function ActionQueueCard({
color: 'var(--color-warning-800)',
}}
>
{actionQueue.importantCount || 0} {t('jtbd.action_queue.important')}
{actionQueue.importantCount || 0} {tReasoning('jtbd.action_queue.important')}
</span>
)}
</div>
@@ -608,8 +618,8 @@ export function ActionQueueCard({
}}
>
{showAll
? t('jtbd.action_queue.show_less')
: t('jtbd.action_queue.show_more', { count: (actionQueue.totalActions || 3) - 3 })}
? tReasoning('jtbd.action_queue.show_less')
: tReasoning('jtbd.action_queue.show_more', { count: (actionQueue.totalActions || 3) - 3 })}
</button>
)}
</div>

View File

@@ -12,6 +12,7 @@ import React from 'react';
import { CheckCircle, AlertTriangle, AlertCircle, Clock, RefreshCw } from 'lucide-react';
import { BakeryHealthStatus } from '../../api/hooks/newDashboard';
import { formatDistanceToNow } from 'date-fns';
import { es, eu, enUS } from 'date-fns/locale';
import { useTranslation } from 'react-i18next';
interface HealthStatusCardProps {
@@ -50,7 +51,10 @@ const iconMap = {
};
export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProps) {
const { t } = useTranslation('reasoning');
const { t, i18n } = useTranslation('reasoning');
// Get date-fns locale based on current language
const dateLocale = i18n.language === 'es' ? es : i18n.language === 'eu' ? eu : enUS;
if (loading || !healthStatus) {
return (
@@ -81,7 +85,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<div className="flex-1 min-w-0">
<h2 className="text-xl md:text-2xl font-bold mb-2" style={{ color: config.textColor }}>
{typeof healthStatus.headline === 'object' && healthStatus.headline?.key
? t(healthStatus.headline.key.replace('.', ':'), healthStatus.headline.params || {})
? t(healthStatus.headline.key, healthStatus.headline.params || {})
: healthStatus.headline || t(`jtbd.health_status.${status}`)}
</h2>
@@ -93,6 +97,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
{healthStatus.lastOrchestrationRun
? formatDistanceToNow(new Date(healthStatus.lastOrchestrationRun), {
addSuffix: true,
locale: dateLocale,
})
: t('jtbd.health_status.never')}
</span>
@@ -104,7 +109,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
<RefreshCw className="w-4 h-4" />
<span>
{t('jtbd.health_status.next_check')}:{' '}
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true })}
{formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true, locale: dateLocale })}
</span>
</div>
)}

View File

@@ -9,6 +9,7 @@
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Insights } from '../../api/hooks/newDashboard';
interface InsightsGridProps {
@@ -38,18 +39,33 @@ const colorConfig = {
};
function InsightCard({
label,
value,
detail,
color,
i18n,
}: {
label: string;
value: string;
detail: string;
color: 'green' | 'amber' | 'red';
i18n: {
label: {
key: string;
params?: Record<string, any>;
};
value: {
key: string;
params?: Record<string, any>;
};
detail: {
key: string;
params?: Record<string, any>;
} | null;
};
}) {
const { t } = useTranslation('dashboard');
const config = colorConfig[color];
// Translate using i18n keys
const displayLabel = t(i18n.label.key, i18n.label.params);
const displayValue = t(i18n.value.key, i18n.value.params);
const displayDetail = i18n.detail ? t(i18n.detail.key, i18n.detail.params) : '';
return (
<div
className="border-2 rounded-xl p-4 md:p-6 transition-all duration-200 hover:shadow-lg cursor-pointer"
@@ -59,13 +75,13 @@ function InsightCard({
}}
>
{/* Label */}
<div className="text-sm md:text-base font-bold mb-2" style={{ color: 'var(--text-primary)' }}>{label}</div>
<div className="text-sm md:text-base font-bold mb-2" style={{ color: 'var(--text-primary)' }}>{displayLabel}</div>
{/* Value */}
<div className="text-xl md:text-2xl font-bold mb-1" style={{ color: config.textColor }}>{value}</div>
<div className="text-xl md:text-2xl font-bold mb-1" style={{ color: config.textColor }}>{displayValue}</div>
{/* Detail */}
<div className="text-sm font-medium" style={{ color: config.detailColor }}>{detail}</div>
<div className="text-sm font-medium" style={{ color: config.detailColor }}>{displayDetail}</div>
</div>
);
}
@@ -93,28 +109,20 @@ export function InsightsGrid({ insights, loading }: InsightsGridProps) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<InsightCard
label={insights.savings?.label || 'Savings'}
value={insights.savings?.value || '€0'}
detail={insights.savings?.detail || 'This week'}
color={insights.savings?.color || 'green'}
color={insights.savings.color}
i18n={insights.savings.i18n}
/>
<InsightCard
label={insights.inventory?.label || 'Inventory'}
value={insights.inventory?.value || '0%'}
detail={insights.inventory?.detail || 'Status'}
color={insights.inventory?.color || 'green'}
color={insights.inventory.color}
i18n={insights.inventory.i18n}
/>
<InsightCard
label={insights.waste?.label || 'Waste'}
value={insights.waste?.value || '0%'}
detail={insights.waste?.detail || 'Reduction'}
color={insights.waste?.color || 'green'}
color={insights.waste.color}
i18n={insights.waste.i18n}
/>
<InsightCard
label={insights.deliveries?.label || 'Deliveries'}
value={insights.deliveries?.value || '0%'}
detail={insights.deliveries?.detail || 'On-time'}
color={insights.deliveries?.color || 'green'}
color={insights.deliveries.color}
i18n={insights.deliveries.i18n}
/>
</div>
);

View File

@@ -38,16 +38,20 @@ function TimelineItemCard({
}) {
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || 'var(--text-tertiary)';
const { formatBatchAction } = useReasoningFormatter();
const { t } = useTranslation('reasoning');
const { t } = useTranslation(['reasoning', 'dashboard']);
// Translate reasoning_data (or fallback to deprecated text field)
// Translate reasoning_data (or use new reasoning_i18n or fallback to deprecated text field)
// Memoize to prevent undefined values from being created on each render
const { reasoning } = useMemo(() => {
if (item.reasoning_data) {
if (item.reasoning_i18n) {
// Use new i18n structure if available
const { key, params } = item.reasoning_i18n;
return { reasoning: t(key, params, { defaultValue: item.reasoning || '' }) };
} else if (item.reasoning_data) {
return formatBatchAction(item.reasoning_data);
}
return { reasoning: item.reasoning || '' };
}, [item.reasoning_data, item.reasoning, formatBatchAction]);
}, [item.reasoning_i18n, item.reasoning_data, item.reasoning, formatBatchAction, t]);
const startTime = item.plannedStartTime
? new Date(item.plannedStartTime).toLocaleTimeString('en-US', {
@@ -97,7 +101,9 @@ function TimelineItemCard({
{/* Status and Progress */}
<div className="mb-3">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{item.statusText || 'Status'}</span>
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
{item.status_i18n ? t(item.status_i18n.key, item.status_i18n.params) : item.statusText || 'Status'}
</span>
{item.status === 'IN_PROGRESS' && (
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>{item.progress || 0}%</span>
)}