Add whatsapp feature

This commit is contained in:
Urtzi Alfaro
2025-11-13 16:01:08 +01:00
parent d7df2b0853
commit 9bc048d360
74 changed files with 9765 additions and 533 deletions

View File

@@ -88,6 +88,7 @@ class ApiClient {
'/auth/register', // Registration
'/auth/login', // Login
'/geocoding', // Geocoding/address search - utility service, no tenant context
'/tenants/register', // Tenant registration - creating new tenant, no existing tenant context
];
const isPublicEndpoint = publicEndpoints.some(endpoint =>

View File

@@ -21,13 +21,20 @@ import { apiClient } from '../client';
export interface HealthChecklistItem {
icon: 'check' | 'warning' | 'alert';
text: string;
text?: string; // Deprecated: Use textKey instead
textKey?: string; // i18n key for translation
textParams?: Record<string, any>; // Parameters for i18n translation
actionRequired: boolean;
}
export interface HeadlineData {
key: string;
params: Record<string, any>;
}
export interface BakeryHealthStatus {
status: 'green' | 'yellow' | 'red';
headline: string;
headline: string | HeadlineData; // Can be string (deprecated) or i18n object
lastOrchestrationRun: string | null;
nextScheduledRun: string;
checklistItems: HealthChecklistItem[];

View File

@@ -6,11 +6,11 @@ import { apiClient } from '../client';
import { UserProgress, UpdateStepRequest } from '../types/onboarding';
// Backend onboarding steps (full list from backend - UPDATED to match refactored flow)
// NOTE: poi-detection removed - now happens automatically in background during tenant registration
export const BACKEND_ONBOARDING_STEPS = [
'user_registered', // Phase 0: User account created (auto-completed)
'bakery-type-selection', // Phase 1: Choose bakery type
'setup', // Phase 2: Basic bakery setup and tenant creation
'poi-detection', // Phase 2a: POI Detection (Location Context)
'upload-sales-data', // Phase 2b: File upload, validation, AI classification
'inventory-review', // Phase 2b: Review AI-detected products with type selection
'initial-stock-entry', // Phase 2b: Capture initial stock levels
@@ -26,10 +26,10 @@ export const BACKEND_ONBOARDING_STEPS = [
];
// Frontend step order for navigation (excludes user_registered as it's auto-completed)
// NOTE: poi-detection removed - now happens automatically in background during tenant registration
export const FRONTEND_STEP_ORDER = [
'bakery-type-selection', // Phase 1: Choose bakery type
'setup', // Phase 2: Basic bakery setup and tenant creation
'poi-detection', // Phase 2a: POI Detection (Location Context)
'upload-sales-data', // Phase 2b: File upload and AI classification
'inventory-review', // Phase 2b: Review AI-detected products
'initial-stock-entry', // Phase 2b: Initial stock levels

View File

@@ -146,6 +146,34 @@ export interface MLInsightsSettings {
ml_confidence_threshold: number;
}
export interface NotificationSettings {
// WhatsApp Configuration
whatsapp_enabled: boolean;
whatsapp_phone_number_id: string;
whatsapp_access_token: string;
whatsapp_business_account_id: string;
whatsapp_api_version: string;
whatsapp_default_language: string;
// Email Configuration
email_enabled: boolean;
email_from_address: string;
email_from_name: string;
email_reply_to: string;
// Notification Preferences
enable_po_notifications: boolean;
enable_inventory_alerts: boolean;
enable_production_alerts: boolean;
enable_forecast_alerts: boolean;
// Notification Channels
po_notification_channels: string[];
inventory_alert_channels: string[];
production_alert_channels: string[];
forecast_alert_channels: string[];
}
export interface TenantSettings {
id: string;
tenant_id: string;
@@ -160,6 +188,7 @@ export interface TenantSettings {
moq_settings: MOQSettings;
supplier_selection_settings: SupplierSelectionSettings;
ml_insights_settings: MLInsightsSettings;
notification_settings: NotificationSettings;
created_at: string;
updated_at: string;
}
@@ -176,6 +205,7 @@ export interface TenantSettingsUpdate {
moq_settings?: Partial<MOQSettings>;
supplier_selection_settings?: Partial<SupplierSelectionSettings>;
ml_insights_settings?: Partial<MLInsightsSettings>;
notification_settings?: Partial<NotificationSettings>;
}
export type SettingsCategory =
@@ -189,7 +219,8 @@ export type SettingsCategory =
| 'safety_stock'
| 'moq'
| 'supplier_selection'
| 'ml_insights';
| 'ml_insights'
| 'notification';
export interface CategoryResetResponse {
category: string;

View File

@@ -80,7 +80,9 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
</div>
<div className="flex-1 min-w-0">
<h2 className="text-xl md:text-2xl font-bold mb-2" style={{ color: config.textColor }}>
{healthStatus.headline || t(`jtbd.health_status.${status}`)}
{typeof healthStatus.headline === 'object' && healthStatus.headline?.key
? t(healthStatus.headline.key.replace('.', ':'), healthStatus.headline.params || {})
: healthStatus.headline || t(`jtbd.health_status.${status}`)}
</h2>
{/* Last Update */}
@@ -117,6 +119,11 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
const iconColor = item.actionRequired ? 'var(--color-warning-600)' : 'var(--color-success-600)';
const bgColor = item.actionRequired ? 'var(--bg-primary)' : 'rgba(255, 255, 255, 0.5)';
// Translate using textKey if available, otherwise use text
const displayText = item.textKey
? t(item.textKey.replace('.', ':'), item.textParams || {})
: item.text || '';
return (
<div
key={index}
@@ -128,7 +135,7 @@ export function HealthStatusCard({ healthStatus, loading }: HealthStatusCardProp
className={`text-sm md:text-base ${item.actionRequired ? 'font-semibold' : ''}`}
style={{ color: 'var(--text-primary)' }}
>
{item.text || ''}
{displayText}
</span>
</div>
);

View File

@@ -20,19 +20,53 @@ import {
Brain,
ChevronDown,
ChevronUp,
Loader2,
} from 'lucide-react';
import { OrchestrationSummary } from '../../api/hooks/newDashboard';
import { runDailyWorkflow } from '../../api/services/orchestrator';
import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { useTenant } from '../../stores/tenant.store';
import toast from 'react-hot-toast';
interface OrchestrationSummaryCardProps {
summary: OrchestrationSummary;
loading?: boolean;
onWorkflowComplete?: () => void;
}
export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSummaryCardProps) {
export function OrchestrationSummaryCard({ summary, loading, onWorkflowComplete }: OrchestrationSummaryCardProps) {
const [expanded, setExpanded] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const { t } = useTranslation('reasoning');
const { currentTenant } = useTenant();
const handleRunPlanning = async () => {
if (!currentTenant?.id) {
toast.error(t('jtbd.orchestration_summary.no_tenant_error') || 'No tenant ID found');
return;
}
setIsRunning(true);
try {
const result = await runDailyWorkflow(currentTenant.id);
if (result.success) {
toast.success(t('jtbd.orchestration_summary.planning_started') || 'Planning started successfully');
// Call callback to refresh the orchestration summary
if (onWorkflowComplete) {
onWorkflowComplete();
}
} else {
toast.error(result.message || t('jtbd.orchestration_summary.planning_failed') || 'Failed to start planning');
}
} catch (error) {
console.error('Error running daily workflow:', error);
toast.error(t('jtbd.orchestration_summary.planning_error') || 'An error occurred while starting planning');
} finally {
setIsRunning(false);
}
};
if (loading || !summary) {
return (
@@ -57,24 +91,27 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div
className="border-2 rounded-xl p-6"
style={{
backgroundColor: 'var(--color-info-50)',
borderColor: 'var(--color-info-200)',
backgroundColor: 'var(--surface-secondary)',
borderColor: 'var(--color-info-300)',
}}
>
<div className="flex items-start gap-4">
<Bot className="w-10 h-10 flex-shrink-0" style={{ color: 'var(--color-info-600)' }} />
<Bot className="w-10 h-10 flex-shrink-0" style={{ color: 'var(--color-info)' }} />
<div>
<h3 className="text-lg font-bold mb-2" style={{ color: 'var(--color-info-900)' }}>
<h3 className="text-lg font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.orchestration_summary.ready_to_plan')}
</h3>
<p className="mb-4" style={{ color: 'var(--color-info-700)' }}>{summary.message || ''}</p>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}>{summary.message || ''}</p>
<button
className="px-4 py-2 rounded-lg font-semibold transition-colors duration-200"
onClick={handleRunPlanning}
disabled={isRunning}
className="px-4 py-2 rounded-lg font-semibold transition-colors duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-90"
style={{
backgroundColor: 'var(--color-info-600)',
color: 'var(--text-inverse)',
backgroundColor: 'var(--color-info)',
color: 'var(--bg-primary)',
}}
>
{isRunning && <Loader2 className="w-4 h-4 animate-spin" />}
{t('jtbd.orchestration_summary.run_planning')}
</button>
</div>
@@ -91,14 +128,14 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div
className="rounded-xl shadow-md p-6 border"
style={{
background: 'linear-gradient(to bottom right, var(--color-primary-50), var(--color-info-50))',
borderColor: 'var(--color-primary-100)',
background: 'linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%)',
borderColor: 'var(--border-primary)',
}}
>
{/* Header */}
<div className="flex items-start gap-4 mb-6">
<div className="p-3 rounded-full" style={{ backgroundColor: 'var(--color-primary-100)' }}>
<Bot className="w-8 h-8" style={{ color: 'var(--color-primary-600)' }} />
<div className="p-3 rounded-full" style={{ backgroundColor: 'var(--bg-tertiary)' }}>
<Bot className="w-8 h-8" style={{ color: 'var(--color-primary)' }} />
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
@@ -210,9 +247,9 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
)}
{/* Reasoning Inputs (How decisions were made) */}
<div className="rounded-lg p-4" style={{ backgroundColor: 'rgba(255, 255, 255, 0.6)' }}>
<div className="rounded-lg p-4" style={{ backgroundColor: 'var(--surface-secondary)', border: '1px solid var(--border-primary)' }}>
<div className="flex items-center gap-2 mb-3">
<Brain className="w-5 h-5" style={{ color: 'var(--color-primary-600)' }} />
<Brain className="w-5 h-5" style={{ color: 'var(--color-primary)' }} />
<h3 className="font-bold" style={{ color: 'var(--text-primary)' }}>{t('jtbd.orchestration_summary.based_on')}</h3>
</div>
@@ -260,13 +297,13 @@ export function OrchestrationSummaryCard({ summary, loading }: OrchestrationSumm
<div
className="mt-4 p-4 border rounded-lg"
style={{
backgroundColor: 'var(--color-warning-50)',
borderColor: 'var(--color-warning-200)',
backgroundColor: 'var(--bg-tertiary)',
borderColor: 'var(--color-warning)',
}}
>
<div className="flex items-center gap-2">
<FileText className="w-5 h-5" style={{ color: 'var(--color-warning-600)' }} />
<p className="text-sm font-medium" style={{ color: 'var(--color-warning-900)' }}>
<FileText className="w-5 h-5" style={{ color: 'var(--color-warning)' }} />
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
{t('jtbd.orchestration_summary.actions_required', {
count: summary.userActionsRequired,
})}

View File

@@ -1,9 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import { EditViewModal, StatusModalSection } from '@/components/ui/EditViewModal/EditViewModal';
import { Badge } from '@/components/ui/Badge';
import { Tooltip } from '@/components/ui/Tooltip';
import { TrainedModelResponse, TrainingMetrics } from '@/types/training';
import { Activity, TrendingUp, Calendar, Settings, Lightbulb, AlertTriangle, CheckCircle, Target } from 'lucide-react';
import { Activity, TrendingUp, Calendar, Settings, Lightbulb, AlertTriangle, CheckCircle, Target, MapPin } from 'lucide-react';
import { POI_CATEGORY_METADATA } from '@/types/poi';
interface ModelDetailsModalProps {
isOpen: boolean;
@@ -88,9 +89,65 @@ const FeatureTag: React.FC<{ feature: string }> = ({ feature }) => {
);
};
const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
isOpen,
onClose,
// POI Category Card component
const POICategoryCard: React.FC<{
category: string;
featureCount: number;
metrics: Set<string>;
}> = ({ category, featureCount, metrics }) => {
const metadata = POI_CATEGORY_METADATA[category];
if (!metadata) return null;
const metricsList = Array.from(metrics);
const hasProximity = metricsList.some(m => m.includes('proximity'));
const hasDistance = metricsList.some(m => m.includes('distance'));
const hasCounts = metricsList.some(m => m.includes('count'));
const metricsDescription = [
hasProximity && 'proximity scores',
hasDistance && 'distances',
hasCounts && 'location counts'
].filter(Boolean).join(', ');
return (
<Tooltip content={`${metadata.description}. Uses: ${metricsDescription}`}>
<div
className="flex items-center gap-3 p-3 rounded-lg border transition-all hover:shadow-sm"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border-color)'
}}
>
<span style={{ fontSize: '28px' }} aria-hidden="true">
{metadata.icon}
</span>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm text-[var(--text-primary)]">
{metadata.displayName}
</div>
<div className="text-xs text-[var(--text-secondary)] truncate">
{featureCount} feature{featureCount !== 1 ? 's' : ''} {metricsList.length} metric{metricsList.length !== 1 ? 's' : ''}
</div>
</div>
<Badge
variant="secondary"
className="text-xs"
style={{
backgroundColor: `${metadata.color}20`,
color: metadata.color
}}
>
Active
</Badge>
</div>
</Tooltip>
);
};
const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
isOpen,
onClose,
model,
onRetrain,
onViewPredictions
@@ -116,6 +173,40 @@ const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
const performanceColor = getPerformanceColor(accuracy);
const performanceMessage = getPerformanceMessage(accuracy);
// Parse POI features from model features array
const poiFeatureAnalysis = useMemo(() => {
const features = ((model as any).features || []) as string[];
const poiFeatures = features.filter(f => f.startsWith('poi_'));
// Group by category
const byCategory: Record<string, string[]> = {};
const categoryMetrics: Record<string, Set<string>> = {};
poiFeatures.forEach(feature => {
const parts = feature.split('_');
if (parts.length >= 3 && parts[0] === 'poi') {
const category = parts[1]; // e.g., "schools"
const metric = parts.slice(2).join('_'); // e.g., "proximity_score" or "count_0_100m"
if (!byCategory[category]) {
byCategory[category] = [];
categoryMetrics[category] = new Set();
}
byCategory[category].push(feature);
categoryMetrics[category].add(metric);
}
});
return {
allPOIFeatures: poiFeatures,
byCategory,
categoryMetrics,
categoryCount: Object.keys(byCategory).length,
hasAnyPOI: poiFeatures.length > 0,
totalPOIFeatures: poiFeatures.length
};
}, [(model as any).features]);
// Prepare sections for StatusModal
const sections: StatusModalSection[] = [
{
@@ -299,6 +390,77 @@ const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
}
]
},
{
title: "Factores de Ubicación (POI)",
icon: MapPin,
fields: [
{
label: "Contexto de la Ubicación",
value: (() => {
if (!poiFeatureAnalysis.hasAnyPOI) {
return (
<div className="text-sm text-[var(--text-secondary)] italic bg-[var(--bg-secondary)] rounded-md p-4 border border-dashed border-[var(--border-color)]">
No se detectaron factores de ubicación (POI) en este modelo.
El modelo se basa en datos de ventas, temporales y climáticos.
</div>
);
}
const categories = Object.keys(poiFeatureAnalysis.byCategory).sort();
return (
<div className="space-y-4">
<div className="text-sm text-[var(--text-secondary)]">
El modelo utiliza <strong>{poiFeatureAnalysis.totalPOIFeatures} características de ubicación</strong> de{' '}
<strong>{poiFeatureAnalysis.categoryCount} categorías POI</strong> para mejorar las predicciones basándose en el entorno de tu panadería.
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{categories.map(category => (
<POICategoryCard
key={category}
category={category}
featureCount={poiFeatureAnalysis.byCategory[category].length}
metrics={poiFeatureAnalysis.categoryMetrics[category]}
/>
))}
</div>
<div className="text-xs text-[var(--text-secondary)] bg-[var(--bg-secondary)] rounded-md p-3 border-l-4 border-[var(--color-success)]">
<strong>📍 Factores POI:</strong> Estos factores de ubicación ayudan al modelo a entender cómo el entorno de tu panadería (escuelas, oficinas, transporte, etc.) afecta tus ventas. Cada categoría contribuye con múltiples métricas como proximidad, distancia y conteos de ubicaciones.
</div>
{/* Advanced debugging info - expandable */}
<details className="text-xs">
<summary className="cursor-pointer font-medium text-[var(--text-primary)] hover:text-[var(--color-primary)] transition-colors py-2">
🔍 Ver detalles técnicos de POI
</summary>
<div className="mt-2 space-y-2 pl-4 border-l-2 border-[var(--border-color)]">
{categories.map(category => {
const features = poiFeatureAnalysis.byCategory[category];
const metadata = POI_CATEGORY_METADATA[category];
return (
<div key={category} className="space-y-1">
<div className="font-medium text-[var(--text-primary)]">
{metadata?.icon} {metadata?.displayName || category}
</div>
<div className="text-[var(--text-secondary)] space-y-0.5 ml-2">
{features.map(feature => (
<div key={feature} className="font-mono text-xs"> {feature}</div>
))}
</div>
</div>
);
})}
</div>
</details>
</div>
);
})(),
span: 2
}
]
},
{
title: "Detalles Técnicos",
icon: Calendar,

View File

@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from '../../../ui/Button';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { ChartBar, ShoppingCart, Users, TrendingUp, Zap, CheckCircle2 } from 'lucide-react';
import { BarChart, ShoppingCart, Users, TrendingUp, Zap, CheckCircle2 } from 'lucide-react';
interface CompletionStepProps {
onNext: () => void;
@@ -148,7 +148,7 @@ export const CompletionStep: React.FC<CompletionStepProps> = ({
onClick={() => navigate('/app/dashboard')}
className="p-4 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] rounded-lg transition-all hover:shadow-lg hover:scale-105 text-left group"
>
<ChartBar className="w-8 h-8 text-[var(--color-primary)] mb-2 group-hover:scale-110 transition-transform" />
<BarChart className="w-8 h-8 text-[var(--color-primary)] mb-2 group-hover:scale-110 transition-transform" />
<h4 className="font-semibold text-[var(--text-primary)] mb-1">
{t('onboarding:completion.quick.analytics', 'Analíticas')}
</h4>

View File

@@ -97,8 +97,8 @@ export const InitialStockEntryStep: React.FC<InitialStockEntryStepProps> = ({
tenantId,
stockData: {
ingredient_id: product.id,
unit_price: 0, // Default price, can be updated later
notes: `Initial stock entry from onboarding`
current_quantity: product.initialStock!, // The actual stock quantity
unit_cost: 0, // Default cost, can be updated later
}
})
);

View File

@@ -308,8 +308,9 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
});
});
await Promise.all(createPromises);
const createdIngredients = await Promise.all(createPromises);
console.log('✅ Inventory items created successfully');
console.log('📋 Created ingredient IDs:', createdIngredients.map(ing => ({ name: ing.name, id: ing.id })));
// STEP 2: Import sales data (only if file was uploaded)
// Now that inventory exists, sales records can reference the inventory IDs
@@ -331,10 +332,21 @@ export const InventoryReviewStep: React.FC<InventoryReviewStepProps> = ({
}
// Complete the step with metadata and inventory items
// Map created ingredients to include their real UUIDs
const itemsWithRealIds = createdIngredients.map(ingredient => ({
id: ingredient.id, // Real UUID from the API
name: ingredient.name,
product_type: ingredient.product_type,
category: ingredient.category,
unit_of_measure: ingredient.unit_of_measure,
}));
console.log('📦 Passing items with real IDs to next step:', itemsWithRealIds);
onComplete({
inventoryItemsCreated: inventoryItems.length,
inventoryItemsCreated: createdIngredients.length,
salesDataImported: salesImported,
inventoryItems: inventoryItems, // Pass the created items to the next step
inventoryItems: itemsWithRealIds, // Pass the created items with real UUIDs to the next step
});
} catch (error) {
console.error('Error creating inventory items:', error);

View File

@@ -59,6 +59,8 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
// Get wizard steps based on selected item type
// CRITICAL: Memoize the steps to prevent component recreation on every render
// Without this, every keystroke causes the component to unmount/remount, losing focus
// IMPORTANT: For dynamic wizards (like sales-entry), we need to include the entryMethod
// in the dependency array so steps update when the user selects manual vs upload
const wizardSteps = useMemo((): WizardStep[] => {
if (!selectedItemType) {
// Step 0: Item Type Selection
@@ -67,7 +69,7 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
id: 'item-type-selection',
title: 'Seleccionar tipo',
description: 'Elige qué deseas agregar',
component: (props) => (
component: () => (
<ItemTypeSelector onSelect={handleItemTypeSelect} />
),
},
@@ -97,7 +99,7 @@ export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
default:
return [];
}
}, [selectedItemType, handleItemTypeSelect]); // Only recreate when item type changes, NOT when wizardData changes
}, [selectedItemType, handleItemTypeSelect, wizardData.entryMethod]); // Include only critical fields for dynamic step generation
// Get wizard title based on selected item type
const getWizardTitle = (): string => {

View File

@@ -109,7 +109,7 @@ export const WizardModal: React.FC<WizardModalProps> = ({
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 z-50 animate-fadeIn"
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 animate-fadeIn"
onClick={handleClose}
/>

View File

@@ -31,7 +31,8 @@
"today_production": "Today's Production",
"pending_po_approvals": "Pending Purchase Orders",
"recent_activity": "Recent Activity",
"quick_actions": "Quick Actions"
"quick_actions": "Quick Actions",
"key_metrics": "Key Metrics"
},
"procurement": {
"title": "What needs to be bought for tomorrow?",
@@ -57,7 +58,11 @@
"start_production": "Start Production",
"check_inventory": "Check Inventory",
"view_reports": "View Reports",
"manage_staff": "Manage Staff"
"manage_staff": "Manage Staff",
"view_orders": "View Orders",
"view_production": "Production",
"view_inventory": "Inventory",
"view_suppliers": "Suppliers"
},
"alerts": {
"title": "Alerts",
@@ -121,7 +126,23 @@
"all_caught_up": "All caught up!",
"stock_healthy": "Stock healthy",
"same_as_yesterday": "Same as yesterday",
"less_than_yesterday": "less than yesterday"
"less_than_yesterday": "less than yesterday",
"your_bakery_at_glance": "Your bakery at a glance"
},
"health": {
"production_on_schedule": "Production on schedule",
"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",
"no_pending_approvals": "No pending approvals",
"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}}}",
"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_general": "Some items need your attention",
"headline_red": "Critical issues require immediate action"
},
"time_periods": {
"today": "Today",

View File

@@ -107,7 +107,11 @@
"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",
"planning_error": "An error occurred while starting planning"
},
"production_timeline": {
"title": "Your Production Plan Today",

View File

@@ -133,6 +133,33 @@
"delivery_tracking": "Permite a los clientes rastrear sus pedidos en tiempo real"
}
},
"notification": {
"title": "Notificaciones",
"whatsapp_config": "Configuración de WhatsApp",
"whatsapp_enabled": "Habilitar notificaciones por WhatsApp",
"whatsapp_phone_number_id": "ID del Número de Teléfono",
"whatsapp_phone_number_id_help": "ID del número de teléfono de WhatsApp Business desde Meta",
"whatsapp_access_token": "Token de Acceso",
"whatsapp_access_token_help": "Token de acceso permanente desde Meta Business Suite",
"whatsapp_business_account_id": "ID de Cuenta de Negocio",
"whatsapp_business_account_id_help": "ID de la cuenta de negocio de WhatsApp",
"whatsapp_api_version": "Versión de API",
"whatsapp_default_language": "Idioma Predeterminado",
"whatsapp_setup_note": "Pasos para configurar WhatsApp Business:",
"whatsapp_setup_step1": "Crea una cuenta de WhatsApp Business en Meta Business Suite",
"whatsapp_setup_step2": "Crea y aprueba plantillas de mensajes (ej: po_notification)",
"whatsapp_setup_step3": "Obtén las credenciales: Phone Number ID, Access Token y Business Account ID",
"email_config": "Configuración de Email",
"email_enabled": "Habilitar notificaciones por email",
"email_from_address": "Email Remitente",
"email_from_name": "Nombre del Remitente",
"email_reply_to": "Email de Respuesta",
"preferences": "Preferencias de Notificación",
"enable_po_notifications": "Notificaciones de Órdenes de Compra",
"enable_inventory_alerts": "Alertas de Inventario",
"enable_production_alerts": "Alertas de Producción",
"enable_forecast_alerts": "Alertas de Previsión"
},
"messages": {
"save_success": "Ajustes guardados correctamente",
"save_error": "Error al guardar ajustes",

View File

@@ -1,6 +1,6 @@
{
"title": "Panel de Control",
"subtitle": "Resumen general de tu panadería",
"subtitle": "Tu panadería de un vistazo",
"stats": {
"sales_today": "Ventas Hoy",
"pending_orders": "Órdenes Pendientes",
@@ -31,7 +31,8 @@
"today_production": "Producción de Hoy",
"pending_po_approvals": "Órdenes de Compra Pendientes",
"recent_activity": "Actividad Reciente",
"quick_actions": "Acciones Rápidas"
"quick_actions": "Acciones Rápidas",
"key_metrics": "Métricas Clave"
},
"procurement": {
"title": "¿Qué necesito comprar para mañana?",
@@ -57,7 +58,11 @@
"start_production": "Iniciar Producción",
"check_inventory": "Revisar Inventario",
"view_reports": "Ver Reportes",
"manage_staff": "Gestionar Personal"
"manage_staff": "Gestionar Personal",
"view_orders": "Ver Órdenes",
"view_production": "Producción",
"view_inventory": "Inventario",
"view_suppliers": "Proveedores"
},
"alerts": {
"title": "Alertas",
@@ -156,7 +161,23 @@
"all_caught_up": "¡Todo al día!",
"stock_healthy": "Stock saludable",
"same_as_yesterday": "Igual que ayer",
"less_than_yesterday": "menos que ayer"
"less_than_yesterday": "menos que ayer",
"your_bakery_at_glance": "Tu panadería de un vistazo"
},
"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}}}",
"all_ingredients_in_stock": "Todos los ingredientes en 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",
"all_systems_operational": "Todos los sistemas operativos",
"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_general": "Algunos elementos necesitan tu atención",
"headline_red": "Problemas críticos requieren acción inmediata"
},
"time_periods": {
"today": "Hoy",

View File

@@ -107,7 +107,11 @@
"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",
"planning_error": "Ocurrió un error al iniciar la planificación"
},
"production_timeline": {
"title": "Tu Plan de Producción de Hoy",

View File

@@ -5,7 +5,8 @@
"tabs": {
"information": "Datos del establecimiento",
"hours": "Horarios",
"operations": "Ajustes operacionales"
"operations": "Ajustes operacionales",
"notifications": "Notificaciones"
},
"information": {
"title": "Información General",

View File

@@ -133,6 +133,33 @@
"delivery_tracking": "Bezeroei beren eskaerak denbora errealean jarraitzeko aukera ematen die"
}
},
"notification": {
"title": "Jakinarazpenak",
"whatsapp_config": "WhatsApp Konfigurazioa",
"whatsapp_enabled": "Gaitu WhatsApp jakinarazpenak",
"whatsapp_phone_number_id": "Telefono Zenbakiaren ID-a",
"whatsapp_phone_number_id_help": "WhatsApp Business telefono zenbakiaren ID-a Meta-tik",
"whatsapp_access_token": "Sarbide Tokena",
"whatsapp_access_token_help": "Token iraunkor Meta Business Suite-tik",
"whatsapp_business_account_id": "Negozio Kontuaren ID-a",
"whatsapp_business_account_id_help": "WhatsApp negozio kontuaren ID-a",
"whatsapp_api_version": "API Bertsioa",
"whatsapp_default_language": "Hizkuntza Lehenetsia",
"whatsapp_setup_note": "WhatsApp Business konfiguratzeko urratsak:",
"whatsapp_setup_step1": "Sortu WhatsApp Business kontua Meta Business Suite-n",
"whatsapp_setup_step2": "Sortu eta onartu mezu txantiloiak (adib: po_notification)",
"whatsapp_setup_step3": "Lortu kredentzialak: Phone Number ID, Access Token eta Business Account ID",
"email_config": "Email Konfigurazioa",
"email_enabled": "Gaitu email jakinarazpenak",
"email_from_address": "Bidaltzailearen Emaila",
"email_from_name": "Bidaltzailearen Izena",
"email_reply_to": "Erantzuteko Emaila",
"preferences": "Jakinarazpen Hobespenak",
"enable_po_notifications": "Erosketa Agindu Jakinarazpenak",
"enable_inventory_alerts": "Inbentario Alertak",
"enable_production_alerts": "Ekoizpen Alertak",
"enable_forecast_alerts": "Aurreikuspen Alertak"
},
"messages": {
"save_success": "Ezarpenak ondo gorde dira",
"save_error": "Errorea ezarpenak gordetzean",

View File

@@ -1,6 +1,6 @@
{
"title": "Aginte Panela",
"subtitle": "Zure okindegiaren eragiketen ikuspegi orokorra",
"subtitle": "Zure okindegia begirada batean",
"stats": {
"sales_today": "Gaurko Salmentak",
"pending_orders": "Eskaera Zain",
@@ -29,7 +29,8 @@
"today_production": "Gaurko Ekoizpena",
"pending_po_approvals": "Erosketa Aginduak Zain",
"recent_activity": "Azken Jarduera",
"quick_actions": "Ekintza Azkarrak"
"quick_actions": "Ekintza Azkarrak",
"key_metrics": "Metrika Nagusiak"
},
"procurement": {
"title": "Zer erosi behar da biarko?",
@@ -55,7 +56,11 @@
"start_production": "Ekoizpena Hasi",
"check_inventory": "Inbentarioa Begiratu",
"view_reports": "Txostenak Ikusi",
"manage_staff": "Langilea Kudeatu"
"manage_staff": "Langilea Kudeatu",
"view_orders": "Aginduak Ikusi",
"view_production": "Ekoizpena",
"view_inventory": "Inbentarioa",
"view_suppliers": "Hornitzaileak"
},
"alerts": {
"title": "Alertak",
@@ -112,7 +117,30 @@
"action_required": "Ekintza beharrezkoa",
"manage_organizations": "Zure erakundeak kudeatu",
"setup_new_business": "Negozio berri bat hutsetik konfiguratu",
"active_organizations": "Erakunde Aktiboak"
"active_organizations": "Erakunde Aktiboak",
"excellent_progress": "Aurrerapen bikaina!",
"keep_improving": "Jarraitu hobetzen",
"from_sustainability": "Iraunkortasunetik",
"all_caught_up": "Dena eguneratuta!",
"stock_healthy": "Stock osasuntsua",
"same_as_yesterday": "Atzo bezala",
"less_than_yesterday": "atzo baino gutxiago",
"your_bakery_at_glance": "Zure okindegia begirada batean"
},
"health": {
"production_on_schedule": "Ekoizpena orduan",
"production_delayed": "{{count}} ekoizpen sorta atzeratuta",
"all_ingredients_in_stock": "Osagai guztiak stockean",
"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",
"all_systems_operational": "Sistema guztiak martxan",
"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_general": "Zenbait elementuk zure arreta behar dute",
"headline_red": "Arazo kritikoek berehalako ekintza behar dute"
},
"time_periods": {
"today": "Gaur",

View File

@@ -3,21 +3,65 @@
"title": "Hasierako Konfigurazioa",
"subtitle": "Pausoz pauso gidatuko zaitugu zure okindegia konfiguratzeko",
"steps": {
"bakery_type": {
"title": "Okindegi Mota",
"description": "Hautatu zure negozio mota"
},
"data_source": {
"title": "Konfigurazio Metodoa",
"description": "Aukeratu nola konfiguratu"
},
"setup": {
"title": "Okindegia Erregistratu",
"description": "Konfiguratu zure okindegiko oinarrizko informazioa"
"description": "Oinarrizko informazioa"
},
"poi_detection": {
"title": "Kokapen Analisia",
"description": "Inguruko interesguneak detektatu"
},
"smart_inventory": {
"title": "Salmenta Datuak Igo",
"description": "AArekin konfigurazioa"
},
"smart_inventory_setup": {
"title": "Inbentarioa Konfiguratu",
"description": "Salmenten datuak igo eta hasierako inbentarioa ezarri"
},
"suppliers": {
"title": "Hornitzaileak",
"description": "Konfiguratu zure hornitzaileak"
},
"inventory": {
"title": "Inbentarioa",
"description": "Produktuak eta osagaiak"
},
"recipes": {
"title": "Errezetak",
"description": "Ekoizpen errezetak"
},
"processes": {
"title": "Prozesuak",
"description": "Amaitzeko prozesuak"
},
"quality": {
"title": "Kalitatea",
"description": "Kalitate estandarrak"
},
"team": {
"title": "Taldea",
"description": "Taldeko kideak"
},
"review": {
"title": "Berrikuspena",
"description": "Berretsi zure konfigurazioa"
},
"ml_training": {
"title": "AA Prestakuntza",
"description": "Entrenatu zure adimen artifizial modelo pertsonalizatua"
"description": "Modelo pertsonalizatua"
},
"completion": {
"title": "Konfigurazioa Osatuta",
"description": "Ongi etorri zure kudeaketa sistema adimentsu honetara!"
"title": "Osatuta",
"description": "Dena prest!"
}
},
"navigation": {
@@ -174,6 +218,172 @@
}
}
},
"bakery_type": {
"title": "Zer motatako okindegia duzu?",
"subtitle": "Honek esperientzia pertsonalizatzen lagunduko digu eta behar dituzun funtzioak bakarrik erakusten",
"features_label": "Ezaugarriak",
"examples_label": "Adibideak",
"continue_button": "Jarraitu",
"help_text": "💡 Ez kezkatu, beti alda dezakezu hau geroago konfigurazioan",
"selected_info_title": "Zure okindegiarentzat ezin hobea",
"production": {
"name": "Ekoizpen Okindegia",
"description": "Oinarrizko osagaiak erabiliz hutsetik ekoizten dugu",
"feature1": "Errezeten kudeaketa osoa",
"feature2": "Osagaien eta kostuen kontrola",
"feature3": "Ekoizpen planifikazioa",
"feature4": "Lehengaien kalitate kontrola",
"example1": "Ogi artisanala",
"example2": "Gozogintza",
"example3": "Erreposteria",
"example4": "Pastelgintza",
"selected_info": "Errezeten, osagaien eta ekoizpenaren kudeaketa sistema oso bat konfiguratuko dugu zure lan-fluxura egokituta."
},
"retail": {
"name": "Salmenta Okindegia (Retail)",
"description": "Aurrez eginiko produktuak labe sartu eta saltzen ditugu",
"feature1": "Produktu amaituen kontrola",
"feature2": "Labe-sartzeko kudeaketa errazo",
"feature3": "Salmenta-puntuaren inbentario kontrola",
"feature4": "Salmenten eta galerak jarraipen",
"example1": "Aurrez labetuta ogia",
"example2": "Amaitzeko izoztutako produktuak",
"example3": "Salmentarako prest gozogintza",
"example4": "Hornitzaileen tartoak eta pastelak",
"selected_info": "Errezeten konplexutasunik gabe, inbentario kontrolan, labetzean eta salmentetan zentratutako sistema sinple bat konfiguratuko dugu."
},
"mixed": {
"name": "Okindegi Mistoa",
"description": "Geure ekoizpena produktu amaituak konbinatzen ditugu",
"feature1": "Errezeta propioak eta kanpo produktuak",
"feature2": "Kudeaketan malgutasun osoa",
"feature3": "Kostuen kontrol osoa",
"feature4": "Egokitasun maximoa",
"example1": "Geure ogia + hornitzaileko gozogintza",
"example2": "Geure pastelak + aurrez labetutakoak",
"example3": "Produktu artisanalak + industrialak",
"example4": "Sasoiaren araberako konbinazioa",
"selected_info": "Sistema malgu bat konfiguratuko dugu zure beharren arabera bai ekoizpen propioa bai kanpoko produktuak kudeatzeko aukera ematen dizuna."
}
},
"data_source": {
"title": "Nola nahiago duzu zure okindegia konfiguratu?",
"subtitle": "Aukeratu zure egungo egoerara hobekien egokitzen den metodoa",
"benefits_label": "Onurak",
"ideal_for_label": "Egokia hauentzat",
"estimated_time_label": "Gutxi gorabeherako denbora",
"continue_button": "Jarraitu",
"help_text": "💡 Metodoen artean edozein unetan alda dezakezu konfigurazio prozesuan",
"ai_assisted": {
"title": "AArekin Konfigurazio Adimentsua",
"description": "Igo zure salmenta historikoko datuak eta gure AAk automatikoki konfiguratuko dizu zure inbentarioa",
"benefit1": "⚡ Produktuen konfigurazio automatikoa",
"benefit2": "🎯 Kategorien araberako sailkapen adimentsua",
"benefit3": "💰 Kostu eta prezio historikoen analisia",
"benefit4": "📊 Salmenta ereduetan oinarritutako gomendioak",
"ideal1": "Salmenta historiala duten okindegiak",
"ideal2": "Beste sistema batetik migrazioa",
"ideal3": "Azkar konfiguratu behar duzu",
"time": "5-10 minutu",
"badge": "Gomendatua"
},
"ai_info_title": "Zer behar duzu AArekin konfiguratzeko?",
"ai_info1": "Salmenta fitxategia (CSV, Excel edo JSON)",
"ai_info2": "Gutxienez 1-3 hilabeteko datuak (gomendatua)",
"ai_info3": "Produktuen, prezioen eta kopuruen informazioa",
"manual": {
"title": "Pausoz Pausoko Eskuzko Konfigurazioa",
"description": "Konfiguratu zure okindegia hutsetik xehetasun bakoitza eskuz sartuz",
"benefit1": "🎯 Xehetasun guztien gaineko kontrol osoa",
"benefit2": "📝 Hutsetik hasteko ezin hobea",
"benefit3": "🧩 Pertsonalizazio osoa",
"benefit4": "✨ Datu historikorik behar gabe",
"ideal1": "Historialik gabeko okindegi berriak",
"ideal2": "Eskuzko kontrol osoa nahiago duzu",
"ideal3": "Oso konfigurazio espezifikoa",
"time": "15-20 minutu"
},
"manual_info_title": "Zer konfiguratuko dugu pausoz pauso?",
"manual_info1": "Hornitzaileak eta haien kontaktu datuak",
"manual_info2": "Osagaien eta produktuen inbentarioa",
"manual_info3": "Errezetak edo ekoizpen prozesuak",
"manual_info4": "Kalitate estandarrak eta taldea (aukerakoa)"
},
"processes": {
"title": "Ekoizpen Prozesuak",
"subtitle": "Definitu aurrez eginiko produktuak produktu amaitutan bihurtzeko erabiltzen dituzun prozesuak",
"your_processes": "Zure Prozesuak",
"add_new": "Prozesu Berria",
"add_button": "Prozesua Gehitu",
"hint": "💡 Gehitu gutxienez prozesu bat jarraitzeko",
"count": "{{count}} prozesu konfiguratuta",
"skip": "Oraingoz saltatu",
"continue": "Jarraitu",
"source": "Hemendik",
"finished": "Hona",
"templates": {
"title": "⚡ Hasi azkar txantiloiekin",
"subtitle": "Egin klik txantiloi batean gehitzeko",
"hide": "Ezkutatu"
},
"type": {
"baking": "Labetzea",
"decorating": "Apainketa",
"finishing": "Amaitze",
"assembly": "Muntatzea"
},
"form": {
"name": "Prozesuaren Izena",
"name_placeholder": "Adib: Ogiaren labetzea",
"source": "Jatorrizko Produktua",
"source_placeholder": "Adib: Aurrez egindako ogia",
"finished": "Produktu Amaitua",
"finished_placeholder": "Adib: Ogi freskoa",
"type": "Prozesu Mota",
"duration": "Iraupena (minutuak)",
"temperature": "Tenperatura (°C)",
"instructions": "Jarraibideak (aukerakoa)",
"instructions_placeholder": "Deskribatu prozesua...",
"cancel": "Ezeztatu",
"add": "Prozesua Gehitu"
}
},
"categorization": {
"title": "Sailkatu zure Produktuak",
"subtitle": "Lagundu gaitzazu ulertzera zeintzuk diren osagaiak (errezetetan erabiltzeko) eta zeintzuk produktu amaituak (saltzeko)",
"info_title": "Zergatik da garrantzitsua?",
"info_text": "Osagaiak errezetetan erabiltzen dira produktuak sortzeko. Produktu amaituak zuzenean saltzen dira. Sailkapen honek kostuak kalkulatzen eta ekoizpena behar bezala planifikatzen laguntzen du.",
"progress": "Sailkapen aurrerapena",
"accept_all_suggestions": "⚡ Onartu AAren gomendio guztiak",
"uncategorized": "Sailkatu gabe",
"ingredients_title": "Osagaiak",
"ingredients_help": "Errezetetan erabiltzeko",
"finished_products_title": "Produktu Amaituak",
"finished_products_help": "Zuzenean saltzeko",
"drag_here": "Arrastatu produktuak hona",
"ingredient": "Osagaia",
"finished_product": "Produktua",
"suggested_ingredient": "Iradokia: Osagaia",
"suggested_finished_product": "Iradokia: Produktua",
"incomplete_warning": "⚠️ Sailkatu produktu guztiak jarraitzeko"
},
"stock": {
"title": "Hasierako Stock Mailak",
"subtitle": "Sartu produktu bakoitzaren egungo kopuruak. Honek sistemak gaurdanik inbentarioa jarraitzea ahalbidetzen du.",
"info_title": "Zergatik da garrantzitsua?",
"info_text": "Hasierako stock-mailarik gabe, sistemak ezin dizu stock baxuari buruzko alertarik eman, ekoizpena planifikatu edo kostuak zuzen kalkulatu. Hartu une bat zure egungo kopuruak sartzeko.",
"progress": "Hartzeko aurrerapena",
"set_all_zero": "Ezarri dena 0an",
"skip_for_now": "Oraingoz saltatu (0an ezarriko da)",
"ingredients": "Osagaiak",
"finished_products": "Produktu Amaituak",
"incomplete_warning": "{{count}} produktu osatu gabe geratzen dira",
"incomplete_help": "Jarraitu dezakezu, baina kopuru guztiak sartzea gomendatzen dizugu inbentario-kontrol hobeagorako.",
"complete": "Konfigurazioa Osatu",
"continue_anyway": "Jarraitu hala ere",
"no_products_title": "Hasierako Stocka",
"no_products_message": "Stock-mailak geroago konfigura ditzakezu inbentario atalean."
},
"errors": {
"step_failed": "Errorea pauso honetan",
"data_invalid": "Datu baliogabeak",

View File

@@ -107,7 +107,11 @@
"historical_demand": "Eskaera historikoa",
"inventory_levels": "Inbentario mailak",
"ai_optimization": "IA optimizazioa",
"actions_required": "{{count}} elementuk zure onespena behar du aurrera jarraitu aurretik"
"actions_required": "{{count}} elementuk zure onespena behar du aurrera jarraitu aurretik",
"no_tenant_error": "Ez da inquilino ID aurkitu. Mesedez, ziurtatu saioa hasi duzula.",
"planning_started": "Plangintza behar bezala hasi da",
"planning_failed": "Errorea plangintza hastean",
"planning_error": "Errore bat gertatu da plangintza hastean"
},
"production_timeline": {
"title": "Zure Gaurko Ekoizpen Plana",

View File

@@ -1 +1,267 @@
{}
{
"title": "Errezeten Kudeaketa",
"subtitle": "Kudeatu zure okindegiaren errezetak",
"navigation": {
"all_recipes": "Errezeta Guztiak",
"active_recipes": "Errezeta Aktiboak",
"draft_recipes": "Zirriborroak",
"signature_recipes": "Errezeta Ezagunak",
"seasonal_recipes": "Denboraldiko Errezetak",
"production_batches": "Ekoizpen Loteak"
},
"actions": {
"create_recipe": "Errezeta Sortu",
"edit_recipe": "Errezeta Editatu",
"duplicate_recipe": "Errezeta Bikoiztu",
"activate_recipe": "Errezeta Aktibatu",
"archive_recipe": "Errezeta Artxibatu",
"delete_recipe": "Errezeta Ezabatu",
"view_recipe": "Errezeta Ikusi",
"check_feasibility": "Bideragarritasuna Egiaztatu",
"create_batch": "Lotea Sortu",
"start_production": "Ekoizpena Hasi",
"complete_batch": "Lotea Osatu",
"cancel_batch": "Lotea Ezeztatu",
"export_recipe": "Errezeta Esportatu",
"print_recipe": "Errezeta Inprimatu"
},
"fields": {
"name": "Errezeta Izena",
"recipe_code": "Errezeta Kodea",
"version": "Bertsioa",
"description": "Deskribapena",
"category": "Kategoria",
"cuisine_type": "Sukaldaritza Mota",
"difficulty_level": "Zailtasun Maila",
"yield_quantity": "Ekoizpen Kantitatea",
"yield_unit": "Ekoizpen Unitatea",
"prep_time": "Prestaketa Denbora",
"cook_time": "Sukaldaketa Denbora",
"total_time": "Denbora Guztira",
"rest_time": "Atseden Denbora",
"instructions": "Jarraibideak",
"preparation_notes": "Prestaketa Oharrak",
"storage_instructions": "Biltegiratzeko Jarraibideak",
"quality_standards": "Kalitate Estandarrak",
"serves_count": "Zati Kopurua",
"is_seasonal": "Denboraldikoa Da",
"season_start": "Denboraldi Hasiera",
"season_end": "Denboraldi Amaiera",
"is_signature": "Errezeta Ezaguna Da",
"target_margin": "Helburu Marjina",
"batch_multiplier": "Lote Biderkatzailea",
"min_batch_size": "Gutxieneko Lote Tamaina",
"max_batch_size": "Gehienezko Lote Tamaina",
"optimal_temperature": "Tenperatura Optimoa",
"optimal_humidity": "Hezetasun Optimoa",
"allergens": "Alergenoak",
"dietary_tags": "Dieta Etiketak",
"nutritional_info": "Nutrizio Informazioa"
},
"ingredients": {
"title": "Osagaiak",
"add_ingredient": "Osagaia Gehitu",
"remove_ingredient": "Osagaia Kendu",
"ingredient_name": "Osagaiaren Izena",
"quantity": "Kantitatea",
"unit": "Unitatea",
"alternative_quantity": "Ordezko Kantitatea",
"alternative_unit": "Ordezko Unitatea",
"preparation_method": "Prestaketa Metodoa",
"notes": "Osagaiaren Oharrak",
"is_optional": "Aukerakoa Da",
"ingredient_order": "Ordena",
"ingredient_group": "Taldea",
"substitutions": "Ordezpenak",
"substitution_ratio": "Ordezkapen Proportzioa",
"cost_per_unit": "Kostu Unitarioa",
"total_cost": "Kostu Totala",
"groups": {
"wet_ingredients": "Osagai Hezeak",
"dry_ingredients": "Osagai Lehorrak",
"spices": "Espezieak eta Apainkiak",
"toppings": "Gainekoak",
"fillings": "Betetzeak",
"decorations": "Apainkiak"
}
},
"status": {
"draft": "Zirriborroa",
"active": "Aktiboa",
"testing": "Probetan",
"archived": "Artxibatua",
"discontinued": "Etena"
},
"difficulty": {
"1": "Oso Erraza",
"2": "Erraza",
"3": "Ertaina",
"4": "Zaila",
"5": "Oso Zaila"
},
"units": {
"g": "gramoak",
"kg": "kilogramoak",
"ml": "mililitroak",
"l": "litroak",
"cups": "kikarak",
"tbsp": "koilarakada",
"tsp": "koilaratxo",
"units": "unitateak",
"pieces": "zatiak",
"%": "ehunekoa"
},
"categories": {
"bread": "Ogiak",
"pastry": "Gozogintza",
"cake": "Tartoak eta Pastelak",
"cookies": "Galletak",
"savory": "Gazidunak",
"desserts": "Postreak",
"seasonal": "Denboraldia",
"specialty": "Espezialitatea"
},
"dietary_tags": {
"vegan": "Beganoa",
"vegetarian": "Begetarianoa",
"gluten_free": "Glutenik Gabe",
"dairy_free": "Esnekiak Gabe",
"nut_free": "Fruitu Lehorrik Gabe",
"sugar_free": "Azukrerik Gabe",
"low_carb": "Karbohidrato Gutxi",
"keto": "Ketogenikoa",
"organic": "Organikoa"
},
"allergens": {
"gluten": "Glutena",
"dairy": "Esnekiak",
"eggs": "Arrautzak",
"nuts": "Fruitu Lehorrak",
"soy": "Soja",
"sesame": "Sezamoa",
"fish": "Arraina",
"shellfish": "Itsaskiak"
},
"production": {
"title": "Ekoizpena",
"batch_number": "Lote Zenbakia",
"production_date": "Ekoizpen Data",
"planned_quantity": "Planifikatutako Kantitatea",
"actual_quantity": "Benetako Kantitatea",
"yield_percentage": "Etekina Ehunekoa",
"priority": "Lehentasuna",
"assigned_staff": "Esleitutako Langilea",
"production_notes": "Ekoizpen Oharrak",
"quality_score": "Kalitate Puntuazioa",
"quality_notes": "Kalitate Oharrak",
"defect_rate": "Akats Tasa",
"rework_required": "Berregin Behar Da",
"waste_quantity": "Hondakin Kantitatea",
"waste_reason": "Hondakin Arrazoia",
"efficiency": "Eraginkortasuna",
"material_cost": "Materialen Kostua",
"labor_cost": "Lan Kostua",
"overhead_cost": "Gastu Orokorrak",
"total_cost": "Kostu Totala",
"cost_per_unit": "Unitateko Kostua",
"status": {
"planned": "Planifikatua",
"in_progress": "Abian",
"completed": "Osatua",
"failed": "Huts Egin Du",
"cancelled": "Ezeztatua"
},
"priority": {
"low": "Baxua",
"normal": "Normala",
"high": "Altua",
"urgent": "Larria"
}
},
"feasibility": {
"title": "Bideragarritasun Egiaztapena",
"feasible": "Bideragarria",
"not_feasible": "Ez Bideragarria",
"missing_ingredients": "Osagai Faltsuak",
"insufficient_ingredients": "Osagai Nahikorik Ez",
"batch_multiplier": "Lote Biderkatzailea",
"required_quantity": "Beharrezko Kantitatea",
"available_quantity": "Eskuragarri Dagoen Kantitatea",
"shortage": "Gabezia"
},
"statistics": {
"title": "Errezeta Estatistikak",
"total_recipes": "Errezeta Guztira",
"active_recipes": "Errezeta Aktiboak",
"signature_recipes": "Errezeta Ezagunak",
"seasonal_recipes": "Denboraldiko Errezetak",
"category_breakdown": "Kategoriaren Banaketa",
"most_popular": "Popularrenak",
"most_profitable": "Errentagarrienak",
"production_volume": "Ekoizpen Bolumena"
},
"filters": {
"all": "Guztiak",
"search_placeholder": "Bilatu errezetak...",
"status_filter": "Iragazi Egoeraren Arabera",
"category_filter": "Iragazi Kategoriaren Arabera",
"difficulty_filter": "Iragazi Zailtasunaren Arabera",
"seasonal_filter": "Denboraldiko Errezetak Bakarrik",
"signature_filter": "Errezeta Ezagunak Bakarrik",
"clear_filters": "Iragazkiak Garbitu"
},
"costs": {
"estimated_cost": "Kalkulatutako Kostua",
"last_calculated": "Azken Kalkulua",
"suggested_price": "Gomendatutako Prezioa",
"margin_percentage": "Marjina Ehunekoa",
"cost_breakdown": "Kostuen Banaketa",
"ingredient_costs": "Osagaien Kostuak",
"labor_costs": "Lan Kostuak",
"overhead_costs": "Gastu Orokorrak"
},
"messages": {
"recipe_created": "Errezeta ongi sortu da",
"recipe_updated": "Errezeta ongi eguneratu da",
"recipe_deleted": "Errezeta ongi ezabatu da",
"recipe_duplicated": "Errezeta ongi bikoiztu da",
"recipe_activated": "Errezeta ongi aktibatu da",
"batch_created": "Ekoizpen lotea ongi sortu da",
"batch_started": "Ekoizpena ongi hasi da",
"batch_completed": "Lotea ongi osatu da",
"batch_cancelled": "Lotea ongi ezeztatu da",
"feasibility_checked": "Bideragarritasuna egiaztatuta",
"loading_recipes": "Errezetak kargatzen...",
"loading_recipe": "Errezeta kargatzen...",
"no_recipes_found": "Ez da errezeta aurkitu",
"no_ingredients": "Ez dago osagairik gehituta",
"confirm_delete": "Ziur zaude errezeta hau ezabatu nahi duzula?",
"confirm_cancel_batch": "Ziur zaude lote hau ezeztatu nahi duzula?",
"recipe_name_required": "Errezeta izena beharrezkoa da",
"at_least_one_ingredient": "Gutxienez osagai bat gehitu behar duzu",
"invalid_quantity": "Kantitatea 0 baino handiagoa izan behar da",
"ingredient_required": "Osagai bat hautatu behar duzu"
},
"placeholders": {
"recipe_name": "Adib: Ogi Klasiko Hartziduna",
"recipe_code": "Adib: OGI-001",
"description": "Deskribatu errezeta honen ezaugarri bereziak...",
"preparation_notes": "Prestaketarako ohar bereziak...",
"storage_instructions": "Nola gorde produktu amaituak...",
"quality_standards": "Azken produktuaren kalitate irizpideak...",
"batch_number": "Adib: LOTE-20231201-001",
"production_notes": "Lote honetarako ohar zehatzak...",
"quality_notes": "Kalitateari buruzko oharrak...",
"waste_reason": "Hondakinaren arrazoia..."
},
"tooltips": {
"difficulty_level": "1 (oso erraza) eta 5 (oso zaila) bitarteko maila",
"yield_quantity": "Errezeta honek ekoizten duen kantitatea",
"batch_multiplier": "Errezeta eskalatzeko faktorea",
"target_margin": "Irabazi marjinaren helburua ehunekoan",
"optimal_temperature": "Ekoizpenerako tenperatura egokiena",
"optimal_humidity": "Ekoizpenerako hezetasun egokiena",
"is_seasonal": "Markatu denboraldi zehatz batekoa bada",
"is_signature": "Markatu okindegiaren errezeta berezia bada"
}
}

View File

@@ -5,7 +5,8 @@
"tabs": {
"information": "Informazioa",
"hours": "Ordutegiak",
"operations": "Ezarpenak"
"operations": "Ezarpenak",
"notifications": "Jakinarazpenak"
},
"information": {
"title": "Informazio Orokorra",

View File

@@ -18,6 +18,7 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { RefreshCw, ExternalLink, Plus, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useTenant } from '../../stores/tenant.store';
import {
useBakeryHealthStatus,
@@ -38,10 +39,10 @@ import { InsightsGrid } from '../../components/dashboard/InsightsGrid';
import { UnifiedAddWizard } from '../../components/domain/unified-wizard';
import type { ItemType } from '../../components/domain/unified-wizard';
import { useDemoTour, shouldStartTour, clearTourStartPending } from '../../features/demo-onboarding';
import { DemoBanner } from '../../components/layout/DemoBanner/DemoBanner';
export function NewDashboardPage() {
const navigate = useNavigate();
const { t } = useTranslation(['dashboard', 'common']);
const { currentTenant } = useTenant();
const tenantId = currentTenant?.id || '';
const { startTour } = useDemoTour();
@@ -188,16 +189,13 @@ export function NewDashboardPage() {
return (
<div className="min-h-screen pb-20 md:pb-8" style={{ backgroundColor: 'var(--bg-secondary)' }}>
{/* Demo Banner */}
{isDemoMode && <DemoBanner />}
{/* Mobile-optimized container */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl md:text-4xl font-bold" style={{ color: 'var(--text-primary)' }}>Panel de Control</h1>
<p className="mt-1" style={{ color: 'var(--text-secondary)' }}>Your bakery at a glance</p>
<h1 className="text-3xl md:text-4xl font-bold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:title')}</h1>
<p className="mt-1" style={{ color: 'var(--text-secondary)' }}>{t('dashboard:subtitle')}</p>
</div>
{/* Action Buttons */}
@@ -213,7 +211,7 @@ export function NewDashboardPage() {
}}
>
<RefreshCw className="w-5 h-5" />
<span className="hidden sm:inline">Refresh</span>
<span className="hidden sm:inline">{t('common:actions.refresh')}</span>
</button>
{/* Unified Add Button */}
@@ -226,7 +224,7 @@ export function NewDashboardPage() {
}}
>
<Plus className="w-5 h-5" />
<span className="hidden sm:inline">Agregar</span>
<span className="hidden sm:inline">{t('common:actions.add')}</span>
<Sparkles className="w-4 h-4 opacity-80" />
</button>
</div>
@@ -272,20 +270,20 @@ export function NewDashboardPage() {
{/* SECTION 5: Quick Insights Grid */}
<div>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>Key Metrics</h2>
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.key_metrics')}</h2>
<InsightsGrid insights={insights} loading={insightsLoading} />
</div>
{/* SECTION 6: Quick Action Links */}
<div className="rounded-xl shadow-md p-6" style={{ backgroundColor: 'var(--bg-primary)' }}>
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>Quick Actions</h2>
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>{t('dashboard:sections.quick_actions')}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<button
onClick={() => navigate('/app/operations/procurement')}
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-info)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>View Orders</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_orders')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-info)' }} />
</button>
@@ -294,7 +292,7 @@ export function NewDashboardPage() {
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-success)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Production</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_production')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-success)' }} />
</button>
@@ -303,7 +301,7 @@ export function NewDashboardPage() {
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-secondary)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Inventory</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_inventory')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-secondary)' }} />
</button>
@@ -312,7 +310,7 @@ export function NewDashboardPage() {
className="flex items-center justify-between p-4 rounded-lg transition-colors duration-200 group"
style={{ backgroundColor: 'var(--bg-tertiary)', borderLeft: '4px solid var(--color-warning)' }}
>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>Suppliers</span>
<span className="font-semibold" style={{ color: 'var(--text-primary)' }}>{t('dashboard:quick_actions.view_suppliers')}</span>
<ExternalLink className="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" style={{ color: 'var(--color-warning)' }} />
</button>
</div>

View File

@@ -0,0 +1,377 @@
import React from 'react';
import { Bell, MessageSquare, Mail, AlertCircle, Globe } from 'lucide-react';
import { Card, Input } from '../../../../../components/ui';
import type { NotificationSettings } from '../../../../../api/types/settings';
import { useTranslation } from 'react-i18next';
interface NotificationSettingsCardProps {
settings: NotificationSettings;
onChange: (settings: NotificationSettings) => void;
disabled?: boolean;
}
const NotificationSettingsCard: React.FC<NotificationSettingsCardProps> = ({
settings,
onChange,
disabled = false,
}) => {
const { t } = useTranslation('ajustes');
const handleChange = (field: keyof NotificationSettings) => (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const value = e.target.type === 'checkbox' ? (e.target as HTMLInputElement).checked :
e.target.value;
onChange({ ...settings, [field]: value });
};
const handleChannelChange = (field: 'po_notification_channels' | 'inventory_alert_channels' | 'production_alert_channels' | 'forecast_alert_channels', channel: string) => {
const currentChannels = settings[field];
const newChannels = currentChannels.includes(channel)
? currentChannels.filter(c => c !== channel)
: [...currentChannels, channel];
onChange({ ...settings, [field]: newChannels });
};
return (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6 flex items-center">
<Bell className="w-5 h-5 mr-2 text-[var(--color-primary)]" />
{t('notification.title')}
</h3>
<div className="space-y-6">
{/* WhatsApp Configuration */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<MessageSquare className="w-4 h-4 mr-2" />
{t('notification.whatsapp_config')}
</h4>
<div className="space-y-4 pl-6">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="whatsapp_enabled"
checked={settings.whatsapp_enabled}
onChange={handleChange('whatsapp_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="whatsapp_enabled" className="text-sm text-[var(--text-secondary)]">
{t('notification.whatsapp_enabled')}
</label>
</div>
{settings.whatsapp_enabled && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<Input
label={t('notification.whatsapp_phone_number_id')}
value={settings.whatsapp_phone_number_id}
onChange={handleChange('whatsapp_phone_number_id')}
disabled={disabled}
placeholder="123456789012345"
helperText={t('notification.whatsapp_phone_number_id_help')}
/>
<Input
type="password"
label={t('notification.whatsapp_access_token')}
value={settings.whatsapp_access_token}
onChange={handleChange('whatsapp_access_token')}
disabled={disabled}
placeholder="EAAxxxxxxxx"
helperText={t('notification.whatsapp_access_token_help')}
/>
<Input
label={t('notification.whatsapp_business_account_id')}
value={settings.whatsapp_business_account_id}
onChange={handleChange('whatsapp_business_account_id')}
disabled={disabled}
placeholder="987654321098765"
helperText={t('notification.whatsapp_business_account_id_help')}
/>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('notification.whatsapp_api_version')}
</label>
<select
value={settings.whatsapp_api_version}
onChange={handleChange('whatsapp_api_version')}
disabled={disabled}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
>
<option value="v18.0">v18.0</option>
<option value="v19.0">v19.0</option>
<option value="v20.0">v20.0</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
{t('notification.whatsapp_default_language')}
</label>
<select
value={settings.whatsapp_default_language}
onChange={handleChange('whatsapp_default_language')}
disabled={disabled}
className="w-full px-3 py-2 border border-[var(--border-primary)] rounded-lg text-sm bg-[var(--bg-primary)] text-[var(--text-primary)] disabled:bg-[var(--bg-secondary)] disabled:text-[var(--text-secondary)]"
>
<option value="es">Español</option>
<option value="eu">Euskara</option>
<option value="en">English</option>
</select>
</div>
</div>
)}
{settings.whatsapp_enabled && (
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<div className="text-xs text-blue-700 dark:text-blue-300">
<p className="font-semibold mb-1">{t('notification.whatsapp_setup_note')}</p>
<ul className="list-disc list-inside space-y-1">
<li>{t('notification.whatsapp_setup_step1')}</li>
<li>{t('notification.whatsapp_setup_step2')}</li>
<li>{t('notification.whatsapp_setup_step3')}</li>
</ul>
</div>
</div>
</div>
)}
</div>
</div>
{/* Email Configuration */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Mail className="w-4 h-4 mr-2" />
{t('notification.email_config')}
</h4>
<div className="space-y-4 pl-6">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="email_enabled"
checked={settings.email_enabled}
onChange={handleChange('email_enabled')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="email_enabled" className="text-sm text-[var(--text-secondary)]">
{t('notification.email_enabled')}
</label>
</div>
{settings.email_enabled && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<Input
type="email"
label={t('notification.email_from_address')}
value={settings.email_from_address}
onChange={handleChange('email_from_address')}
disabled={disabled}
placeholder="orders@yourbakery.com"
/>
<Input
label={t('notification.email_from_name')}
value={settings.email_from_name}
onChange={handleChange('email_from_name')}
disabled={disabled}
placeholder="Your Bakery Name"
/>
<Input
type="email"
label={t('notification.email_reply_to')}
value={settings.email_reply_to}
onChange={handleChange('email_reply_to')}
disabled={disabled}
placeholder="info@yourbakery.com"
/>
</div>
)}
</div>
</div>
{/* Notification Preferences */}
<div>
<h4 className="text-sm font-semibold text-[var(--text-secondary)] mb-4 flex items-center">
<Globe className="w-4 h-4 mr-2" />
{t('notification.preferences')}
</h4>
<div className="space-y-4 pl-6">
{/* PO Notifications */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_po_notifications"
checked={settings.enable_po_notifications}
onChange={handleChange('enable_po_notifications')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_po_notifications" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_po_notifications')}
</label>
</div>
{settings.enable_po_notifications && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.po_notification_channels.includes('email')}
onChange={() => handleChannelChange('po_notification_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.po_notification_channels.includes('whatsapp')}
onChange={() => handleChannelChange('po_notification_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
{/* Inventory Alerts */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_inventory_alerts"
checked={settings.enable_inventory_alerts}
onChange={handleChange('enable_inventory_alerts')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_inventory_alerts" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_inventory_alerts')}
</label>
</div>
{settings.enable_inventory_alerts && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.inventory_alert_channels.includes('email')}
onChange={() => handleChannelChange('inventory_alert_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.inventory_alert_channels.includes('whatsapp')}
onChange={() => handleChannelChange('inventory_alert_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
{/* Production Alerts */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_production_alerts"
checked={settings.enable_production_alerts}
onChange={handleChange('enable_production_alerts')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_production_alerts" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_production_alerts')}
</label>
</div>
{settings.enable_production_alerts && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.production_alert_channels.includes('email')}
onChange={() => handleChannelChange('production_alert_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.production_alert_channels.includes('whatsapp')}
onChange={() => handleChannelChange('production_alert_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
{/* Forecast Alerts */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enable_forecast_alerts"
checked={settings.enable_forecast_alerts}
onChange={handleChange('enable_forecast_alerts')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
<label htmlFor="enable_forecast_alerts" className="text-sm font-medium text-[var(--text-secondary)]">
{t('notification.enable_forecast_alerts')}
</label>
</div>
{settings.enable_forecast_alerts && (
<div className="pl-6 flex gap-4">
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.forecast_alert_channels.includes('email')}
onChange={() => handleChannelChange('forecast_alert_channels', 'email')}
disabled={disabled}
className="rounded border-[var(--border-primary)]"
/>
Email
</label>
<label className="flex items-center gap-2 text-sm text-[var(--text-tertiary)]">
<input
type="checkbox"
checked={settings.forecast_alert_channels.includes('whatsapp')}
onChange={() => handleChannelChange('forecast_alert_channels', 'whatsapp')}
disabled={disabled || !settings.whatsapp_enabled}
className="rounded border-[var(--border-primary)]"
/>
WhatsApp
</label>
</div>
)}
</div>
</div>
</div>
</div>
</Card>
);
};
export default NotificationSettingsCard;

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Store, MapPin, Clock, Settings as SettingsIcon, Save, X, AlertCircle, Loader } from 'lucide-react';
import { Store, MapPin, Clock, Settings as SettingsIcon, Save, X, AlertCircle, Loader, Bell } from 'lucide-react';
import { Button, Card, Input, Select } from '../../../../components/ui';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../components/ui/Tabs';
import { PageHeader } from '../../../../components/layout';
@@ -15,6 +15,7 @@ import type {
SupplierSettings,
POSSettings,
OrderSettings,
NotificationSettings,
} from '../../../../api/types/settings';
import ProcurementSettingsCard from '../../database/ajustes/cards/ProcurementSettingsCard';
import InventorySettingsCard from '../../database/ajustes/cards/InventorySettingsCard';
@@ -22,6 +23,7 @@ import ProductionSettingsCard from '../../database/ajustes/cards/ProductionSetti
import SupplierSettingsCard from '../../database/ajustes/cards/SupplierSettingsCard';
import POSSettingsCard from '../../database/ajustes/cards/POSSettingsCard';
import OrderSettingsCard from '../../database/ajustes/cards/OrderSettingsCard';
import NotificationSettingsCard from '../../database/ajustes/cards/NotificationSettingsCard';
interface BakeryConfig {
name: string;
@@ -98,6 +100,7 @@ const BakerySettingsPage: React.FC = () => {
const [supplierSettings, setSupplierSettings] = useState<SupplierSettings | null>(null);
const [posSettings, setPosSettings] = useState<POSSettings | null>(null);
const [orderSettings, setOrderSettings] = useState<OrderSettings | null>(null);
const [notificationSettings, setNotificationSettings] = useState<NotificationSettings | null>(null);
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -137,6 +140,7 @@ const BakerySettingsPage: React.FC = () => {
setSupplierSettings(settings.supplier_settings);
setPosSettings(settings.pos_settings);
setOrderSettings(settings.order_settings);
setNotificationSettings(settings.notification_settings);
}
}, [settings]);
@@ -232,7 +236,7 @@ const BakerySettingsPage: React.FC = () => {
const handleSaveOperationalSettings = async () => {
if (!tenantId || !procurementSettings || !inventorySettings || !productionSettings ||
!supplierSettings || !posSettings || !orderSettings) {
!supplierSettings || !posSettings || !orderSettings || !notificationSettings) {
return;
}
@@ -248,6 +252,7 @@ const BakerySettingsPage: React.FC = () => {
supplier_settings: supplierSettings,
pos_settings: posSettings,
order_settings: orderSettings,
notification_settings: notificationSettings,
},
});
@@ -314,6 +319,7 @@ const BakerySettingsPage: React.FC = () => {
setSupplierSettings(settings.supplier_settings);
setPosSettings(settings.pos_settings);
setOrderSettings(settings.order_settings);
setNotificationSettings(settings.notification_settings);
}
setHasUnsavedChanges(false);
};
@@ -387,6 +393,10 @@ const BakerySettingsPage: React.FC = () => {
<SettingsIcon className="w-4 h-4 mr-2" />
{t('bakery.tabs.operations')}
</TabsTrigger>
<TabsTrigger value="notifications" className="flex-1 sm:flex-none whitespace-nowrap">
<Bell className="w-4 h-4 mr-2" />
{t('bakery.tabs.notifications')}
</TabsTrigger>
</TabsList>
{/* Tab 1: Information */}
@@ -689,6 +699,22 @@ const BakerySettingsPage: React.FC = () => {
)}
</div>
</TabsContent>
{/* Tab 4: Notifications */}
<TabsContent value="notifications">
<div className="space-y-6">
{notificationSettings && (
<NotificationSettingsCard
settings={notificationSettings}
onChange={(newSettings) => {
setNotificationSettings(newSettings);
handleOperationalSettingsChange();
}}
disabled={isLoading}
/>
)}
</div>
</TabsContent>
</Tabs>
{/* Floating Save Button */}
@@ -714,7 +740,7 @@ const BakerySettingsPage: React.FC = () => {
<Button
variant="primary"
size="sm"
onClick={activeTab === 'operations' ? handleSaveOperationalSettings : handleSaveConfig}
onClick={activeTab === 'operations' || activeTab === 'notifications' ? handleSaveOperationalSettings : handleSaveConfig}
isLoading={isLoading}
loadingText={t('common.saving')}
className="flex-1 sm:flex-none"

View File

@@ -37,35 +37,35 @@
--color-secondary-light: #4ade80;
--color-secondary-dark: #16a34a;
/* Success Colors */
--color-success-50: #ecfdf5;
--color-success-100: #d1fae5;
--color-success-200: #a7f3d0;
--color-success-300: #6ee7b7;
--color-success-400: #34d399;
/* Success Colors - Inverted scale for dark mode */
--color-success-50: #064e3b;
--color-success-100: #065f46;
--color-success-200: #047857;
--color-success-300: #059669;
--color-success-400: #10b981;
--color-success-500: #10b981;
--color-success-600: #059669;
--color-success-700: #047857;
--color-success-800: #065f46;
--color-success-900: #064e3b;
--color-success-600: #34d399;
--color-success-700: #6ee7b7;
--color-success-800: #a7f3d0;
--color-success-900: #d1fae5;
--color-success: #22c55e; /* Brighter for dark theme */
--color-success-light: #4ade80;
--color-success-dark: #16a34a;
/* Warning Colors - Inverted scale for dark mode */
--color-warning-50: #422006;
--color-warning-100: #78350f;
--color-warning-200: #9a3412;
--color-warning-300: #c2410c;
--color-warning-400: #ea580c;
--color-warning-500: #f59e0b;
--color-warning-600: #fbbf24;
--color-warning-700: #fcd34d;
--color-warning-800: #fde68a;
--color-warning-900: #fef3c7;
--color-warning: #fb923c; /* Brighter for dark theme */
--color-warning-light: #fdba74;
--color-warning-dark: #ea580c;
--color-warning-50: #78350f;
--color-warning-100: #92400e;
--color-warning-200: #b45309;
--color-warning-300: #d97706;
--color-warning-400: #f59e0b;
--color-warning-500: #fbbf24;
--color-warning-600: #fcd34d;
--color-warning-700: #fde68a;
--color-warning-800: #fef3c7;
--color-warning-900: #fffbeb;
--color-warning: #fbbf24; /* Brighter for dark theme */
--color-warning-light: #fcd34d;
--color-warning-dark: #f59e0b;
/* Error Colors - Inverted scale for dark mode */
--color-error-50: #450a0a;
@@ -83,19 +83,19 @@
--color-error-dark: #dc2626;
/* Info Colors - Adjusted for dark mode */
--color-info-50: #0c4a6e;
--color-info-100: #075985;
--color-info-200: #0369a1;
--color-info-300: #0284c7;
--color-info-400: #0ea5e9;
--color-info-500: #38bdf8;
--color-info-600: #38bdf8;
--color-info-700: #60a5fa;
--color-info-800: #93c5fd;
--color-info-900: #bfdbfe;
--color-info: #38bdf8; /* Brighter cyan for dark theme */
--color-info-light: #60a5fa;
--color-info-dark: #0ea5e9;
--color-info-50: #172554;
--color-info-100: #1e3a8a;
--color-info-200: #1e40af;
--color-info-300: #1d4ed8;
--color-info-400: #2563eb;
--color-info-500: #3b82f6;
--color-info-600: #60a5fa;
--color-info-700: #93c5fd;
--color-info-800: #bfdbfe;
--color-info-900: #dbeafe;
--color-info: #60a5fa; /* Brighter blue for dark theme */
--color-info-light: #93c5fd;
--color-info-dark: #3b82f6;
/* === THEME-SPECIFIC COLORS === */