Files
bakery-ia/frontend/src/components/sales/SalesDataCard.tsx
2025-08-13 17:39:35 +02:00

315 lines
10 KiB
TypeScript

import React, { useState } from 'react';
import {
Calendar,
DollarSign,
Package,
TrendingUp,
TrendingDown,
Eye,
Edit3,
MoreHorizontal,
MapPin,
ShoppingCart,
Star,
AlertTriangle,
CheckCircle,
Clock
} from 'lucide-react';
import { SalesData } from '../../api/types';
import Card from '../ui/Card';
import Button from '../ui/Button';
interface SalesDataCardProps {
salesData: SalesData;
compact?: boolean;
showActions?: boolean;
inventoryProduct?: {
id: string;
name: string;
category: string;
};
onEdit?: (salesData: SalesData) => void;
onDelete?: (salesData: SalesData) => void;
onViewDetails?: (salesData: SalesData) => void;
}
const SalesDataCard: React.FC<SalesDataCardProps> = ({
salesData,
compact = false,
showActions = true,
inventoryProduct,
onEdit,
onDelete,
onViewDetails
}) => {
const [showMenu, setShowMenu] = useState(false);
// Format currency
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount);
};
// Format date
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('es-ES', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
};
// Format time
const formatTime = (dateString: string) => {
return new Date(dateString).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
});
};
// Get sales channel icon and label
const getSalesChannelInfo = () => {
switch (salesData.sales_channel) {
case 'online':
return { icon: ShoppingCart, label: 'Online', color: 'text-blue-600' };
case 'delivery':
return { icon: MapPin, label: 'Delivery', color: 'text-green-600' };
case 'in_store':
default:
return { icon: Package, label: 'Tienda', color: 'text-purple-600' };
}
};
// Get validation status
const getValidationStatus = () => {
if (salesData.is_validated) {
return { icon: CheckCircle, label: 'Validado', color: 'text-green-600', bg: 'bg-green-50' };
}
return { icon: Clock, label: 'Pendiente', color: 'text-yellow-600', bg: 'bg-yellow-50' };
};
// Calculate profit margin
const profitMargin = salesData.cost_of_goods
? ((salesData.revenue - salesData.cost_of_goods) / salesData.revenue * 100)
: null;
const channelInfo = getSalesChannelInfo();
const validationStatus = getValidationStatus();
if (compact) {
return (
<Card className="p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<Package className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="font-medium text-gray-900">
{inventoryProduct?.name || `Producto ${salesData.inventory_product_id.slice(0, 8)}...`}
</p>
<div className="flex items-center space-x-2 text-sm text-gray-500">
<span>{salesData.quantity_sold} unidades</span>
<span></span>
<span>{formatDate(salesData.date)}</span>
</div>
</div>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">
{formatCurrency(salesData.revenue)}
</p>
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs ${channelInfo.color} bg-gray-50`}>
<channelInfo.icon className="w-3 h-3 mr-1" />
{channelInfo.label}
</div>
</div>
</div>
</Card>
);
}
return (
<Card className="p-6 hover:shadow-md transition-shadow">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<Package className="w-6 h-6 text-blue-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">
{inventoryProduct?.name || `Producto ${salesData.inventory_product_id.slice(0, 8)}...`}
</h3>
<div className="flex items-center space-x-2 text-sm text-gray-500">
{inventoryProduct?.category && (
<>
<span className="capitalize">{inventoryProduct.category}</span>
<span></span>
</>
)}
<span>ID: {salesData.id.slice(0, 8)}...</span>
</div>
</div>
</div>
{showActions && (
<div className="relative">
<button
onClick={() => setShowMenu(!showMenu)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<MoreHorizontal className="w-4 h-4" />
</button>
{showMenu && (
<div className="absolute right-0 top-full mt-1 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-10">
<div className="py-1">
{onViewDetails && (
<button
onClick={() => {
onViewDetails(salesData);
setShowMenu(false);
}}
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 w-full text-left"
>
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</button>
)}
{onEdit && (
<button
onClick={() => {
onEdit(salesData);
setShowMenu(false);
}}
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 w-full text-left"
>
<Edit3 className="w-4 h-4 mr-2" />
Editar
</button>
)}
</div>
</div>
)}
</div>
)}
</div>
{/* Sales Metrics */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div className="text-center">
<div className="text-xl font-bold text-gray-900">{salesData.quantity_sold}</div>
<div className="text-xs text-gray-600">Cantidad Vendida</div>
</div>
<div className="text-center">
<div className="text-xl font-bold text-green-600">
{formatCurrency(salesData.revenue)}
</div>
<div className="text-xs text-gray-600">Ingresos</div>
</div>
{salesData.unit_price && (
<div className="text-center">
<div className="text-xl font-bold text-blue-600">
{formatCurrency(salesData.unit_price)}
</div>
<div className="text-xs text-gray-600">Precio Unitario</div>
</div>
)}
{profitMargin !== null && (
<div className="text-center">
<div className={`text-xl font-bold ${profitMargin > 0 ? 'text-green-600' : 'text-red-600'}`}>
{profitMargin.toFixed(1)}%
</div>
<div className="text-xs text-gray-600">Margen</div>
</div>
)}
</div>
{/* Details Row */}
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-600 mb-4">
<div className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
<span>{formatDate(salesData.date)} {formatTime(salesData.date)}</span>
</div>
<div className={`flex items-center ${channelInfo.color}`}>
<channelInfo.icon className="w-4 h-4 mr-1" />
<span>{channelInfo.label}</span>
</div>
{salesData.location_id && (
<div className="flex items-center">
<MapPin className="w-4 h-4 mr-1" />
<span>Local {salesData.location_id}</span>
</div>
)}
<div className={`flex items-center px-2 py-1 rounded-full text-xs ${validationStatus.bg} ${validationStatus.color}`}>
<validationStatus.icon className="w-3 h-3 mr-1" />
{validationStatus.label}
</div>
</div>
{/* Additional Info */}
<div className="border-t pt-3">
<div className="flex flex-wrap items-center justify-between text-xs text-gray-500">
<div className="flex items-center space-x-4">
<span>Origen: {salesData.source}</span>
{salesData.discount_applied && salesData.discount_applied > 0 && (
<span>Descuento: {salesData.discount_applied}%</span>
)}
</div>
{salesData.weather_condition && (
<div className="flex items-center">
<span className="mr-1">
{salesData.weather_condition.includes('rain') ? '🌧️' :
salesData.weather_condition.includes('sun') ? '☀️' :
salesData.weather_condition.includes('cloud') ? '☁️' : '🌤️'}
</span>
<span className="capitalize">{salesData.weather_condition}</span>
</div>
)}
</div>
</div>
{/* Actions */}
{showActions && (
<div className="flex items-center space-x-3 mt-4 pt-3 border-t">
{onViewDetails && (
<Button
variant="outline"
size="sm"
onClick={() => onViewDetails(salesData)}
>
<Eye className="w-4 h-4 mr-2" />
Ver Detalles
</Button>
)}
{onEdit && (
<Button
variant="outline"
size="sm"
onClick={() => onEdit(salesData)}
>
<Edit3 className="w-4 h-4 mr-2" />
Editar
</Button>
)}
</div>
)}
</Card>
);
};
export default SalesDataCard;