Add whatsapp feature
This commit is contained in:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user