Improve the UI and tests
This commit is contained in:
409
frontend/src/components/dashboard/PurchaseOrderDetailsModal.tsx
Normal file
409
frontend/src/components/dashboard/PurchaseOrderDetailsModal.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
// ================================================================
|
||||
// frontend/src/components/dashboard/PurchaseOrderDetailsModal.tsx
|
||||
// ================================================================
|
||||
/**
|
||||
* Purchase Order Details Modal
|
||||
* Quick view of PO details from the Action Queue
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
X,
|
||||
Package,
|
||||
Building2,
|
||||
Calendar,
|
||||
Truck,
|
||||
Euro,
|
||||
FileText,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePurchaseOrder } from '../../api/hooks/purchase-orders';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { es, enUS, eu as euLocale } from 'date-fns/locale';
|
||||
|
||||
interface PurchaseOrderDetailsModalProps {
|
||||
poId: string;
|
||||
tenantId: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onApprove?: (poId: string) => void;
|
||||
onModify?: (poId: string) => void;
|
||||
}
|
||||
|
||||
const localeMap = {
|
||||
es: es,
|
||||
en: enUS,
|
||||
eu: euLocale,
|
||||
};
|
||||
|
||||
export const PurchaseOrderDetailsModal: React.FC<PurchaseOrderDetailsModalProps> = ({
|
||||
poId,
|
||||
tenantId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onApprove,
|
||||
onModify,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { data: po, isLoading } = usePurchaseOrder(tenantId, poId);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const dateLocale = localeMap[i18n.language as keyof typeof localeMap] || enUS;
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (value: any) => {
|
||||
const num = Number(value);
|
||||
return isNaN(num) ? '0.00' : num.toFixed(2);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
draft: {
|
||||
bg: 'var(--color-gray-100)',
|
||||
text: 'var(--color-gray-700)',
|
||||
label: t('purchase_orders:status.draft'),
|
||||
},
|
||||
pending_approval: {
|
||||
bg: 'var(--color-warning-100)',
|
||||
text: 'var(--color-warning-700)',
|
||||
label: t('purchase_orders:status.pending_approval'),
|
||||
},
|
||||
approved: {
|
||||
bg: 'var(--color-success-100)',
|
||||
text: 'var(--color-success-700)',
|
||||
label: t('purchase_orders:status.approved'),
|
||||
},
|
||||
sent: {
|
||||
bg: 'var(--color-info-100)',
|
||||
text: 'var(--color-info-700)',
|
||||
label: t('purchase_orders:status.sent'),
|
||||
},
|
||||
partially_received: {
|
||||
bg: 'var(--color-warning-100)',
|
||||
text: 'var(--color-warning-700)',
|
||||
label: t('purchase_orders:status.partially_received'),
|
||||
},
|
||||
received: {
|
||||
bg: 'var(--color-success-100)',
|
||||
text: 'var(--color-success-700)',
|
||||
label: t('purchase_orders:status.received'),
|
||||
},
|
||||
cancelled: {
|
||||
bg: 'var(--color-error-100)',
|
||||
text: 'var(--color-error-700)',
|
||||
label: t('purchase_orders:status.cancelled'),
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.draft;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="px-3 py-1 rounded-full text-sm font-medium"
|
||||
style={{ backgroundColor: config.bg, color: config.text }}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 animate-fadeIn"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
backdropFilter: 'blur(4px)',
|
||||
animation: 'fadeIn 0.2s ease-out'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden transform transition-all"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
animation: 'slideUp 0.3s ease-out'
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="flex items-center justify-between p-6 border-b bg-gradient-to-r from-transparent to-transparent"
|
||||
style={{
|
||||
borderColor: 'var(--border-primary)',
|
||||
background: 'linear-gradient(to right, var(--bg-primary), var(--bg-secondary))'
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="p-3 rounded-xl"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-primary-100)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)'
|
||||
}}
|
||||
>
|
||||
<Package className="w-6 h-6" style={{ color: 'var(--color-primary-600)' }} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||
{isLoading ? t('common:loading') : po?.po_number || t('purchase_orders:purchase_order')}
|
||||
</h2>
|
||||
{po && (
|
||||
<p className="text-sm flex items-center gap-1" style={{ color: 'var(--text-secondary)' }}>
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{t('purchase_orders:created')} {formatDistanceToNow(new Date(po.created_at), { addSuffix: true, locale: dateLocale })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 transition-all hover:scale-110"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
aria-label={t('purchase_orders:actions.close')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-3 border-primary-200 border-t-primary-600"></div>
|
||||
</div>
|
||||
) : po ? (
|
||||
<div className="space-y-6">
|
||||
{/* Status and Key Info */}
|
||||
<div className="flex flex-wrap items-center gap-4 p-4 rounded-xl" style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
border: '1px solid var(--border-primary)'
|
||||
}}>
|
||||
{getStatusBadge(po.status)}
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<p className="text-xs uppercase tracking-wide mb-1" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('purchase_orders:total_amount')}
|
||||
</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<Euro className="w-5 h-5" style={{ color: 'var(--color-primary-600)' }} />
|
||||
<span className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
{formatCurrency(po.total_amount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Supplier Info */}
|
||||
<div className="rounded-xl p-5 border" style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border-primary)',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)'
|
||||
}}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="p-2 rounded-lg" style={{ backgroundColor: 'var(--color-primary-100)' }}>
|
||||
<Building2 className="w-5 h-5" style={{ color: 'var(--color-primary-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm uppercase tracking-wide" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('purchase_orders:supplier')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-xl font-semibold ml-1" style={{ color: 'var(--text-primary)' }}>
|
||||
{po.supplier_name || t('common:unknown')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="rounded-xl p-4 border" style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border-primary)',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)'
|
||||
}}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="p-1.5 rounded-lg" style={{ backgroundColor: 'var(--color-primary-100)' }}>
|
||||
<Calendar className="w-4 h-4" style={{ color: 'var(--color-primary-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('purchase_orders:order_date')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-lg font-semibold ml-1" style={{ color: 'var(--text-primary)' }}>
|
||||
{new Date(po.order_date).toLocaleDateString(i18n.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{po.expected_delivery_date && (
|
||||
<div className="rounded-xl p-4 border" style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border-primary)',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)'
|
||||
}}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="p-1.5 rounded-lg" style={{ backgroundColor: 'var(--color-success-100)' }}>
|
||||
<Truck className="w-4 h-4" style={{ color: 'var(--color-success-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('purchase_orders:expected_delivery')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-lg font-semibold ml-1" style={{ color: 'var(--text-primary)' }}>
|
||||
{new Date(po.expected_delivery_date).toLocaleDateString(i18n.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="p-2 rounded-lg" style={{ backgroundColor: 'var(--color-primary-100)' }}>
|
||||
<FileText className="w-5 h-5" style={{ color: 'var(--color-primary-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-bold text-lg" style={{ color: 'var(--text-primary)' }}>
|
||||
{t('purchase_orders:items')}
|
||||
</h3>
|
||||
{po.items && po.items.length > 0 && (
|
||||
<span
|
||||
className="ml-auto px-3 py-1 rounded-full text-sm font-medium"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-primary-100)',
|
||||
color: 'var(--color-primary-700)'
|
||||
}}
|
||||
>
|
||||
{po.items.length} {po.items.length === 1 ? t('common:item') : t('common:items')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{po.items && po.items.length > 0 ? (
|
||||
po.items.map((item: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex justify-between items-center p-4 rounded-xl border transition-all hover:shadow-md"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderColor: 'var(--border-primary)'
|
||||
}}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-base mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||
{item.ingredient_name || item.product_name}
|
||||
</p>
|
||||
<p className="text-sm flex items-center gap-1.5" style={{ color: 'var(--text-secondary)' }}>
|
||||
<span className="font-medium">{item.quantity} {item.unit}</span>
|
||||
<span>×</span>
|
||||
<span>€{formatCurrency(item.unit_price)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-bold text-lg ml-4" style={{ color: 'var(--color-primary-600)' }}>
|
||||
€{formatCurrency(item.subtotal)}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 rounded-xl border-2 border-dashed" style={{
|
||||
borderColor: 'var(--border-primary)',
|
||||
color: 'var(--text-secondary)'
|
||||
}}>
|
||||
<FileText className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
||||
<p>{t('purchase_orders:no_items')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
{po.notes && (
|
||||
<div className="rounded-xl p-5 border-l-4" style={{
|
||||
backgroundColor: 'var(--color-info-50)',
|
||||
borderLeftColor: 'var(--color-info-500)',
|
||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)'
|
||||
}}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="p-1.5 rounded-lg" style={{ backgroundColor: 'var(--color-info-100)' }}>
|
||||
<AlertCircle className="w-5 h-5" style={{ color: 'var(--color-info-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm uppercase tracking-wide" style={{ color: 'var(--color-info-700)' }}>
|
||||
{t('purchase_orders:notes')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed ml-1" style={{ color: 'var(--text-primary)' }}>{po.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p style={{ color: 'var(--text-secondary)' }}>{t('purchase_orders:not_found')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
{po && po.status === 'pending_approval' && (
|
||||
<div
|
||||
className="flex justify-end gap-3 p-6 border-t bg-gradient-to-r from-transparent to-transparent"
|
||||
style={{
|
||||
borderColor: 'var(--border-primary)',
|
||||
background: 'linear-gradient(to right, var(--bg-secondary), var(--bg-primary))'
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
onModify?.(poId);
|
||||
onClose();
|
||||
}}
|
||||
className="px-6 py-2.5 rounded-xl font-semibold transition-all hover:scale-105 hover:shadow-md border"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
color: 'var(--text-primary)',
|
||||
borderColor: 'var(--border-primary)'
|
||||
}}
|
||||
>
|
||||
{t('purchase_orders:actions.modify')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onApprove?.(poId);
|
||||
onClose();
|
||||
}}
|
||||
className="px-6 py-2.5 rounded-xl font-semibold text-white transition-all hover:scale-105 hover:shadow-lg flex items-center gap-2"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-primary-600)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{t('purchase_orders:actions.approve')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,10 @@ export const DemoBanner: React.FC = () => {
|
||||
const { useTenantStore } = await import('../../../stores/tenant.store');
|
||||
useTenantStore.getState().clearTenants();
|
||||
|
||||
// Clear notification storage to ensure notifications don't persist across sessions
|
||||
const { clearNotificationStorage } = await import('../../../hooks/useNotifications');
|
||||
clearNotificationStorage();
|
||||
|
||||
navigate('/demo');
|
||||
};
|
||||
|
||||
|
||||
@@ -16,46 +16,59 @@ export interface NotificationData {
|
||||
const STORAGE_KEY = 'bakery-notifications';
|
||||
const SNOOZE_STORAGE_KEY = 'bakery-snoozed-alerts';
|
||||
|
||||
/**
|
||||
* Clear all notification data from sessionStorage
|
||||
* This is typically called during logout to ensure notifications don't persist across sessions
|
||||
*/
|
||||
export const clearNotificationStorage = () => {
|
||||
try {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
sessionStorage.removeItem(SNOOZE_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear notification storage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadNotificationsFromStorage = (): NotificationData[] => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const stored = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (Array.isArray(parsed)) {
|
||||
// Clean up old alerts (older than 24 hours)
|
||||
// This prevents accumulation of stale alerts in localStorage
|
||||
// This prevents accumulation of stale alerts in sessionStorage
|
||||
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
|
||||
const recentAlerts = parsed.filter(n => {
|
||||
const alertTime = new Date(n.timestamp).getTime();
|
||||
return alertTime > oneDayAgo;
|
||||
});
|
||||
|
||||
// If we filtered out alerts, update localStorage
|
||||
// If we filtered out alerts, update sessionStorage
|
||||
if (recentAlerts.length !== parsed.length) {
|
||||
console.log(`Cleaned ${parsed.length - recentAlerts.length} old alerts from localStorage`);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(recentAlerts));
|
||||
console.log(`Cleaned ${parsed.length - recentAlerts.length} old alerts from sessionStorage`);
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(recentAlerts));
|
||||
}
|
||||
|
||||
return recentAlerts;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load notifications from localStorage:', error);
|
||||
console.warn('Failed to load notifications from sessionStorage:', error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const saveNotificationsToStorage = (notifications: NotificationData[]) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(notifications));
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(notifications));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save notifications to localStorage:', error);
|
||||
console.warn('Failed to save notifications to sessionStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSnoozedAlertsFromStorage = (): Map<string, SnoozedAlert> => {
|
||||
try {
|
||||
const stored = localStorage.getItem(SNOOZE_STORAGE_KEY);
|
||||
const stored = sessionStorage.getItem(SNOOZE_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
const map = new Map<string, SnoozedAlert>();
|
||||
@@ -71,7 +84,7 @@ const loadSnoozedAlertsFromStorage = (): Map<string, SnoozedAlert> => {
|
||||
return map;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load snoozed alerts from localStorage:', error);
|
||||
console.warn('Failed to load snoozed alerts from sessionStorage:', error);
|
||||
}
|
||||
return new Map();
|
||||
};
|
||||
@@ -85,9 +98,9 @@ const saveSnoozedAlertsToStorage = (snoozedAlerts: Map<string, SnoozedAlert>) =>
|
||||
obj[key] = value;
|
||||
}
|
||||
});
|
||||
localStorage.setItem(SNOOZE_STORAGE_KEY, JSON.stringify(obj));
|
||||
sessionStorage.setItem(SNOOZE_STORAGE_KEY, JSON.stringify(obj));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save snoozed alerts to localStorage:', error);
|
||||
console.warn('Failed to save snoozed alerts to sessionStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,11 +109,12 @@ const saveSnoozedAlertsToStorage = (snoozedAlerts: Map<string, SnoozedAlert>) =>
|
||||
*
|
||||
* Features:
|
||||
* - SSE connection for real-time alerts
|
||||
* - localStorage persistence with auto-cleanup (alerts >24h are removed on load)
|
||||
* - sessionStorage persistence with auto-cleanup (alerts >24h are removed on load)
|
||||
* - Snooze functionality with expiration tracking
|
||||
* - Bulk operations (mark multiple as read, remove, snooze)
|
||||
*
|
||||
* Note: localStorage is automatically cleaned of alerts older than 24 hours
|
||||
* Note: Notifications are session-only and cleared when the browser tab/window closes
|
||||
* or when the user logs out. Alerts older than 24 hours are automatically cleaned
|
||||
* on load to prevent accumulation of stale data.
|
||||
*/
|
||||
export const useNotifications = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// frontend/src/i18n/index.ts
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import ICU from 'i18next-icu';
|
||||
import { resources, defaultLanguage, supportedLanguages } from '../locales';
|
||||
|
||||
// Get saved language from localStorage or default
|
||||
@@ -24,6 +25,7 @@ const getSavedLanguage = () => {
|
||||
const initialLanguage = getSavedLanguage();
|
||||
|
||||
i18n
|
||||
.use(ICU)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse"
|
||||
},
|
||||
"item": "item",
|
||||
"items": "items",
|
||||
"unknown": "Unknown",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
|
||||
@@ -34,6 +34,32 @@
|
||||
"quick_actions": "Quick Actions",
|
||||
"key_metrics": "Key Metrics"
|
||||
},
|
||||
"insights": {
|
||||
"savings": {
|
||||
"label": "💰 SAVINGS",
|
||||
"this_week": "this week",
|
||||
"vs_last": "vs. last"
|
||||
},
|
||||
"inventory": {
|
||||
"label": "📦 INVENTORY",
|
||||
"all_stocked": "All stocked",
|
||||
"low_stock": "Low stock",
|
||||
"stock_issues": "⚠️ Stock issues",
|
||||
"no_alerts": "No alerts",
|
||||
"out_of_stock": "{count} out of stock",
|
||||
"alerts": "{count} alert{count, plural, one {} other {s}}"
|
||||
},
|
||||
"waste": {
|
||||
"label": "♻️ WASTE",
|
||||
"this_month": "this month",
|
||||
"vs_goal": "vs. goal"
|
||||
},
|
||||
"deliveries": {
|
||||
"label": "🚚 DELIVERIES",
|
||||
"arriving_today": "{count} arriving today",
|
||||
"none_scheduled": "None scheduled"
|
||||
}
|
||||
},
|
||||
"procurement": {
|
||||
"title": "What needs to be bought for tomorrow?",
|
||||
"empty": "All supplies ready for tomorrow",
|
||||
@@ -72,8 +98,8 @@
|
||||
"view_all": "View all alerts",
|
||||
"time": {
|
||||
"now": "Now",
|
||||
"minutes_ago": "{{count}} min ago",
|
||||
"hours_ago": "{{count}} h ago",
|
||||
"minutes_ago": "{count} min ago",
|
||||
"hours_ago": "{count} h ago",
|
||||
"yesterday": "Yesterday"
|
||||
},
|
||||
"types": {
|
||||
@@ -101,7 +127,7 @@
|
||||
"additional_details": "Additional Details",
|
||||
"mark_as_read": "Mark as read",
|
||||
"remove": "Remove",
|
||||
"active_count": "{{count}} active alerts"
|
||||
"active_count": "{count} active alerts"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "Welcome back",
|
||||
@@ -131,16 +157,16 @@
|
||||
},
|
||||
"health": {
|
||||
"production_on_schedule": "Production on schedule",
|
||||
"production_delayed": "{{count}} production batch{{count, plural, one {} other {es}}} delayed",
|
||||
"production_delayed": "{count} production batch{count, plural, one {} other {es}} delayed",
|
||||
"all_ingredients_in_stock": "All ingredients in stock",
|
||||
"ingredients_out_of_stock": "{{count}} ingredient{{count, plural, one {} other {s}}} out of stock",
|
||||
"ingredients_out_of_stock": "{count} ingredient{count, plural, one {} other {s}} out of stock",
|
||||
"no_pending_approvals": "No pending approvals",
|
||||
"approvals_awaiting": "{{count}} purchase order{{count, plural, one {} other {s}}} awaiting approval",
|
||||
"approvals_awaiting": "{count} purchase order{count, plural, one {} other {s}} awaiting approval",
|
||||
"all_systems_operational": "All systems operational",
|
||||
"critical_issues": "{{count}} critical issue{{count, plural, one {} other {s}}}",
|
||||
"critical_issues": "{count} critical issue{count, plural, one {} other {s}}",
|
||||
"headline_green": "Your bakery is running smoothly",
|
||||
"headline_yellow_approvals": "Please review {{count}} pending approval{{count, plural, one {} other {s}}}",
|
||||
"headline_yellow_alerts": "You have {{count}} alert{{count, plural, one {} other {s}}} needing attention",
|
||||
"headline_yellow_approvals": "Please review {count} pending approval{count, plural, one {} other {s}}",
|
||||
"headline_yellow_alerts": "You have {count} alert{count, plural, one {} other {s}} needing attention",
|
||||
"headline_yellow_general": "Some items need your attention",
|
||||
"headline_red": "Critical issues require immediate action"
|
||||
},
|
||||
@@ -160,7 +186,7 @@
|
||||
"suppliers": "Suppliers",
|
||||
"recipes": "Recipes",
|
||||
"quality": "Quality Standards",
|
||||
"add_ingredients": "Add at least {{count}} ingredients",
|
||||
"add_ingredients": "Add at least {count} ingredients",
|
||||
"add_supplier": "Add your first supplier",
|
||||
"add_recipe": "Create your first recipe",
|
||||
"add_quality": "Add quality checks (optional)",
|
||||
|
||||
36
frontend/src/locales/en/purchase_orders.json
Normal file
36
frontend/src/locales/en/purchase_orders.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"purchase_order": "Purchase Order",
|
||||
"purchase_orders": "Purchase Orders",
|
||||
"created": "Created",
|
||||
"supplier": "Supplier",
|
||||
"order_date": "Order Date",
|
||||
"expected_delivery": "Expected Delivery",
|
||||
"items": "Items",
|
||||
"no_items": "No items in this order",
|
||||
"notes": "Notes",
|
||||
"not_found": "Purchase order not found",
|
||||
"total_amount": "Total Amount",
|
||||
"status": {
|
||||
"draft": "Draft",
|
||||
"pending_approval": "Pending Approval",
|
||||
"approved": "Approved",
|
||||
"sent": "Sent",
|
||||
"partially_received": "Partially Received",
|
||||
"received": "Received",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"details": {
|
||||
"title": "Purchase Order Details",
|
||||
"quick_view": "Quick Order View",
|
||||
"summary": "Summary",
|
||||
"supplier_info": "Supplier Information",
|
||||
"delivery_info": "Delivery Information",
|
||||
"order_items": "Order Items",
|
||||
"additional_notes": "Additional Notes"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Approve Order",
|
||||
"modify": "Modify Order",
|
||||
"close": "Close"
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@
|
||||
"last_updated": "Last updated",
|
||||
"next_check": "Next check",
|
||||
"never": "Never",
|
||||
"critical_issues": "{{count}} critical issue{{count, plural, one {} other {s}}}",
|
||||
"actions_needed": "{{count}} action{{count, plural, one {} other {s}}} needed"
|
||||
"critical_issues": "{count} critical issue{count, plural, one {} other {s}}",
|
||||
"actions_needed": "{count} action{count, plural, one {} other {s}} needed"
|
||||
},
|
||||
"action_queue": {
|
||||
"title": "What Needs Your Attention",
|
||||
@@ -85,11 +85,23 @@
|
||||
"estimated_time": "Estimated time",
|
||||
"all_caught_up": "All caught up!",
|
||||
"no_actions": "No actions requiring your attention right now.",
|
||||
"show_more": "Show {{count}} More Action{{count, plural, one {} other {s}}}",
|
||||
"show_more": "Show {count} More Action{count, plural, one {} other {s}}",
|
||||
"show_less": "Show Less",
|
||||
"total": "total",
|
||||
"critical": "critical",
|
||||
"important": "important"
|
||||
"important": "important",
|
||||
"consequences": {
|
||||
"delayed_delivery_impact": "Delayed delivery may impact production schedule",
|
||||
"immediate_action_required": "Immediate action required to prevent production issues",
|
||||
"some_features_limited": "Some features are limited"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Approve",
|
||||
"view_details": "View Details",
|
||||
"modify": "Modify",
|
||||
"dismiss": "Dismiss",
|
||||
"complete_setup": "Complete Setup"
|
||||
}
|
||||
},
|
||||
"orchestration_summary": {
|
||||
"title": "Last Night I Planned Your Day",
|
||||
@@ -97,17 +109,17 @@
|
||||
"run_planning": "Run Daily Planning",
|
||||
"run_info": "Orchestration run #{{runNumber}}",
|
||||
"took": "Took {{seconds}}s",
|
||||
"created_pos": "Created {{count}} purchase order{{count, plural, one {} other {s}}}",
|
||||
"scheduled_batches": "Scheduled {{count}} production batch{{count, plural, one {} other {es}}}",
|
||||
"show_more": "Show {{count}} more",
|
||||
"created_pos": "Created {count} purchase order{count, plural, one {} other {s}}",
|
||||
"scheduled_batches": "Scheduled {count} production batch{count, plural, one {} other {es}}",
|
||||
"show_more": "Show {count} more",
|
||||
"show_less": "Show less",
|
||||
"no_actions": "No new actions needed - everything is on track!",
|
||||
"based_on": "Based on:",
|
||||
"customer_orders": "{{count}} customer order{{count, plural, one {} other {s}}}",
|
||||
"customer_orders": "{count} customer order{count, plural, one {} other {s}}",
|
||||
"historical_demand": "Historical demand",
|
||||
"inventory_levels": "Inventory levels",
|
||||
"ai_optimization": "AI optimization",
|
||||
"actions_required": "{{count}} item{{count, plural, one {} other {s}}} need{{count, plural, one {s} other {}}} your approval before proceeding",
|
||||
"actions_required": "{count} item{count, plural, one {} other {s}} need{count, plural, one {s} other {}}} your approval before proceeding",
|
||||
"no_tenant_error": "No tenant ID found. Please ensure you're logged in.",
|
||||
"planning_started": "Planning started successfully",
|
||||
"planning_failed": "Failed to start planning",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"title": "Add Inventory",
|
||||
"inventoryDetails": "Inventory Item Details",
|
||||
"fillRequiredInfo": "Fill in the required information to create an inventory item",
|
||||
"summary": "Summary",
|
||||
"steps": {
|
||||
"productType": "Product Type",
|
||||
"basicInfo": "Basic Information",
|
||||
"stockConfig": "Stock Configuration"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
"ingredient": "Raw materials and ingredients used in recipes",
|
||||
"finished_product": "Final products ready for sale or consumption"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
"namePlaceholder": "E.g., All-Purpose Flour, Sourdough Bread",
|
||||
@@ -81,6 +91,11 @@
|
||||
"basicInformation": "Basic Information",
|
||||
"advancedOptions": "Advanced Options",
|
||||
"advancedOptionsDescription": "Optional fields for comprehensive inventory management",
|
||||
"additionalInformationDescription": "Optional product identifiers",
|
||||
"additionalDetails": "Additional Details",
|
||||
"additionalDetailsDescription": "Optional product details",
|
||||
"advancedStockSettings": "Advanced Stock Settings",
|
||||
"advancedStockSettingsDescription": "Configure inventory thresholds and reorder points",
|
||||
"pricingInformation": "Pricing Information",
|
||||
"inventoryManagement": "Inventory Management",
|
||||
"productInformation": "Product Information",
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
"expand": "Expandir",
|
||||
"collapse": "Contraer"
|
||||
},
|
||||
"item": "artículo",
|
||||
"items": "artículos",
|
||||
"unknown": "Desconocido",
|
||||
"status": {
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo",
|
||||
|
||||
@@ -34,6 +34,32 @@
|
||||
"quick_actions": "Acciones Rápidas",
|
||||
"key_metrics": "Métricas Clave"
|
||||
},
|
||||
"insights": {
|
||||
"savings": {
|
||||
"label": "💰 AHORROS",
|
||||
"this_week": "esta semana",
|
||||
"vs_last": "vs. anterior"
|
||||
},
|
||||
"inventory": {
|
||||
"label": "📦 INVENTARIO",
|
||||
"all_stocked": "Todo en stock",
|
||||
"low_stock": "Stock bajo",
|
||||
"stock_issues": "⚠️ Problemas de stock",
|
||||
"no_alerts": "Sin alertas",
|
||||
"out_of_stock": "{count} sin stock",
|
||||
"alerts": "{count} alerta{count, plural, one {} other {s}}"
|
||||
},
|
||||
"waste": {
|
||||
"label": "♻️ DESPERDICIO",
|
||||
"this_month": "este mes",
|
||||
"vs_goal": "vs. objetivo"
|
||||
},
|
||||
"deliveries": {
|
||||
"label": "🚚 ENTREGAS",
|
||||
"arriving_today": "{count} llegan hoy",
|
||||
"none_scheduled": "Ninguna programada"
|
||||
}
|
||||
},
|
||||
"procurement": {
|
||||
"title": "¿Qué necesito comprar para mañana?",
|
||||
"empty": "Todos los suministros listos para mañana",
|
||||
@@ -166,16 +192,16 @@
|
||||
},
|
||||
"health": {
|
||||
"production_on_schedule": "Producción a tiempo",
|
||||
"production_delayed": "{{count}} lote{{count, plural, one {} other {s}}} de producción retrasado{{count, plural, one {} other {s}}}",
|
||||
"production_delayed": "{count} lote{count, plural, one {} other {s}} de producción retrasado{count, plural, one {} other {s}}",
|
||||
"all_ingredients_in_stock": "Todos los ingredientes en stock",
|
||||
"ingredients_out_of_stock": "{{count}} ingrediente{{count, plural, one {} other {s}}} sin stock",
|
||||
"ingredients_out_of_stock": "{count} ingrediente{count, plural, one {} other {s}} sin stock",
|
||||
"no_pending_approvals": "Sin aprobaciones pendientes",
|
||||
"approvals_awaiting": "{{count}} orden{{count, plural, one {} other {es}}} de compra esperando aprobación",
|
||||
"approvals_awaiting": "{count} orden{count, plural, one {} other {es}} de compra esperando aprobación",
|
||||
"all_systems_operational": "Todos los sistemas operativos",
|
||||
"critical_issues": "{{count}} problema{{count, plural, one {} other {s}}} crítico{{count, plural, one {} other {s}}}",
|
||||
"critical_issues": "{count} problema{count, plural, one {} other {s}} crítico{count, plural, one {} other {s}}",
|
||||
"headline_green": "Tu panadería funciona sin problemas",
|
||||
"headline_yellow_approvals": "Por favor revisa {{count}} aprobación{{count, plural, one {} other {es}}} pendiente{{count, plural, one {} other {s}}}",
|
||||
"headline_yellow_alerts": "Tienes {{count}} alerta{{count, plural, one {} other {s}}} que necesita{{count, plural, one {} other {n}}} atención",
|
||||
"headline_yellow_approvals": "Por favor revisa {count} aprobación{count, plural, one {} other {es}} pendiente{count, plural, one {} other {s}}",
|
||||
"headline_yellow_alerts": "Tienes {count} alerta{count, plural, one {} other {s}} que necesita{count, plural, one {} other {n}} atención",
|
||||
"headline_yellow_general": "Algunos elementos necesitan tu atención",
|
||||
"headline_red": "Problemas críticos requieren acción inmediata"
|
||||
},
|
||||
|
||||
36
frontend/src/locales/es/purchase_orders.json
Normal file
36
frontend/src/locales/es/purchase_orders.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"purchase_order": "Orden de Compra",
|
||||
"purchase_orders": "Órdenes de Compra",
|
||||
"created": "Creada",
|
||||
"supplier": "Proveedor",
|
||||
"order_date": "Fecha de Pedido",
|
||||
"expected_delivery": "Entrega Esperada",
|
||||
"items": "Artículos",
|
||||
"no_items": "No hay artículos en esta orden",
|
||||
"notes": "Notas",
|
||||
"not_found": "Orden de compra no encontrada",
|
||||
"total_amount": "Monto Total",
|
||||
"status": {
|
||||
"draft": "Borrador",
|
||||
"pending_approval": "Pendiente de Aprobación",
|
||||
"approved": "Aprobada",
|
||||
"sent": "Enviada",
|
||||
"partially_received": "Parcialmente Recibida",
|
||||
"received": "Recibida",
|
||||
"cancelled": "Cancelada"
|
||||
},
|
||||
"details": {
|
||||
"title": "Detalles de la Orden de Compra",
|
||||
"quick_view": "Vista Rápida de la Orden",
|
||||
"summary": "Resumen",
|
||||
"supplier_info": "Información del Proveedor",
|
||||
"delivery_info": "Información de Entrega",
|
||||
"order_items": "Artículos del Pedido",
|
||||
"additional_notes": "Notas Adicionales"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Aprobar Orden",
|
||||
"modify": "Modificar Orden",
|
||||
"close": "Cerrar"
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@
|
||||
"last_updated": "Última actualización",
|
||||
"next_check": "Próxima verificación",
|
||||
"never": "Nunca",
|
||||
"critical_issues": "{{count}} problema{{count, plural, one {} other {s}}} crítico{{count, plural, one {} other {s}}}",
|
||||
"actions_needed": "{{count}} acción{{count, plural, one {} other {es}}} necesaria{{count, plural, one {} other {s}}}"
|
||||
"critical_issues": "{count} problema{count, plural, one {} other {s}} crítico{count, plural, one {} other {s}}",
|
||||
"actions_needed": "{count} acción{count, plural, one {} other {es}} necesaria{count, plural, one {} other {s}}"
|
||||
},
|
||||
"action_queue": {
|
||||
"title": "Qué Necesita Tu Atención",
|
||||
@@ -85,11 +85,23 @@
|
||||
"estimated_time": "Tiempo estimado",
|
||||
"all_caught_up": "¡Todo al día!",
|
||||
"no_actions": "No hay acciones que requieran tu atención en este momento.",
|
||||
"show_more": "Mostrar {{count}} Acción{{count, plural, one {} other {es}}} Más",
|
||||
"show_more": "Mostrar {count} Acción{count, plural, one {} other {es}} Más",
|
||||
"show_less": "Mostrar Menos",
|
||||
"total": "total",
|
||||
"critical": "críticas",
|
||||
"important": "importantes"
|
||||
"important": "importantes",
|
||||
"consequences": {
|
||||
"delayed_delivery_impact": "El retraso en la entrega puede afectar el cronograma de producción",
|
||||
"immediate_action_required": "Se requiere acción inmediata para prevenir problemas de producción",
|
||||
"some_features_limited": "Algunas funciones están limitadas"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Aprobar",
|
||||
"view_details": "Ver Detalles",
|
||||
"modify": "Modificar",
|
||||
"dismiss": "Descartar",
|
||||
"complete_setup": "Completar Configuración"
|
||||
}
|
||||
},
|
||||
"orchestration_summary": {
|
||||
"title": "Anoche Planifiqué Tu Día",
|
||||
@@ -97,17 +109,17 @@
|
||||
"run_planning": "Ejecutar Planificación Diaria",
|
||||
"run_info": "Ejecución de orquestación #{{runNumber}}",
|
||||
"took": "Duró {{seconds}}s",
|
||||
"created_pos": "{{count}} orden{{count, plural, one {} other {es}}} de compra creada{{count, plural, one {} other {s}}}",
|
||||
"scheduled_batches": "{{count}} lote{{count, plural, one {} other {s}}} de producción programado{{count, plural, one {} other {s}}}",
|
||||
"show_more": "Mostrar {{count}} más",
|
||||
"created_pos": "{count} orden{count, plural, one {} other {es}} de compra creada{count, plural, one {} other {s}}",
|
||||
"scheduled_batches": "{count} lote{count, plural, one {} other {s}} de producción programado{count, plural, one {} other {s}}",
|
||||
"show_more": "Mostrar {count} más",
|
||||
"show_less": "Mostrar menos",
|
||||
"no_actions": "¡No se necesitan nuevas acciones - todo va según lo planeado!",
|
||||
"based_on": "Basado en:",
|
||||
"customer_orders": "{{count}} pedido{{count, plural, one {} other {s}}} de cliente",
|
||||
"customer_orders": "{count} pedido{count, plural, one {} other {s}} de cliente",
|
||||
"historical_demand": "Demanda histórica",
|
||||
"inventory_levels": "Niveles de inventario",
|
||||
"ai_optimization": "Optimización por IA",
|
||||
"actions_required": "{{count}} elemento{{count, plural, one {} other {s}}} necesita{{count, plural, one {} other {n}}} tu aprobación antes de continuar",
|
||||
"actions_required": "{count} elemento{count, plural, one {} other {s}} necesita{count, plural, one {} other {n}} tu aprobación antes de continuar",
|
||||
"no_tenant_error": "No se encontró ID de inquilino. Por favor, asegúrate de haber iniciado sesión.",
|
||||
"planning_started": "Planificación iniciada correctamente",
|
||||
"planning_failed": "Error al iniciar la planificación",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"title": "Agregar Inventario",
|
||||
"inventoryDetails": "Detalles del Artículo de Inventario",
|
||||
"fillRequiredInfo": "Complete la información requerida para crear un artículo de inventario",
|
||||
"summary": "Resumen",
|
||||
"steps": {
|
||||
"productType": "Tipo de Producto",
|
||||
"basicInfo": "Información Básica",
|
||||
"stockConfig": "Configuración de Stock"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
"ingredient": "Materias primas e ingredientes utilizados en recetas",
|
||||
"finished_product": "Productos finales listos para venta o consumo"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nombre",
|
||||
"namePlaceholder": "Ej: Harina de Uso General, Pan de Masa Madre",
|
||||
@@ -81,6 +91,11 @@
|
||||
"basicInformation": "Información Básica",
|
||||
"advancedOptions": "Opciones Avanzadas",
|
||||
"advancedOptionsDescription": "Campos opcionales para gestión completa de inventario",
|
||||
"additionalInformationDescription": "Identificadores de producto opcionales",
|
||||
"additionalDetails": "Detalles Adicionales",
|
||||
"additionalDetailsDescription": "Detalles opcionales del producto",
|
||||
"advancedStockSettings": "Configuración Avanzada de Stock",
|
||||
"advancedStockSettingsDescription": "Configurar umbrales de inventario y puntos de reorden",
|
||||
"pricingInformation": "Información de Precios",
|
||||
"inventoryManagement": "Gestión de Inventario",
|
||||
"productInformation": "Información del Producto",
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
"expand": "Zabaldu",
|
||||
"collapse": "Tolestu"
|
||||
},
|
||||
"item": "produktua",
|
||||
"items": "produktuak",
|
||||
"unknown": "Ezezaguna",
|
||||
"status": {
|
||||
"active": "Aktibo",
|
||||
"inactive": "Ez aktibo",
|
||||
|
||||
@@ -32,6 +32,32 @@
|
||||
"quick_actions": "Ekintza Azkarrak",
|
||||
"key_metrics": "Metrika Nagusiak"
|
||||
},
|
||||
"insights": {
|
||||
"savings": {
|
||||
"label": "💰 AURREZKIAK",
|
||||
"this_week": "aste honetan",
|
||||
"vs_last": "vs. aurrekoa"
|
||||
},
|
||||
"inventory": {
|
||||
"label": "📦 INBENTARIOA",
|
||||
"all_stocked": "Guztia stock-ean",
|
||||
"low_stock": "Stock baxua",
|
||||
"stock_issues": "⚠️ Stock arazoak",
|
||||
"no_alerts": "Ez dago alertarik",
|
||||
"out_of_stock": "{count} stock-ik gabe",
|
||||
"alerts": "{count} alerta"
|
||||
},
|
||||
"waste": {
|
||||
"label": "♻️ HONDAKINAK",
|
||||
"this_month": "hilabete honetan",
|
||||
"vs_goal": "vs. helburua"
|
||||
},
|
||||
"deliveries": {
|
||||
"label": "🚚 BIDALKETA",
|
||||
"arriving_today": "{count} gaur iristen",
|
||||
"none_scheduled": "Ez dago programaturik"
|
||||
}
|
||||
},
|
||||
"procurement": {
|
||||
"title": "Zer erosi behar da biarko?",
|
||||
"empty": "Hornikuntza guztiak prest biarko",
|
||||
@@ -70,8 +96,8 @@
|
||||
"view_all": "Alerta guztiak ikusi",
|
||||
"time": {
|
||||
"now": "Orain",
|
||||
"minutes_ago": "duela {{count}} min",
|
||||
"hours_ago": "duela {{count}} h",
|
||||
"minutes_ago": "duela {count} min",
|
||||
"hours_ago": "duela {count} h",
|
||||
"yesterday": "Atzo"
|
||||
},
|
||||
"types": {
|
||||
@@ -99,7 +125,7 @@
|
||||
"additional_details": "Xehetasun Gehigarriak",
|
||||
"mark_as_read": "Irakurritako gisa markatu",
|
||||
"remove": "Kendu",
|
||||
"active_count": "{{count}} alerta aktibo"
|
||||
"active_count": "{count} alerta aktibo"
|
||||
},
|
||||
"messages": {
|
||||
"welcome": "Ongi etorri berriro",
|
||||
@@ -129,16 +155,16 @@
|
||||
},
|
||||
"health": {
|
||||
"production_on_schedule": "Ekoizpena orduan",
|
||||
"production_delayed": "{{count}} ekoizpen sorta atzeratuta",
|
||||
"production_delayed": "{count} ekoizpen sorta atzeratuta",
|
||||
"all_ingredients_in_stock": "Osagai guztiak stockean",
|
||||
"ingredients_out_of_stock": "{{count}} osagai stockik gabe",
|
||||
"ingredients_out_of_stock": "{count} osagai stockik gabe",
|
||||
"no_pending_approvals": "Ez dago onarpen pendienteik",
|
||||
"approvals_awaiting": "{{count}} erosketa agindu{{count, plural, one {} other {k}}} onarpenaren zai",
|
||||
"approvals_awaiting": "{count} erosketa agindu{count, plural, one {} other {k}}} onarpenaren zai",
|
||||
"all_systems_operational": "Sistema guztiak martxan",
|
||||
"critical_issues": "{{count}} arazo kritiko",
|
||||
"critical_issues": "{count} arazo kritiko",
|
||||
"headline_green": "Zure okindegia arazorik gabe dabil",
|
||||
"headline_yellow_approvals": "Mesedez berrikusi {{count}} onarpen zain",
|
||||
"headline_yellow_alerts": "{{count}} alerta{{count, plural, one {} other {k}}} arreta behar d{{count, plural, one {u} other {ute}}}",
|
||||
"headline_yellow_approvals": "Mesedez berrikusi {count} onarpen zain",
|
||||
"headline_yellow_alerts": "{count} alerta{count, plural, one {} other {k}}} arreta behar d{count, plural, one {u} other {ute}}}",
|
||||
"headline_yellow_general": "Zenbait elementuk zure arreta behar dute",
|
||||
"headline_red": "Arazo kritikoek berehalako ekintza behar dute"
|
||||
},
|
||||
@@ -158,7 +184,7 @@
|
||||
"suppliers": "Hornitzaileak",
|
||||
"recipes": "Errezetak",
|
||||
"quality": "Kalitate Estandarrak",
|
||||
"add_ingredients": "Gehitu gutxienez {{count}} osagai",
|
||||
"add_ingredients": "Gehitu gutxienez {count} osagai",
|
||||
"add_supplier": "Gehitu zure lehen hornitzailea",
|
||||
"add_recipe": "Sortu zure lehen errezeta",
|
||||
"add_quality": "Gehitu kalitate kontrolak (aukerakoa)",
|
||||
|
||||
36
frontend/src/locales/eu/purchase_orders.json
Normal file
36
frontend/src/locales/eu/purchase_orders.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"purchase_order": "Erosketa Agindua",
|
||||
"purchase_orders": "Erosketa Aginduak",
|
||||
"created": "Sortua",
|
||||
"supplier": "Hornitzailea",
|
||||
"order_date": "Eskabidearen Data",
|
||||
"expected_delivery": "Espero den Entrega",
|
||||
"items": "Produktuak",
|
||||
"no_items": "Ez dago produkturik eskaera honetan",
|
||||
"notes": "Oharrak",
|
||||
"not_found": "Erosketa agindua ez da aurkitu",
|
||||
"total_amount": "Guztira",
|
||||
"status": {
|
||||
"draft": "Zirriborroa",
|
||||
"pending_approval": "Onarpenaren Zain",
|
||||
"approved": "Onartuta",
|
||||
"sent": "Bidalita",
|
||||
"partially_received": "Partzialki Jasota",
|
||||
"received": "Jasota",
|
||||
"cancelled": "Bertan Behera Utzita"
|
||||
},
|
||||
"details": {
|
||||
"title": "Erosketa Aginduaren Xehetasunak",
|
||||
"quick_view": "Eskaeraren Ikuspegi Azkarra",
|
||||
"summary": "Laburpena",
|
||||
"supplier_info": "Hornitzailearen Informazioa",
|
||||
"delivery_info": "Entregaren Informazioa",
|
||||
"order_items": "Eskaeraren Produktuak",
|
||||
"additional_notes": "Ohar Gehigarriak"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Agindua Onartu",
|
||||
"modify": "Agindua Aldatu",
|
||||
"close": "Itxi"
|
||||
}
|
||||
}
|
||||
@@ -85,11 +85,23 @@
|
||||
"estimated_time": "Estimatutako denbora",
|
||||
"all_caught_up": "Dena egunean!",
|
||||
"no_actions": "Ez dago une honetan zure arreta behar duen ekintzarik.",
|
||||
"show_more": "Erakutsi {{count}} Ekintza gehiago",
|
||||
"show_more": "Erakutsi {count} Ekintza gehiago",
|
||||
"show_less": "Erakutsi Gutxiago",
|
||||
"total": "guztira",
|
||||
"critical": "kritiko",
|
||||
"important": "garrantzitsu"
|
||||
"important": "garrantzitsu",
|
||||
"consequences": {
|
||||
"delayed_delivery_impact": "Entregatze atzerapena ekoizpen programan eragina izan dezake",
|
||||
"immediate_action_required": "Berehalako ekintza behar da ekoizpen arazoak saihesteko",
|
||||
"some_features_limited": "Funtzio batzuk mugatuta daude"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Onartu",
|
||||
"view_details": "Xehetasunak Ikusi",
|
||||
"modify": "Aldatu",
|
||||
"dismiss": "Baztertu",
|
||||
"complete_setup": "Osatu Konfigurazioa"
|
||||
}
|
||||
},
|
||||
"orchestration_summary": {
|
||||
"title": "Bart Gauean Zure Eguna Planifikatu Nuen",
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"title": "Inbentarioa Gehitu",
|
||||
"inventoryDetails": "Inbentario Elementuaren Xehetasunak",
|
||||
"fillRequiredInfo": "Bete beharrezko informazioa inbentario elementu bat sortzeko",
|
||||
"summary": "Laburpena",
|
||||
"steps": {
|
||||
"productType": "Produktu Mota",
|
||||
"basicInfo": "Oinarrizko Informazioa",
|
||||
"stockConfig": "Stock Konfigurazioa"
|
||||
},
|
||||
"typeDescriptions": {
|
||||
"ingredient": "Errezetetan erabiltzen diren lehengaiak eta osagaiak",
|
||||
"finished_product": "Salmentarako edo kontsumorako prest dauden produktu finalak"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"namePlaceholder": "Adib: Erabilera Anitzeko Irina, Masa Zaharreko Ogia",
|
||||
@@ -81,6 +91,11 @@
|
||||
"basicInformation": "Oinarrizko Informazioa",
|
||||
"advancedOptions": "Aukera Aurreratuak",
|
||||
"advancedOptionsDescription": "Inbentario kudeaketa osoa egiteko eremu aukerazkoak",
|
||||
"additionalInformationDescription": "Produktu identifikatzaile aukerazkoak",
|
||||
"additionalDetails": "Xehetasun Gehigarriak",
|
||||
"additionalDetailsDescription": "Produktuaren xehetasun aukerazkoak",
|
||||
"advancedStockSettings": "Stock Ezarpen Aurreratuak",
|
||||
"advancedStockSettingsDescription": "Konfiguratu inbentario atalaseak eta berriro eskatzeko puntuak",
|
||||
"pricingInformation": "Prezioen Informazioa",
|
||||
"inventoryManagement": "Inbentario Kudeaketa",
|
||||
"productInformation": "Produktuaren Informazioa",
|
||||
|
||||
@@ -16,6 +16,7 @@ import ajustesEs from './es/ajustes.json';
|
||||
import reasoningEs from './es/reasoning.json';
|
||||
import wizardsEs from './es/wizards.json';
|
||||
import subscriptionEs from './es/subscription.json';
|
||||
import purchaseOrdersEs from './es/purchase_orders.json';
|
||||
|
||||
// English translations
|
||||
import commonEn from './en/common.json';
|
||||
@@ -35,6 +36,7 @@ import ajustesEn from './en/ajustes.json';
|
||||
import reasoningEn from './en/reasoning.json';
|
||||
import wizardsEn from './en/wizards.json';
|
||||
import subscriptionEn from './en/subscription.json';
|
||||
import purchaseOrdersEn from './en/purchase_orders.json';
|
||||
|
||||
// Basque translations
|
||||
import commonEu from './eu/common.json';
|
||||
@@ -54,6 +56,7 @@ import ajustesEu from './eu/ajustes.json';
|
||||
import reasoningEu from './eu/reasoning.json';
|
||||
import wizardsEu from './eu/wizards.json';
|
||||
import subscriptionEu from './eu/subscription.json';
|
||||
import purchaseOrdersEu from './eu/purchase_orders.json';
|
||||
|
||||
// Translation resources by language
|
||||
export const resources = {
|
||||
@@ -75,6 +78,7 @@ export const resources = {
|
||||
reasoning: reasoningEs,
|
||||
wizards: wizardsEs,
|
||||
subscription: subscriptionEs,
|
||||
purchase_orders: purchaseOrdersEs,
|
||||
},
|
||||
en: {
|
||||
common: commonEn,
|
||||
@@ -94,6 +98,7 @@ export const resources = {
|
||||
reasoning: reasoningEn,
|
||||
wizards: wizardsEn,
|
||||
subscription: subscriptionEn,
|
||||
purchase_orders: purchaseOrdersEn,
|
||||
},
|
||||
eu: {
|
||||
common: commonEu,
|
||||
@@ -113,6 +118,7 @@ export const resources = {
|
||||
reasoning: reasoningEu,
|
||||
wizards: wizardsEu,
|
||||
subscription: subscriptionEu,
|
||||
purchase_orders: purchaseOrdersEu,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -149,7 +155,7 @@ export const languageConfig = {
|
||||
};
|
||||
|
||||
// Namespaces available in translations
|
||||
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors', 'dashboard', 'production', 'equipment', 'landing', 'settings', 'ajustes', 'reasoning', 'wizards', 'subscription'] as const;
|
||||
export const namespaces = ['common', 'auth', 'inventory', 'foodSafety', 'suppliers', 'orders', 'recipes', 'errors', 'dashboard', 'production', 'equipment', 'landing', 'settings', 'ajustes', 'reasoning', 'wizards', 'subscription', 'purchase_orders'] as const;
|
||||
export type Namespace = typeof namespaces[number];
|
||||
|
||||
// Helper function to get language display name
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ActionQueueCard } from '../../components/dashboard/ActionQueueCard';
|
||||
import { OrchestrationSummaryCard } from '../../components/dashboard/OrchestrationSummaryCard';
|
||||
import { ProductionTimelineCard } from '../../components/dashboard/ProductionTimelineCard';
|
||||
import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
|
||||
import { PurchaseOrderDetailsModal } from '../../components/dashboard/PurchaseOrderDetailsModal';
|
||||
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
|
||||
import type { ItemType } from '../../components/domain/unified-wizard';
|
||||
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
|
||||
@@ -52,6 +53,10 @@ export function NewDashboardPage() {
|
||||
const [isAddWizardOpen, setIsAddWizardOpen] = useState(false);
|
||||
const [addWizardError, setAddWizardError] = useState<string | null>(null);
|
||||
|
||||
// PO Details Modal state
|
||||
const [selectedPOId, setSelectedPOId] = useState<string | null>(null);
|
||||
const [isPOModalOpen, setIsPOModalOpen] = useState(false);
|
||||
|
||||
// Data fetching
|
||||
const {
|
||||
data: healthStatus,
|
||||
@@ -113,11 +118,13 @@ export function NewDashboardPage() {
|
||||
};
|
||||
|
||||
const handleViewDetails = (actionId: string) => {
|
||||
// Navigate to appropriate detail page based on action type
|
||||
navigate(`/app/operations/procurement`);
|
||||
// Open modal to show PO details
|
||||
setSelectedPOId(actionId);
|
||||
setIsPOModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModify = (actionId: string) => {
|
||||
// Navigate to procurement page for modification
|
||||
navigate(`/app/operations/procurement`);
|
||||
};
|
||||
|
||||
@@ -327,6 +334,21 @@ export function NewDashboardPage() {
|
||||
onClose={() => setIsAddWizardOpen(false)}
|
||||
onComplete={handleAddWizardComplete}
|
||||
/>
|
||||
|
||||
{/* Purchase Order Details Modal */}
|
||||
{selectedPOId && (
|
||||
<PurchaseOrderDetailsModal
|
||||
poId={selectedPOId}
|
||||
tenantId={tenantId}
|
||||
isOpen={isPOModalOpen}
|
||||
onClose={() => {
|
||||
setIsPOModalOpen(false);
|
||||
setSelectedPOId(null);
|
||||
}}
|
||||
onApprove={handleApprove}
|
||||
onModify={handleModify}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,6 +160,13 @@ export const useAuthStore = create<AuthState>()(
|
||||
console.warn('Failed to clear tenant store on logout:', err);
|
||||
});
|
||||
|
||||
// Clear notification storage to ensure notifications don't persist across sessions
|
||||
import('../hooks/useNotifications').then(({ clearNotificationStorage }) => {
|
||||
clearNotificationStorage();
|
||||
}).catch(err => {
|
||||
console.warn('Failed to clear notification storage on logout:', err);
|
||||
});
|
||||
|
||||
set({
|
||||
user: null,
|
||||
token: null,
|
||||
|
||||
Reference in New Issue
Block a user