315 lines
10 KiB
TypeScript
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; |