Improve frontend 5
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user