1272 lines
50 KiB
TypeScript
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; |