Files
bakery-ia/frontend/src/components/domain/sales/CustomerInfo.tsx
2025-08-28 10:41:04 +02:00

1272 lines
50 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import {
Card,
Button,
Badge,
Input,
Select,
Avatar,
Tooltip,
Modal
} from '../../ui';
import {
SalesRecord,
SalesChannel,
PaymentMethod
} from '../../../types/sales.types';
import { salesService } from '../../../services/api/sales.service';
import { useSales } from '../../../hooks/api/useSales';
// Customer interfaces
interface Customer {
id: string;
name: string;
email?: string;
phone?: string;
address?: string;
city?: string;
postal_code?: string;
birth_date?: string;
registration_date: string;
status: CustomerStatus;
segment: CustomerSegment;
loyalty_points: number;
preferred_channel: SalesChannel;
notes?: string;
tags: string[];
communication_preferences: CommunicationPreferences;
}
interface CustomerOrder {
id: string;
date: string;
total: number;
items_count: number;
status: OrderStatus;
channel: SalesChannel;
payment_method?: PaymentMethod;
products: string[];
}
interface CustomerStats {
total_orders: number;
total_spent: number;
average_order_value: number;
last_order_date?: string;
favorite_products: string[];
preferred_times: string[];
loyalty_tier: LoyaltyTier;
lifetime_value: number;
churn_risk: ChurnRisk;
}
interface CommunicationPreferences {
email_marketing: boolean;
sms_notifications: boolean;
push_notifications: boolean;
promotional_offers: boolean;
order_updates: boolean;
loyalty_updates: boolean;
}
interface PaymentMethod {
id: string;
type: 'credit_card' | 'debit_card' | 'digital_wallet' | 'bank_account';
last_four: string;
brand?: string;
is_default: boolean;
expires_at?: string;
}
interface LoyaltyProgram {
current_points: number;
points_to_next_tier: number;
current_tier: LoyaltyTier;
benefits: string[];
rewards_history: LoyaltyTransaction[];
}
interface LoyaltyTransaction {
id: string;
date: string;
type: 'earned' | 'redeemed';
points: number;
description: string;
order_id?: string;
}
enum CustomerStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
SUSPENDED = 'suspended',
VIP = 'vip'
}
enum CustomerSegment {
NEW = 'new',
REGULAR = 'regular',
PREMIUM = 'premium',
ENTERPRISE = 'enterprise',
AT_RISK = 'at_risk'
}
enum OrderStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
PREPARING = 'preparing',
READY = 'ready',
DELIVERED = 'delivered',
CANCELLED = 'cancelled'
}
enum LoyaltyTier {
BRONZE = 'bronze',
SILVER = 'silver',
GOLD = 'gold',
PLATINUM = 'platinum'
}
enum ChurnRisk {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
interface CustomerInfoProps {
customerId?: string;
onCustomerSelect?: (customer: Customer) => void;
onCustomerUpdate?: (customerId: string, updates: Partial<Customer>) => void;
showOrderHistory?: boolean;
showLoyaltyProgram?: boolean;
allowEditing?: boolean;
className?: string;
}
const StatusColors = {
[CustomerStatus.ACTIVE]: 'green',
[CustomerStatus.INACTIVE]: 'gray',
[CustomerStatus.SUSPENDED]: 'red',
[CustomerStatus.VIP]: 'purple'
} as const;
const StatusLabels = {
[CustomerStatus.ACTIVE]: 'Activo',
[CustomerStatus.INACTIVE]: 'Inactivo',
[CustomerStatus.SUSPENDED]: 'Suspendido',
[CustomerStatus.VIP]: 'VIP'
} as const;
const SegmentColors = {
[CustomerSegment.NEW]: 'blue',
[CustomerSegment.REGULAR]: 'gray',
[CustomerSegment.PREMIUM]: 'gold',
[CustomerSegment.ENTERPRISE]: 'purple',
[CustomerSegment.AT_RISK]: 'red'
} as const;
const SegmentLabels = {
[CustomerSegment.NEW]: 'Nuevo',
[CustomerSegment.REGULAR]: 'Regular',
[CustomerSegment.PREMIUM]: 'Premium',
[CustomerSegment.ENTERPRISE]: 'Empresa',
[CustomerSegment.AT_RISK]: 'En Riesgo'
} as const;
const TierColors = {
[LoyaltyTier.BRONZE]: 'orange',
[LoyaltyTier.SILVER]: 'gray',
[LoyaltyTier.GOLD]: 'yellow',
[LoyaltyTier.PLATINUM]: 'purple'
} as const;
const TierLabels = {
[LoyaltyTier.BRONZE]: 'Bronce',
[LoyaltyTier.SILVER]: 'Plata',
[LoyaltyTier.GOLD]: 'Oro',
[LoyaltyTier.PLATINUM]: 'Platino'
} as const;
const ChurnRiskColors = {
[ChurnRisk.LOW]: 'green',
[ChurnRisk.MEDIUM]: 'yellow',
[ChurnRisk.HIGH]: 'orange',
[ChurnRisk.CRITICAL]: 'red'
} as const;
const ChurnRiskLabels = {
[ChurnRisk.LOW]: 'Bajo',
[ChurnRisk.MEDIUM]: 'Medio',
[ChurnRisk.HIGH]: 'Alto',
[ChurnRisk.CRITICAL]: 'Crítico'
} as const;
const ChannelLabels = {
[SalesChannel.STORE_FRONT]: 'Tienda',
[SalesChannel.ONLINE]: 'Online',
[SalesChannel.PHONE_ORDER]: 'Teléfono',
[SalesChannel.DELIVERY]: 'Delivery',
[SalesChannel.CATERING]: 'Catering',
[SalesChannel.WHOLESALE]: 'Mayorista',
[SalesChannel.FARMERS_MARKET]: 'Mercado',
[SalesChannel.THIRD_PARTY]: 'Terceros'
} as const;
export const CustomerInfo: React.FC<CustomerInfoProps> = ({
customerId,
onCustomerSelect,
onCustomerUpdate,
showOrderHistory = true,
showLoyaltyProgram = true,
allowEditing = true,
className = ''
}) => {
// State
const [customer, setCustomer] = useState<Customer | null>(null);
const [customerStats, setCustomerStats] = useState<CustomerStats | null>(null);
const [orderHistory, setOrderHistory] = useState<CustomerOrder[]>([]);
const [loyaltyProgram, setLoyaltyProgram] = useState<LoyaltyProgram | null>(null);
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// UI State
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState<Partial<Customer>>({});
const [activeTab, setActiveTab] = useState<'info' | 'orders' | 'loyalty' | 'communication'>('info');
const [showAddNote, setShowAddNote] = useState(false);
const [newNote, setNewNote] = useState('');
const [showLoyaltyModal, setShowLoyaltyModal] = useState(false);
const [rewardToRedeem, setRewardToRedeem] = useState<string | null>(null);
// Pagination for order history
const [orderPage, setOrderPage] = useState(1);
const [orderPageSize, setOrderPageSize] = useState(10);
// Effects
useEffect(() => {
if (customerId) {
loadCustomerData(customerId);
}
}, [customerId]);
// Load customer data
const loadCustomerData = async (id: string) => {
setLoading(true);
setError(null);
try {
// In a real app, these would be separate API calls
const mockCustomer: Customer = {
id,
name: 'María García López',
email: 'maria.garcia@email.com',
phone: '+34 612 345 678',
address: 'Calle Mayor, 123',
city: 'Madrid',
postal_code: '28001',
birth_date: '1985-05-15',
registration_date: '2023-01-15T00:00:00Z',
status: CustomerStatus.VIP,
segment: CustomerSegment.PREMIUM,
loyalty_points: 2450,
preferred_channel: SalesChannel.ONLINE,
notes: 'Cliente preferente. Le gusta el pan integral sin gluten.',
tags: ['gluten-free', 'premium', 'weekly-order'],
communication_preferences: {
email_marketing: true,
sms_notifications: true,
push_notifications: false,
promotional_offers: true,
order_updates: true,
loyalty_updates: true
}
};
const mockStats: CustomerStats = {
total_orders: 47,
total_spent: 1247.85,
average_order_value: 26.55,
last_order_date: '2024-01-20T10:30:00Z',
favorite_products: ['Pan Integral', 'Croissant de Chocolate', 'Tarta de Santiago'],
preferred_times: ['09:00-11:00', '17:00-19:00'],
loyalty_tier: LoyaltyTier.GOLD,
lifetime_value: 1850.00,
churn_risk: ChurnRisk.LOW
};
const mockOrders: CustomerOrder[] = Array.from({ length: 47 }, (_, i) => ({
id: `order_${i + 1}`,
date: new Date(Date.now() - i * 7 * 24 * 60 * 60 * 1000).toISOString(),
total: Math.random() * 50 + 15,
items_count: Math.floor(Math.random() * 5) + 1,
status: Object.values(OrderStatus)[Math.floor(Math.random() * Object.values(OrderStatus).length)],
channel: Object.values(SalesChannel)[Math.floor(Math.random() * Object.values(SalesChannel).length)],
payment_method: Object.values(PaymentMethod)[Math.floor(Math.random() * Object.values(PaymentMethod).length)],
products: ['Pan Integral', 'Croissant', 'Magdalenas'].slice(0, Math.floor(Math.random() * 3) + 1)
}));
const mockLoyalty: LoyaltyProgram = {
current_points: 2450,
points_to_next_tier: 550,
current_tier: LoyaltyTier.GOLD,
benefits: [
'Descuento 10% en todos los productos',
'Producto gratis cada 10 compras',
'Reserva prioritaria para productos especiales',
'Invitaciones exclusivas a eventos'
],
rewards_history: Array.from({ length: 20 }, (_, i) => ({
id: `loyalty_${i + 1}`,
date: new Date(Date.now() - i * 14 * 24 * 60 * 60 * 1000).toISOString(),
type: Math.random() > 0.7 ? 'redeemed' as const : 'earned' as const,
points: Math.floor(Math.random() * 200) + 50,
description: Math.random() > 0.7 ? 'Canje por producto gratis' : 'Puntos ganados por compra',
order_id: `order_${Math.floor(Math.random() * 47) + 1}`
}))
};
const mockPaymentMethods: PaymentMethod[] = [
{
id: 'pm_1',
type: 'credit_card',
last_four: '4242',
brand: 'Visa',
is_default: true,
expires_at: '2026-12-31'
},
{
id: 'pm_2',
type: 'digital_wallet',
last_four: 'PayPal',
is_default: false
}
];
setCustomer(mockCustomer);
setCustomerStats(mockStats);
setOrderHistory(mockOrders);
setLoyaltyProgram(mockLoyalty);
setPaymentMethods(mockPaymentMethods);
} catch (err) {
setError('Error al cargar datos del cliente');
console.error('Error loading customer data:', err);
} finally {
setLoading(false);
}
};
// Handle edit
const handleEditStart = () => {
if (customer) {
setEditForm({ ...customer });
setIsEditing(true);
}
};
const handleEditSave = async () => {
if (!customer || !editForm) return;
try {
const updatedCustomer = { ...customer, ...editForm };
setCustomer(updatedCustomer);
onCustomerUpdate?.(customer.id, editForm);
setIsEditing(false);
setEditForm({});
} catch (err) {
setError('Error al actualizar cliente');
}
};
const handleEditCancel = () => {
setIsEditing(false);
setEditForm({});
};
// Handle notes
const handleAddNote = () => {
if (!customer || !newNote.trim()) return;
const updatedNotes = customer.notes ? `${customer.notes}\n\n${new Date().toLocaleDateString('es-ES')}: ${newNote}` : newNote;
const updatedCustomer = { ...customer, notes: updatedNotes };
setCustomer(updatedCustomer);
onCustomerUpdate?.(customer.id, { notes: updatedNotes });
setNewNote('');
setShowAddNote(false);
};
// Handle loyalty redemption
const handleRedeemPoints = (rewardId: string, pointsCost: number) => {
if (!customer || !loyaltyProgram) return;
if (loyaltyProgram.current_points >= pointsCost) {
const newTransaction: LoyaltyTransaction = {
id: `loyalty_${Date.now()}`,
date: new Date().toISOString(),
type: 'redeemed',
points: -pointsCost,
description: `Canje: ${rewardId}`,
};
const updatedLoyalty = {
...loyaltyProgram,
current_points: loyaltyProgram.current_points - pointsCost,
rewards_history: [newTransaction, ...loyaltyProgram.rewards_history]
};
const updatedCustomer = {
...customer,
loyalty_points: loyaltyProgram.current_points - pointsCost
};
setLoyaltyProgram(updatedLoyalty);
setCustomer(updatedCustomer);
onCustomerUpdate?.(customer.id, { loyalty_points: updatedCustomer.loyalty_points });
}
};
// Filtered order history
const paginatedOrders = useMemo(() => {
const start = (orderPage - 1) * orderPageSize;
return orderHistory.slice(start, start + orderPageSize);
}, [orderHistory, orderPage, orderPageSize]);
if (loading) {
return (
<div className={`flex items-center justify-center p-8 ${className}`}>
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-[var(--text-secondary)]">Cargando información del cliente...</span>
</div>
);
}
if (error || !customer) {
return (
<div className={`p-6 ${className}`}>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex">
<svg className="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<div>
<h3 className="text-sm font-medium text-[var(--color-error)]">Error</h3>
<p className="text-sm text-[var(--color-error)] mt-1">{error || 'Cliente no encontrado'}</p>
</div>
</div>
</div>
</div>
);
}
return (
<div className={`space-y-6 ${className}`}>
{/* Header */}
<Card className="p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center space-x-4">
<Avatar
size="lg"
name={customer.name}
src={`https://api.dicebear.com/7.x/initials/svg?seed=${customer.name}`}
/>
<div>
<h2 className="text-2xl font-bold text-[var(--text-primary)]">{customer.name}</h2>
<div className="flex items-center space-x-2 mt-1">
<Badge color={StatusColors[customer.status]} variant="soft">
{StatusLabels[customer.status]}
</Badge>
<Badge color={SegmentColors[customer.segment]} variant="soft">
{SegmentLabels[customer.segment]}
</Badge>
{customerStats && (
<Badge color={TierColors[customerStats.loyalty_tier]} variant="soft">
{TierLabels[customerStats.loyalty_tier]}
</Badge>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-3">
{allowEditing && (
<>
{isEditing ? (
<>
<Button variant="outline" onClick={handleEditCancel}>
Cancelar
</Button>
<Button onClick={handleEditSave}>
Guardar
</Button>
</>
) : (
<Button variant="outline" onClick={handleEditStart}>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Editar
</Button>
)}
</>
)}
<Button onClick={() => setShowAddNote(true)}>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Agregar Nota
</Button>
</div>
</div>
</Card>
{/* Stats Overview */}
{customerStats && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Gastado</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{customerStats.total_spent.toFixed(2)}
</p>
</div>
<div className="p-2 bg-[var(--color-info)]/10 rounded-lg">
<svg className="w-6 h-6 text-[var(--color-info)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Pedidos Totales</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">{customerStats.total_orders}</p>
</div>
<div className="p-2 bg-[var(--color-success)]/10 rounded-lg">
<svg className="w-6 h-6 text-[var(--color-success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ticket Promedio</p>
<p className="text-2xl font-bold text-[var(--text-primary)]">
{customerStats.average_order_value.toFixed(2)}
</p>
</div>
<div className="p-2 bg-yellow-100 rounded-lg">
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Riesgo de Fuga</p>
<div className="flex items-center">
<Badge color={ChurnRiskColors[customerStats.churn_risk]} variant="soft">
{ChurnRiskLabels[customerStats.churn_risk]}
</Badge>
</div>
</div>
<div className={`p-2 rounded-lg ${
customerStats.churn_risk === ChurnRisk.LOW ? 'bg-[var(--color-success)]/10' :
customerStats.churn_risk === ChurnRisk.MEDIUM ? 'bg-yellow-100' :
customerStats.churn_risk === ChurnRisk.HIGH ? 'bg-[var(--color-primary)]/10' : 'bg-[var(--color-error)]/10'
}`}>
<svg className={`w-6 h-6 ${
customerStats.churn_risk === ChurnRisk.LOW ? 'text-[var(--color-success)]' :
customerStats.churn_risk === ChurnRisk.MEDIUM ? 'text-yellow-600' :
customerStats.churn_risk === ChurnRisk.HIGH ? 'text-[var(--color-primary)]' : 'text-[var(--color-error)]'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
</div>
</Card>
</div>
)}
{/* Tabs */}
<div className="border-b border-[var(--border-primary)]">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('info')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'info'
? 'border-blue-500 text-[var(--color-info)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Información Personal
</button>
{showOrderHistory && (
<button
onClick={() => setActiveTab('orders')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'orders'
? 'border-blue-500 text-[var(--color-info)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Historial de Pedidos
</button>
)}
{showLoyaltyProgram && (
<button
onClick={() => setActiveTab('loyalty')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'loyalty'
? 'border-blue-500 text-[var(--color-info)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Programa de Fidelización
</button>
)}
<button
onClick={() => setActiveTab('communication')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'communication'
? 'border-blue-500 text-[var(--color-info)]'
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
}`}
>
Comunicación
</button>
</nav>
</div>
{/* Tab Content */}
{activeTab === 'info' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Datos Personales</h3>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)]">Nombre</label>
{isEditing ? (
<Input
value={editForm.name || ''}
onChange={(e) => setEditForm(prev => ({ ...prev, name: e.target.value }))}
/>
) : (
<p className="text-[var(--text-primary)]">{customer.name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)]">Email</label>
{isEditing ? (
<Input
type="email"
value={editForm.email || ''}
onChange={(e) => setEditForm(prev => ({ ...prev, email: e.target.value }))}
/>
) : (
<p className="text-[var(--text-primary)]">{customer.email}</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)]">Teléfono</label>
{isEditing ? (
<Input
value={editForm.phone || ''}
onChange={(e) => setEditForm(prev => ({ ...prev, phone: e.target.value }))}
/>
) : (
<p className="text-[var(--text-primary)]">{customer.phone}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)]">Fecha de Nacimiento</label>
{isEditing ? (
<Input
type="date"
value={editForm.birth_date || ''}
onChange={(e) => setEditForm(prev => ({ ...prev, birth_date: e.target.value }))}
/>
) : (
<p className="text-[var(--text-primary)]">
{customer.birth_date ? new Date(customer.birth_date).toLocaleDateString('es-ES') : 'No especificado'}
</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)]">Dirección</label>
{isEditing ? (
<Input
value={editForm.address || ''}
onChange={(e) => setEditForm(prev => ({ ...prev, address: e.target.value }))}
/>
) : (
<p className="text-[var(--text-primary)]">{customer.address}</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)]">Ciudad</label>
{isEditing ? (
<Input
value={editForm.city || ''}
onChange={(e) => setEditForm(prev => ({ ...prev, city: e.target.value }))}
/>
) : (
<p className="text-[var(--text-primary)]">{customer.city}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)]">Código Postal</label>
{isEditing ? (
<Input
value={editForm.postal_code || ''}
onChange={(e) => setEditForm(prev => ({ ...prev, postal_code: e.target.value }))}
/>
) : (
<p className="text-[var(--text-primary)]">{customer.postal_code}</p>
)}
</div>
</div>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Preferencias y Segmentación</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Canal Preferido</label>
{isEditing ? (
<Select
value={editForm.preferred_channel || customer.preferred_channel}
onChange={(value) => setEditForm(prev => ({ ...prev, preferred_channel: value as SalesChannel }))}
options={Object.values(SalesChannel).map(channel => ({
value: channel,
label: ChannelLabels[channel]
}))}
/>
) : (
<p className="text-[var(--text-primary)]">{ChannelLabels[customer.preferred_channel]}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Tags</label>
<div className="flex flex-wrap gap-2">
{customer.tags.map(tag => (
<Badge key={tag} variant="soft" color="gray">
{tag}
</Badge>
))}
</div>
</div>
{customerStats && (
<>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Productos Favoritos</label>
<div className="space-y-1">
{customerStats.favorite_products.map(product => (
<p key={product} className="text-[var(--text-primary)] text-sm"> {product}</p>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Horarios Preferidos</label>
<div className="space-y-1">
{customerStats.preferred_times.map(time => (
<p key={time} className="text-[var(--text-primary)] text-sm"> {time}</p>
))}
</div>
</div>
</>
)}
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Cliente desde</label>
<p className="text-[var(--text-primary)]">
{new Date(customer.registration_date).toLocaleDateString('es-ES')}
</p>
</div>
</div>
</Card>
{customer.notes && (
<Card className="p-6 lg:col-span-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Notas del Cliente</h3>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<pre className="text-sm text-[var(--text-secondary)] whitespace-pre-wrap font-sans">
{customer.notes}
</pre>
</div>
</Card>
)}
</div>
)}
{activeTab === 'orders' && showOrderHistory && (
<Card className="p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Historial de Pedidos</h3>
<div className="text-sm text-[var(--text-secondary)]">
Mostrando {paginatedOrders.length} de {orderHistory.length} pedidos
</div>
</div>
<div className="space-y-4">
{paginatedOrders.map(order => (
<div key={order.id} className="border border-[var(--border-primary)] rounded-lg p-4 hover:bg-[var(--bg-secondary)]">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div>
<p className="font-medium text-[var(--text-primary)]">Pedido #{order.id.slice(-8)}</p>
<p className="text-sm text-[var(--text-secondary)]">
{new Date(order.date).toLocaleDateString('es-ES')} - {ChannelLabels[order.channel]}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="font-medium text-[var(--text-primary)]">{order.total.toFixed(2)}</p>
<p className="text-sm text-[var(--text-secondary)]">{order.items_count} artículos</p>
</div>
<Badge color={order.status === OrderStatus.DELIVERED ? 'green' : 'blue'} variant="soft">
{order.status}
</Badge>
</div>
</div>
<div className="mt-3 text-sm text-[var(--text-secondary)]">
<p>Productos: {order.products.join(', ')}</p>
</div>
</div>
))}
</div>
{/* Pagination */}
{orderHistory.length > orderPageSize && (
<div className="flex items-center justify-between mt-6 pt-4 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
disabled={orderPage === 1}
onClick={() => setOrderPage(prev => prev - 1)}
>
Anterior
</Button>
<div className="text-sm text-[var(--text-secondary)]">
Página {orderPage} de {Math.ceil(orderHistory.length / orderPageSize)}
</div>
<Button
variant="outline"
size="sm"
disabled={orderPage >= Math.ceil(orderHistory.length / orderPageSize)}
onClick={() => setOrderPage(prev => prev + 1)}
>
Siguiente
</Button>
</div>
)}
</Card>
)}
{activeTab === 'loyalty' && showLoyaltyProgram && loyaltyProgram && (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Programa de Fidelización</h3>
<Button onClick={() => setShowLoyaltyModal(true)}>
Canjear Puntos
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-info)]">{loyaltyProgram.current_points}</div>
<p className="text-sm text-[var(--text-secondary)]">Puntos Disponibles</p>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[var(--color-success)]">{loyaltyProgram.points_to_next_tier}</div>
<p className="text-sm text-[var(--text-secondary)]">Puntos para Siguiente Nivel</p>
</div>
<div className="text-center">
<Badge color={TierColors[loyaltyProgram.current_tier]} variant="soft" className="text-lg px-4 py-2">
{TierLabels[loyaltyProgram.current_tier]}
</Badge>
<p className="text-sm text-[var(--text-secondary)] mt-1">Nivel Actual</p>
</div>
</div>
<div className="mt-6">
<h4 className="font-medium text-[var(--text-primary)] mb-3">Beneficios Actuales</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{loyaltyProgram.benefits.map((benefit, index) => (
<div key={index} className="flex items-center text-sm text-[var(--text-secondary)]">
<svg className="w-4 h-4 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
{benefit}
</div>
))}
</div>
</div>
</Card>
<Card className="p-6">
<h4 className="font-medium text-[var(--text-primary)] mb-4">Historial de Puntos</h4>
<div className="space-y-3 max-h-96 overflow-y-auto">
{loyaltyProgram.rewards_history.slice(0, 10).map(transaction => (
<div key={transaction.id} className="flex items-center justify-between py-2 border-b border-[var(--border-primary)]">
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">{transaction.description}</p>
<p className="text-xs text-[var(--text-secondary)]">
{new Date(transaction.date).toLocaleDateString('es-ES')}
</p>
</div>
<div className={`text-sm font-medium ${
transaction.type === 'earned' ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'
}`}>
{transaction.type === 'earned' ? '+' : ''}{transaction.points}
</div>
</div>
))}
</div>
</Card>
</div>
)}
{activeTab === 'communication' && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-6">Preferencias de Comunicación</h3>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-[var(--text-primary)] mb-4">Canales de Comunicación</h4>
<div className="space-y-3">
<label className="flex items-center">
<input
type="checkbox"
checked={customer.communication_preferences.email_marketing}
onChange={(e) => {
if (isEditing || allowEditing) {
const updated = {
...customer,
communication_preferences: {
...customer.communication_preferences,
email_marketing: e.target.checked
}
};
setCustomer(updated);
onCustomerUpdate?.(customer.id, { communication_preferences: updated.communication_preferences });
}
}}
className="rounded border-[var(--border-secondary)] text-[var(--color-info)] focus:ring-blue-500"
disabled={!isEditing && !allowEditing}
/>
<span className="ml-2 text-sm text-[var(--text-secondary)]">Marketing por email</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={customer.communication_preferences.sms_notifications}
onChange={(e) => {
if (isEditing || allowEditing) {
const updated = {
...customer,
communication_preferences: {
...customer.communication_preferences,
sms_notifications: e.target.checked
}
};
setCustomer(updated);
onCustomerUpdate?.(customer.id, { communication_preferences: updated.communication_preferences });
}
}}
className="rounded border-[var(--border-secondary)] text-[var(--color-info)] focus:ring-blue-500"
disabled={!isEditing && !allowEditing}
/>
<span className="ml-2 text-sm text-[var(--text-secondary)]">Notificaciones SMS</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={customer.communication_preferences.push_notifications}
onChange={(e) => {
if (isEditing || allowEditing) {
const updated = {
...customer,
communication_preferences: {
...customer.communication_preferences,
push_notifications: e.target.checked
}
};
setCustomer(updated);
onCustomerUpdate?.(customer.id, { communication_preferences: updated.communication_preferences });
}
}}
className="rounded border-[var(--border-secondary)] text-[var(--color-info)] focus:ring-blue-500"
disabled={!isEditing && !allowEditing}
/>
<span className="ml-2 text-sm text-[var(--text-secondary)]">Notificaciones push</span>
</label>
</div>
</div>
<div>
<h4 className="font-medium text-[var(--text-primary)] mb-4">Tipos de Comunicación</h4>
<div className="space-y-3">
<label className="flex items-center">
<input
type="checkbox"
checked={customer.communication_preferences.promotional_offers}
onChange={(e) => {
if (isEditing || allowEditing) {
const updated = {
...customer,
communication_preferences: {
...customer.communication_preferences,
promotional_offers: e.target.checked
}
};
setCustomer(updated);
onCustomerUpdate?.(customer.id, { communication_preferences: updated.communication_preferences });
}
}}
className="rounded border-[var(--border-secondary)] text-[var(--color-info)] focus:ring-blue-500"
disabled={!isEditing && !allowEditing}
/>
<span className="ml-2 text-sm text-[var(--text-secondary)]">Ofertas promocionales</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={customer.communication_preferences.order_updates}
onChange={(e) => {
if (isEditing || allowEditing) {
const updated = {
...customer,
communication_preferences: {
...customer.communication_preferences,
order_updates: e.target.checked
}
};
setCustomer(updated);
onCustomerUpdate?.(customer.id, { communication_preferences: updated.communication_preferences });
}
}}
className="rounded border-[var(--border-secondary)] text-[var(--color-info)] focus:ring-blue-500"
disabled={!isEditing && !allowEditing}
/>
<span className="ml-2 text-sm text-[var(--text-secondary)]">Actualizaciones de pedidos</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
checked={customer.communication_preferences.loyalty_updates}
onChange={(e) => {
if (isEditing || allowEditing) {
const updated = {
...customer,
communication_preferences: {
...customer.communication_preferences,
loyalty_updates: e.target.checked
}
};
setCustomer(updated);
onCustomerUpdate?.(customer.id, { communication_preferences: updated.communication_preferences });
}
}}
className="rounded border-[var(--border-secondary)] text-[var(--color-info)] focus:ring-blue-500"
disabled={!isEditing && !allowEditing}
/>
<span className="ml-2 text-sm text-[var(--text-secondary)]">Programa de fidelización</span>
</label>
</div>
</div>
</div>
{paymentMethods.length > 0 && (
<div>
<h4 className="font-medium text-[var(--text-primary)] mb-4">Métodos de Pago</h4>
<div className="space-y-3">
{paymentMethods.map(method => (
<div key={method.id} className="flex items-center justify-between p-3 border border-[var(--border-primary)] rounded-lg">
<div className="flex items-center">
<div className="p-2 bg-[var(--bg-tertiary)] rounded-lg mr-3">
<svg className="w-4 h-4 text-[var(--text-secondary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-[var(--text-primary)]">
{method.brand} {method.last_four}
</p>
{method.expires_at && (
<p className="text-xs text-[var(--text-secondary)]">
Expira {new Date(method.expires_at).toLocaleDateString('es-ES')}
</p>
)}
</div>
</div>
{method.is_default && (
<Badge variant="soft" color="blue">Por defecto</Badge>
)}
</div>
))}
</div>
</div>
)}
</div>
</Card>
)}
{/* Add Note Modal */}
<Modal
isOpen={showAddNote}
onClose={() => setShowAddNote(false)}
title="Agregar Nota"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Nueva nota para {customer.name}
</label>
<textarea
value={newNote}
onChange={(e) => setNewNote(e.target.value)}
rows={4}
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="Escribe aquí la nota del cliente..."
/>
</div>
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={() => setShowAddNote(false)}>
Cancelar
</Button>
<Button onClick={handleAddNote} disabled={!newNote.trim()}>
Guardar Nota
</Button>
</div>
</div>
</Modal>
{/* Loyalty Redemption Modal */}
<Modal
isOpen={showLoyaltyModal}
onClose={() => setShowLoyaltyModal(false)}
title="Canjear Puntos"
>
{loyaltyProgram && (
<div className="space-y-4">
<div className="text-center py-4">
<div className="text-2xl font-bold text-[var(--color-info)]">{loyaltyProgram.current_points}</div>
<p className="text-[var(--text-secondary)]">Puntos disponibles</p>
</div>
<div className="space-y-3">
<div className="border border-[var(--border-primary)] rounded-lg p-4 hover:bg-[var(--bg-secondary)] cursor-pointer">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-[var(--text-primary)]">Producto gratis (panadería)</p>
<p className="text-sm text-[var(--text-secondary)]">Válido para productos hasta 5</p>
</div>
<div className="text-right">
<Button
size="sm"
onClick={() => handleRedeemPoints('free-bakery-item', 500)}
disabled={loyaltyProgram.current_points < 500}
>
500 pts
</Button>
</div>
</div>
</div>
<div className="border border-[var(--border-primary)] rounded-lg p-4 hover:bg-[var(--bg-secondary)] cursor-pointer">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-[var(--text-primary)]">Descuento 15%</p>
<p className="text-sm text-[var(--text-secondary)]">En tu próxima compra</p>
</div>
<div className="text-right">
<Button
size="sm"
onClick={() => handleRedeemPoints('15-percent-discount', 750)}
disabled={loyaltyProgram.current_points < 750}
>
750 pts
</Button>
</div>
</div>
</div>
<div className="border border-[var(--border-primary)] rounded-lg p-4 hover:bg-[var(--bg-secondary)] cursor-pointer">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-[var(--text-primary)]">Tarta personalizada</p>
<p className="text-sm text-[var(--text-secondary)]">Decoración especial incluida</p>
</div>
<div className="text-right">
<Button
size="sm"
onClick={() => handleRedeemPoints('custom-cake', 1200)}
disabled={loyaltyProgram.current_points < 1200}
>
1200 pts
</Button>
</div>
</div>
</div>
</div>
<div className="flex justify-end pt-4">
<Button variant="outline" onClick={() => setShowLoyaltyModal(false)}>
Cerrar
</Button>
</div>
</div>
)}
</Modal>
</div>
);
};
export default CustomerInfo;