Add whatsapp feature
This commit is contained in:
@@ -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 =>
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"tabs": {
|
||||
"information": "Datos del establecimiento",
|
||||
"hours": "Horarios",
|
||||
"operations": "Ajustes operacionales"
|
||||
"operations": "Ajustes operacionales",
|
||||
"notifications": "Notificaciones"
|
||||
},
|
||||
"information": {
|
||||
"title": "Información General",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"tabs": {
|
||||
"information": "Informazioa",
|
||||
"hours": "Ordutegiak",
|
||||
"operations": "Ezarpenak"
|
||||
"operations": "Ezarpenak",
|
||||
"notifications": "Jakinarazpenak"
|
||||
},
|
||||
"information": {
|
||||
"title": "Informazio Orokorra",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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 === */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user