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

@@ -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}
/>