diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 29410397..d9cb932f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -138,6 +138,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2267,7 +2268,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", "license": "MIT", - "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", @@ -2280,7 +2280,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -2290,7 +2289,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", @@ -2302,7 +2300,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" @@ -2313,7 +2310,6 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -2742,6 +2738,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6044,6 +6041,7 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-3.5.0.tgz", "integrity": "sha512-pKS3wZnJoL1iTyGBXAvCwduNNeghJHY6QSRSNNvpYnrrQrLZ6Owsazjyynu0e0ObRgks0i7Rv+pe2M7/MBTZpQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=12.16" } @@ -6133,6 +6131,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.89.0.tgz", "integrity": "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.89.0" }, @@ -6625,6 +6624,7 @@ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -6636,6 +6636,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -6777,6 +6778,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -7115,6 +7117,7 @@ "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "1.6.1", "fast-glob": "^3.3.2", @@ -7212,6 +7215,7 @@ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7754,6 +7758,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -7959,6 +7964,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -8323,7 +8329,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -8505,6 +8512,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -8547,8 +8555,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/decimal.js-light": { "version": "2.5.1", @@ -9032,6 +9039,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -9135,6 +9143,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10560,6 +10569,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -10608,6 +10618,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -12766,6 +12777,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12932,6 +12944,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13204,6 +13217,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13276,6 +13290,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13329,6 +13344,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -13920,6 +13936,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -14802,6 +14819,7 @@ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15333,6 +15351,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15745,6 +15764,7 @@ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -16308,6 +16328,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -16689,6 +16710,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/frontend/src/api/hooks/newDashboard.ts b/frontend/src/api/hooks/newDashboard.ts index 3cf0edac..0e433860 100644 --- a/frontend/src/api/hooks/newDashboard.ts +++ b/frontend/src/api/hooks/newDashboard.ts @@ -75,11 +75,17 @@ export interface OrchestrationSummary { userActionsRequired: number; durationSeconds: number | null; aiAssisted: boolean; - message?: string; + message_i18n?: { + key: string; + params?: Record; + }; // i18n data for message } export interface ActionButton { - label: string; + label_i18n: { + key: string; + params?: Record; + }; // i18n data for button label type: 'primary' | 'secondary' | 'tertiary'; action: string; } @@ -88,10 +94,25 @@ export interface ActionItem { id: string; type: string; urgency: 'critical' | 'important' | 'normal'; - title: string; - subtitle: string; - reasoning?: string; // Deprecated: Use reasoning_data instead - consequence?: string; // Deprecated: Use reasoning_data instead + title?: string; // Legacy field kept for alerts + title_i18n?: { + key: string; + params?: Record; + }; // i18n data for title + subtitle?: string; // Legacy field kept for alerts + subtitle_i18n?: { + key: string; + params?: Record; + }; // i18n data for subtitle + reasoning?: string; // Legacy field kept for alerts + reasoning_i18n?: { + key: string; + params?: Record; + }; // i18n data for reasoning + consequence_i18n: { + key: string; + params?: Record; + }; // i18n data for consequence reasoning_data?: any; // Structured reasoning data for i18n translation amount?: number; currency?: string; @@ -123,6 +144,14 @@ export interface ProductionTimelineItem { priority: string; reasoning?: string; // Deprecated: Use reasoning_data instead reasoning_data?: any; // Structured reasoning data for i18n translation + reasoning_i18n?: { + key: string; + params?: Record; + }; // i18n data for reasoning + status_i18n?: { + key: string; + params?: Record; + }; // i18n data for status text } export interface ProductionTimeline { @@ -134,10 +163,21 @@ export interface ProductionTimeline { } export interface InsightCard { - label: string; - value: string; - detail: string; color: 'green' | 'amber' | 'red'; + i18n: { + label: { + key: string; + params?: Record; + }; + value: { + key: string; + params?: Record; + }; + detail: { + key: string; + params?: Record; + } | null; + }; } export interface Insights { diff --git a/frontend/src/components/dashboard/ActionQueueCard.tsx b/frontend/src/components/dashboard/ActionQueueCard.tsx index 0e3a8583..0298ebc0 100644 --- a/frontend/src/components/dashboard/ActionQueueCard.tsx +++ b/frontend/src/components/dashboard/ActionQueueCard.tsx @@ -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 (
-

{action.title || 'Action Required'}

+

+ {action.title_i18n ? tDashboard(action.title_i18n.key, action.title_i18n.params) : (action.title || 'Action Required')} +

-

{action.subtitle || ''}

+

+ {action.subtitle_i18n ? tDashboard(action.subtitle_i18n.key, action.subtitle_i18n.params) : (action.subtitle || '')} +

@@ -152,7 +167,7 @@ function ActionItemCard({ {/* Reasoning (always visible) */}

- {t('jtbd.action_queue.why_needed')} + {tReasoning('jtbd.action_queue.why_needed')}

{reasoning}

@@ -166,7 +181,7 @@ function ActionItemCard({ style={{ color: 'var(--text-secondary)' }} > {expanded ? : } - {t('jtbd.action_queue.what_if_not')} + {tReasoning('jtbd.action_queue.what_if_not')} {expanded && ( @@ -178,11 +193,6 @@ function ActionItemCard({ }} >

{consequence}

- {severity && ( - - {severity} - - )} )} @@ -343,7 +353,7 @@ function ActionItemCard({
- {t('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes || 5} min + {tReasoning('jtbd.action_queue.estimated_time')}: {action.estimatedTimeMinutes || 5} min
@@ -483,7 +493,7 @@ function ActionItemCard({ {button.action === 'reject' && } {button.action === 'view_details' && } {button.action === 'modify' && } - {button.label} + {tDashboard(button.label_i18n.key, button.label_i18n.params)} ); })} @@ -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({ >

- {t('jtbd.action_queue.all_caught_up')} + {tReasoning('jtbd.action_queue.all_caught_up')}

-

{t('jtbd.action_queue.no_actions')}

+

{tReasoning('jtbd.action_queue.no_actions')}

); } @@ -540,7 +550,7 @@ export function ActionQueueCard({
{/* Header */}
-

{t('jtbd.action_queue.title')}

+

{tReasoning('jtbd.action_queue.title')}

{(actionQueue.totalActions || 0) > 3 && ( - {actionQueue.totalActions || 0} {t('jtbd.action_queue.total')} + {actionQueue.totalActions || 0} {tReasoning('jtbd.action_queue.total')} )}
@@ -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')} )} {(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')} )}
@@ -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 })} )} diff --git a/frontend/src/components/dashboard/HealthStatusCard.tsx b/frontend/src/components/dashboard/HealthStatusCard.tsx index 0e3c3579..5f89995d 100644 --- a/frontend/src/components/dashboard/HealthStatusCard.tsx +++ b/frontend/src/components/dashboard/HealthStatusCard.tsx @@ -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

{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}`)}

@@ -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')} @@ -104,7 +109,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp {t('jtbd.health_status.next_check')}:{' '} - {formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true })} + {formatDistanceToNow(new Date(healthStatus.nextScheduledRun), { addSuffix: true, locale: dateLocale })}
)} diff --git a/frontend/src/components/dashboard/InsightsGrid.tsx b/frontend/src/components/dashboard/InsightsGrid.tsx index a2223eba..f13db368 100644 --- a/frontend/src/components/dashboard/InsightsGrid.tsx +++ b/frontend/src/components/dashboard/InsightsGrid.tsx @@ -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; + }; + value: { + key: string; + params?: Record; + }; + detail: { + key: string; + params?: Record; + } | 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 (
{/* Label */} -
{label}
+
{displayLabel}
{/* Value */} -
{value}
+
{displayValue}
{/* Detail */} -
{detail}
+
{displayDetail}
); } @@ -93,28 +109,20 @@ export function InsightsGrid({ insights, loading }: InsightsGridProps) { return (
); diff --git a/frontend/src/components/dashboard/ProductionTimelineCard.tsx b/frontend/src/components/dashboard/ProductionTimelineCard.tsx index 0519a11d..de41e6a2 100644 --- a/frontend/src/components/dashboard/ProductionTimelineCard.tsx +++ b/frontend/src/components/dashboard/ProductionTimelineCard.tsx @@ -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 */}
- {item.statusText || 'Status'} + + {item.status_i18n ? t(item.status_i18n.key, item.status_i18n.params) : item.statusText || 'Status'} + {item.status === 'IN_PROGRESS' && ( {item.progress || 0}% )} diff --git a/frontend/src/components/domain/recipes/RecipeInstructionsEditor.tsx b/frontend/src/components/domain/recipes/RecipeInstructionsEditor.tsx new file mode 100644 index 00000000..5611b05c --- /dev/null +++ b/frontend/src/components/domain/recipes/RecipeInstructionsEditor.tsx @@ -0,0 +1,186 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { Plus, Trash2, FileText, Clock } from 'lucide-react'; + +interface RecipeInstructionsEditorProps { + value: any; + onChange: (value: any) => void; +} + +export const RecipeInstructionsEditor: React.FC = ({ value, onChange }) => { + // Initialize with empty steps array if value is empty/null + const initialSteps = useMemo(() => { + if (!value) { + return [{ step: 1, title: '', description: '', duration_minutes: '' }]; + } + + // Handle structured format with steps + if (value && value.steps && Array.isArray(value.steps)) { + return value.steps.map((step: any, index: number) => ({ + step: step.step || index + 1, + title: step.title || '', + description: step.description || '', + duration_minutes: step.duration_minutes || '' + })); + } + + // Handle array format + if (Array.isArray(value)) { + return value.map((item: any, index: number) => { + if (typeof item === 'object' && item !== null) { + return { + step: item.step || index + 1, + title: item.title || '', + description: item.description || '', + duration_minutes: item.duration_minutes || '' + }; + } + return { + step: index + 1, + title: '', + description: typeof item === 'string' ? item : '', + duration_minutes: '' + }; + }); + } + + // For any other format, start with one empty step + return [{ step: 1, title: '', description: '', duration_minutes: '' }]; + }, [value]); + + const [steps, setSteps] = useState(initialSteps); + + // Update parent when steps change + useEffect(() => { + // Format as structured object with steps array + const formattedSteps = steps.map((step, index) => ({ + ...step, + step: index + 1 // Ensure steps are numbered sequentially + })); + + onChange({ + steps: formattedSteps + }); + }, [steps, onChange]); + + const addStep = () => { + const newStepNumber = steps.length + 1; + setSteps([...steps, { step: newStepNumber, title: '', description: '', duration_minutes: '' }]); + }; + + const removeStep = (index: number) => { + if (steps.length <= 1) return; // Don't remove the last step + const newSteps = steps.filter((_, i) => i !== index); + // Renumber the steps after removal + const renumberedSteps = newSteps.map((step, i) => ({ + ...step, + step: i + 1 + })); + setSteps(renumberedSteps); + }; + + const updateStep = (index: number, field: string, newValue: string | number) => { + const newSteps = [...steps]; + newSteps[index] = { ...newSteps[index], [field]: newValue }; + setSteps(newSteps); + }; + + return ( +
+
+

Pasos de Preparación

+ +
+ +
+ {steps.map((step, index) => ( +
+
+
+
+ {step.step || index + 1} +
+
Paso {step.step || index + 1}
+
+ {steps.length > 1 && ( + + )} +
+ +
+ {/* Step Title */} +
+ + updateStep(index, 'title', e.target.value)} + className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm" + placeholder="Ej: Amasado, Fermentación, etc." + /> +
+ + {/* Step Description */} +
+ +