ADD new frontend
This commit is contained in:
6
frontend/src/pages/app/operations/index.ts
Normal file
6
frontend/src/pages/app/operations/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './inventory';
|
||||
export * from './production';
|
||||
export * from './recipes';
|
||||
export * from './procurement';
|
||||
export * from './orders';
|
||||
export * from './pos';
|
||||
229
frontend/src/pages/app/operations/inventory/InventoryPage.tsx
Normal file
229
frontend/src/pages/app/operations/inventory/InventoryPage.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { InventoryTable, InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
|
||||
|
||||
const InventoryPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [filterCategory, setFilterCategory] = useState('all');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
|
||||
const mockInventoryItems = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Harina de Trigo',
|
||||
category: 'Harinas',
|
||||
currentStock: 45,
|
||||
minStock: 20,
|
||||
maxStock: 100,
|
||||
unit: 'kg',
|
||||
cost: 1.20,
|
||||
supplier: 'Molinos del Sur',
|
||||
lastRestocked: '2024-01-20',
|
||||
expirationDate: '2024-06-30',
|
||||
status: 'normal',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Levadura Fresca',
|
||||
category: 'Levaduras',
|
||||
currentStock: 8,
|
||||
minStock: 10,
|
||||
maxStock: 25,
|
||||
unit: 'kg',
|
||||
cost: 8.50,
|
||||
supplier: 'Levaduras SA',
|
||||
lastRestocked: '2024-01-25',
|
||||
expirationDate: '2024-02-15',
|
||||
status: 'low',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Mantequilla',
|
||||
category: 'Lácteos',
|
||||
currentStock: 15,
|
||||
minStock: 5,
|
||||
maxStock: 30,
|
||||
unit: 'kg',
|
||||
cost: 5.80,
|
||||
supplier: 'Lácteos Frescos',
|
||||
lastRestocked: '2024-01-24',
|
||||
expirationDate: '2024-02-10',
|
||||
status: 'normal',
|
||||
},
|
||||
];
|
||||
|
||||
const lowStockItems = mockInventoryItems.filter(item => item.status === 'low');
|
||||
|
||||
const stats = {
|
||||
totalItems: mockInventoryItems.length,
|
||||
lowStockItems: lowStockItems.length,
|
||||
totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0),
|
||||
needsReorder: lowStockItems.length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Inventario"
|
||||
description="Controla el stock de ingredientes y materias primas"
|
||||
action={
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Artículo
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Artículos</p>
|
||||
<p className="text-3xl font-bold text-[var(--text-primary)]">{stats.totalItems}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-[var(--color-info)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4-8-4m16 0v10l-8 4-8-4V7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Stock Bajo</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-error)]">{stats.lowStockItems}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-error)]/10 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="h-6 w-6 text-[var(--color-error)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Valor Total</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">€{stats.totalValue.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-[var(--color-success)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Necesita Reorden</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{stats.needsReorder}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-[var(--color-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Low Stock Alert */}
|
||||
{lowStockItems.length > 0 && (
|
||||
<LowStockAlert items={lowStockItems} />
|
||||
)}
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar artículos..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={filterCategory}
|
||||
onChange={(e) => setFilterCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="all">Todas las categorías</option>
|
||||
<option value="Harinas">Harinas</option>
|
||||
<option value="Levaduras">Levaduras</option>
|
||||
<option value="Lácteos">Lácteos</option>
|
||||
<option value="Grasas">Grasas</option>
|
||||
<option value="Azúcares">Azúcares</option>
|
||||
<option value="Especias">Especias</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
<option value="all">Todos los estados</option>
|
||||
<option value="normal">Stock normal</option>
|
||||
<option value="low">Stock bajo</option>
|
||||
<option value="out">Sin stock</option>
|
||||
<option value="expired">Caducado</option>
|
||||
</select>
|
||||
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Más filtros
|
||||
</Button>
|
||||
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Inventory Table */}
|
||||
<Card>
|
||||
<InventoryTable
|
||||
data={mockInventoryItems}
|
||||
onEdit={(item) => {
|
||||
setSelectedItem(item);
|
||||
setShowForm(true);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Inventory Form Modal */}
|
||||
{showForm && (
|
||||
<InventoryForm
|
||||
item={selectedItem}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedItem(null);
|
||||
}}
|
||||
onSave={(item) => {
|
||||
// Handle save logic
|
||||
console.log('Saving item:', item);
|
||||
setShowForm(false);
|
||||
setSelectedItem(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InventoryPage;
|
||||
@@ -0,0 +1,232 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Download, AlertTriangle } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { InventoryTable, InventoryForm, LowStockAlert } from '../../../../components/domain/inventory';
|
||||
|
||||
const InventoryPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [filterCategory, setFilterCategory] = useState('all');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
|
||||
const mockInventoryItems = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Harina de Trigo',
|
||||
category: 'Harinas',
|
||||
currentStock: 45,
|
||||
minStock: 20,
|
||||
maxStock: 100,
|
||||
unit: 'kg',
|
||||
cost: 1.20,
|
||||
supplier: 'Molinos del Sur',
|
||||
lastRestocked: '2024-01-20',
|
||||
expirationDate: '2024-06-30',
|
||||
status: 'normal',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Levadura Fresca',
|
||||
category: 'Levaduras',
|
||||
currentStock: 8,
|
||||
minStock: 10,
|
||||
maxStock: 25,
|
||||
unit: 'kg',
|
||||
cost: 8.50,
|
||||
supplier: 'Levaduras SA',
|
||||
lastRestocked: '2024-01-25',
|
||||
expirationDate: '2024-02-15',
|
||||
status: 'low',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Mantequilla',
|
||||
category: 'Lácteos',
|
||||
currentStock: 15,
|
||||
minStock: 5,
|
||||
maxStock: 30,
|
||||
unit: 'kg',
|
||||
cost: 5.80,
|
||||
supplier: 'Lácteos Frescos',
|
||||
lastRestocked: '2024-01-24',
|
||||
expirationDate: '2024-02-10',
|
||||
status: 'normal',
|
||||
},
|
||||
];
|
||||
|
||||
const lowStockItems = mockInventoryItems.filter(item => item.status === 'low');
|
||||
|
||||
const stats = {
|
||||
totalItems: mockInventoryItems.length,
|
||||
lowStockItems: lowStockItems.length,
|
||||
totalValue: mockInventoryItems.reduce((sum, item) => sum + (item.currentStock * item.cost), 0),
|
||||
needsReorder: lowStockItems.length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Inventario"
|
||||
description="Controla el stock de ingredientes y materias primas"
|
||||
action={
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Artículo
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Artículos</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{stats.totalItems}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4-8-4m16 0v10l-8 4-8-4V7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Stock Bajo</p>
|
||||
<p className="text-3xl font-bold text-red-600">{stats.lowStockItems}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Valor Total</p>
|
||||
<p className="text-3xl font-bold text-green-600">€{stats.totalValue.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Necesita Reorden</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{stats.needsReorder}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Low Stock Alert */}
|
||||
{lowStockItems.length > 0 && (
|
||||
<LowStockAlert items={lowStockItems} />
|
||||
)}
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar artículos..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={filterCategory}
|
||||
onChange={(e) => setFilterCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="all">Todas las categorías</option>
|
||||
<option value="Harinas">Harinas</option>
|
||||
<option value="Levaduras">Levaduras</option>
|
||||
<option value="Lácteos">Lácteos</option>
|
||||
<option value="Grasas">Grasas</option>
|
||||
<option value="Azúcares">Azúcares</option>
|
||||
<option value="Especias">Especias</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="all">Todos los estados</option>
|
||||
<option value="normal">Stock normal</option>
|
||||
<option value="low">Stock bajo</option>
|
||||
<option value="out">Sin stock</option>
|
||||
<option value="expired">Caducado</option>
|
||||
</select>
|
||||
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Más filtros
|
||||
</Button>
|
||||
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Inventory Table */}
|
||||
<Card>
|
||||
<InventoryTable
|
||||
items={mockInventoryItems}
|
||||
searchTerm={searchTerm}
|
||||
filterCategory={filterCategory}
|
||||
filterStatus={filterStatus}
|
||||
onEdit={(item) => {
|
||||
setSelectedItem(item);
|
||||
setShowForm(true);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Inventory Form Modal */}
|
||||
{showForm && (
|
||||
<InventoryForm
|
||||
item={selectedItem}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedItem(null);
|
||||
}}
|
||||
onSave={(item) => {
|
||||
// Handle save logic
|
||||
console.log('Saving item:', item);
|
||||
setShowForm(false);
|
||||
setSelectedItem(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InventoryPage;
|
||||
1
frontend/src/pages/app/operations/inventory/index.ts
Normal file
1
frontend/src/pages/app/operations/inventory/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as InventoryPage } from './InventoryPage';
|
||||
405
frontend/src/pages/app/operations/orders/OrdersPage.tsx
Normal file
405
frontend/src/pages/app/operations/orders/OrdersPage.tsx
Normal file
@@ -0,0 +1,405 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Download, Calendar, Clock, User, Package } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { OrdersTable, OrderForm } from '../../../../components/domain/sales';
|
||||
|
||||
const OrdersPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||
|
||||
const mockOrders = [
|
||||
{
|
||||
id: 'ORD-2024-001',
|
||||
customerName: 'María García',
|
||||
customerEmail: 'maria@email.com',
|
||||
customerPhone: '+34 600 123 456',
|
||||
status: 'pending',
|
||||
orderDate: '2024-01-26T09:30:00Z',
|
||||
deliveryDate: '2024-01-26T16:00:00Z',
|
||||
items: [
|
||||
{ id: '1', name: 'Pan de Molde Integral', quantity: 2, price: 4.50, total: 9.00 },
|
||||
{ id: '2', name: 'Croissants de Mantequilla', quantity: 6, price: 1.50, total: 9.00 },
|
||||
],
|
||||
subtotal: 18.00,
|
||||
tax: 1.89,
|
||||
discount: 0,
|
||||
total: 19.89,
|
||||
paymentMethod: 'card',
|
||||
paymentStatus: 'pending',
|
||||
deliveryMethod: 'pickup',
|
||||
notes: 'Sin gluten por favor en el pan',
|
||||
priority: 'normal',
|
||||
},
|
||||
{
|
||||
id: 'ORD-2024-002',
|
||||
customerName: 'Juan Pérez',
|
||||
customerEmail: 'juan@email.com',
|
||||
customerPhone: '+34 600 654 321',
|
||||
status: 'completed',
|
||||
orderDate: '2024-01-25T14:15:00Z',
|
||||
deliveryDate: '2024-01-25T18:30:00Z',
|
||||
items: [
|
||||
{ id: '3', name: 'Tarta de Chocolate', quantity: 1, price: 25.00, total: 25.00 },
|
||||
{ id: '4', name: 'Magdalenas', quantity: 12, price: 0.75, total: 9.00 },
|
||||
],
|
||||
subtotal: 34.00,
|
||||
tax: 3.57,
|
||||
discount: 2.00,
|
||||
total: 35.57,
|
||||
paymentMethod: 'cash',
|
||||
paymentStatus: 'paid',
|
||||
deliveryMethod: 'delivery',
|
||||
notes: 'Cumpleaños - decoración especial',
|
||||
priority: 'high',
|
||||
},
|
||||
{
|
||||
id: 'ORD-2024-003',
|
||||
customerName: 'Ana Martínez',
|
||||
customerEmail: 'ana@email.com',
|
||||
customerPhone: '+34 600 987 654',
|
||||
status: 'in_progress',
|
||||
orderDate: '2024-01-26T07:45:00Z',
|
||||
deliveryDate: '2024-01-26T12:00:00Z',
|
||||
items: [
|
||||
{ id: '5', name: 'Baguettes Francesas', quantity: 4, price: 2.80, total: 11.20 },
|
||||
{ id: '6', name: 'Empanadas', quantity: 8, price: 2.50, total: 20.00 },
|
||||
],
|
||||
subtotal: 31.20,
|
||||
tax: 3.28,
|
||||
discount: 0,
|
||||
total: 34.48,
|
||||
paymentMethod: 'transfer',
|
||||
paymentStatus: 'paid',
|
||||
deliveryMethod: 'pickup',
|
||||
notes: '',
|
||||
priority: 'normal',
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
in_progress: { color: 'blue', text: 'En Proceso' },
|
||||
ready: { color: 'green', text: 'Listo' },
|
||||
completed: { color: 'green', text: 'Completado' },
|
||||
cancelled: { color: 'red', text: 'Cancelado' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority: string) => {
|
||||
const priorityConfig = {
|
||||
low: { color: 'gray', text: 'Baja' },
|
||||
normal: { color: 'blue', text: 'Normal' },
|
||||
high: { color: 'orange', text: 'Alta' },
|
||||
urgent: { color: 'red', text: 'Urgente' },
|
||||
};
|
||||
|
||||
const config = priorityConfig[priority as keyof typeof priorityConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || priority}</Badge>;
|
||||
};
|
||||
|
||||
const getPaymentStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
paid: { color: 'green', text: 'Pagado' },
|
||||
failed: { color: 'red', text: 'Fallido' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const filteredOrders = mockOrders.filter(order => {
|
||||
const matchesSearch = order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
order.customerEmail.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesTab = activeTab === 'all' || order.status === activeTab;
|
||||
|
||||
return matchesSearch && matchesTab;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: mockOrders.length,
|
||||
pending: mockOrders.filter(o => o.status === 'pending').length,
|
||||
inProgress: mockOrders.filter(o => o.status === 'in_progress').length,
|
||||
completed: mockOrders.filter(o => o.status === 'completed').length,
|
||||
totalRevenue: mockOrders.reduce((sum, order) => sum + order.total, 0),
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'all', label: 'Todos', count: stats.total },
|
||||
{ id: 'pending', label: 'Pendientes', count: stats.pending },
|
||||
{ id: 'in_progress', label: 'En Proceso', count: stats.inProgress },
|
||||
{ id: 'ready', label: 'Listos', count: 0 },
|
||||
{ id: 'completed', label: 'Completados', count: stats.completed },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Pedidos"
|
||||
description="Administra y controla todos los pedidos de tu panadería"
|
||||
action={
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Pedido
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 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 Pedidos</p>
|
||||
<p className="text-2xl font-bold text-[var(--text-primary)]">{stats.total}</p>
|
||||
</div>
|
||||
<Package className="h-8 w-8 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Pendientes</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">{stats.pending}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">En Proceso</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{stats.inProgress}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-[var(--color-info)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</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)]">Completados</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{stats.completed}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-[var(--color-success)]" 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>
|
||||
</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)]">Ingresos</p>
|
||||
<p className="text-2xl font-bold text-purple-600">€{stats.totalRevenue.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-[var(--border-primary)]">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${
|
||||
activeTab === tab.id
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.count > 0 && (
|
||||
<span className="ml-2 bg-[var(--bg-tertiary)] text-[var(--text-primary)] py-0.5 px-2.5 rounded-full text-xs">
|
||||
{tab.count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar pedidos por cliente, ID o email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
Fecha
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Orders Table */}
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-[var(--bg-secondary)]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Pedido
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Cliente
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Prioridad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Fecha Pedido
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Entrega
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Total
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Pago
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-[var(--bg-secondary)]">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">{order.id}</div>
|
||||
<div className="text-xs text-[var(--text-tertiary)]">{order.deliveryMethod === 'delivery' ? 'Entrega' : 'Recogida'}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<User className="h-4 w-4 text-[var(--text-tertiary)] mr-2" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">{order.customerName}</div>
|
||||
<div className="text-xs text-[var(--text-tertiary)]">{order.customerEmail}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(order.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPriorityBadge(order.priority)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{new Date(order.orderDate).toLocaleDateString('es-ES')}
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
{new Date(order.orderDate).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
{new Date(order.deliveryDate).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">€{order.total.toFixed(2)}</div>
|
||||
<div className="text-xs text-[var(--text-tertiary)]">{order.items.length} artículos</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPaymentStatusBadge(order.paymentStatus)}
|
||||
<div className="text-xs text-[var(--text-tertiary)] capitalize">{order.paymentMethod}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedOrder(order);
|
||||
setShowForm(true);
|
||||
}}
|
||||
>
|
||||
Ver
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Editar
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Order Form Modal */}
|
||||
{showForm && (
|
||||
<OrderForm
|
||||
order={selectedOrder}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedOrder(null);
|
||||
}}
|
||||
onSave={(order) => {
|
||||
// Handle save logic
|
||||
console.log('Saving order:', order);
|
||||
setShowForm(false);
|
||||
setSelectedOrder(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersPage;
|
||||
405
frontend/src/pages/app/operations/orders/OrdersPage.tsx.backup
Normal file
405
frontend/src/pages/app/operations/orders/OrdersPage.tsx.backup
Normal file
@@ -0,0 +1,405 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Download, Calendar, Clock, User, Package } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { OrdersTable, OrderForm } from '../../../../components/domain/sales';
|
||||
|
||||
const OrdersPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||
|
||||
const mockOrders = [
|
||||
{
|
||||
id: 'ORD-2024-001',
|
||||
customerName: 'María García',
|
||||
customerEmail: 'maria@email.com',
|
||||
customerPhone: '+34 600 123 456',
|
||||
status: 'pending',
|
||||
orderDate: '2024-01-26T09:30:00Z',
|
||||
deliveryDate: '2024-01-26T16:00:00Z',
|
||||
items: [
|
||||
{ id: '1', name: 'Pan de Molde Integral', quantity: 2, price: 4.50, total: 9.00 },
|
||||
{ id: '2', name: 'Croissants de Mantequilla', quantity: 6, price: 1.50, total: 9.00 },
|
||||
],
|
||||
subtotal: 18.00,
|
||||
tax: 1.89,
|
||||
discount: 0,
|
||||
total: 19.89,
|
||||
paymentMethod: 'card',
|
||||
paymentStatus: 'pending',
|
||||
deliveryMethod: 'pickup',
|
||||
notes: 'Sin gluten por favor en el pan',
|
||||
priority: 'normal',
|
||||
},
|
||||
{
|
||||
id: 'ORD-2024-002',
|
||||
customerName: 'Juan Pérez',
|
||||
customerEmail: 'juan@email.com',
|
||||
customerPhone: '+34 600 654 321',
|
||||
status: 'completed',
|
||||
orderDate: '2024-01-25T14:15:00Z',
|
||||
deliveryDate: '2024-01-25T18:30:00Z',
|
||||
items: [
|
||||
{ id: '3', name: 'Tarta de Chocolate', quantity: 1, price: 25.00, total: 25.00 },
|
||||
{ id: '4', name: 'Magdalenas', quantity: 12, price: 0.75, total: 9.00 },
|
||||
],
|
||||
subtotal: 34.00,
|
||||
tax: 3.57,
|
||||
discount: 2.00,
|
||||
total: 35.57,
|
||||
paymentMethod: 'cash',
|
||||
paymentStatus: 'paid',
|
||||
deliveryMethod: 'delivery',
|
||||
notes: 'Cumpleaños - decoración especial',
|
||||
priority: 'high',
|
||||
},
|
||||
{
|
||||
id: 'ORD-2024-003',
|
||||
customerName: 'Ana Martínez',
|
||||
customerEmail: 'ana@email.com',
|
||||
customerPhone: '+34 600 987 654',
|
||||
status: 'in_progress',
|
||||
orderDate: '2024-01-26T07:45:00Z',
|
||||
deliveryDate: '2024-01-26T12:00:00Z',
|
||||
items: [
|
||||
{ id: '5', name: 'Baguettes Francesas', quantity: 4, price: 2.80, total: 11.20 },
|
||||
{ id: '6', name: 'Empanadas', quantity: 8, price: 2.50, total: 20.00 },
|
||||
],
|
||||
subtotal: 31.20,
|
||||
tax: 3.28,
|
||||
discount: 0,
|
||||
total: 34.48,
|
||||
paymentMethod: 'transfer',
|
||||
paymentStatus: 'paid',
|
||||
deliveryMethod: 'pickup',
|
||||
notes: '',
|
||||
priority: 'normal',
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
in_progress: { color: 'blue', text: 'En Proceso' },
|
||||
ready: { color: 'green', text: 'Listo' },
|
||||
completed: { color: 'green', text: 'Completado' },
|
||||
cancelled: { color: 'red', text: 'Cancelado' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority: string) => {
|
||||
const priorityConfig = {
|
||||
low: { color: 'gray', text: 'Baja' },
|
||||
normal: { color: 'blue', text: 'Normal' },
|
||||
high: { color: 'orange', text: 'Alta' },
|
||||
urgent: { color: 'red', text: 'Urgente' },
|
||||
};
|
||||
|
||||
const config = priorityConfig[priority as keyof typeof priorityConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || priority}</Badge>;
|
||||
};
|
||||
|
||||
const getPaymentStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
paid: { color: 'green', text: 'Pagado' },
|
||||
failed: { color: 'red', text: 'Fallido' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const filteredOrders = mockOrders.filter(order => {
|
||||
const matchesSearch = order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
order.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
order.customerEmail.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesTab = activeTab === 'all' || order.status === activeTab;
|
||||
|
||||
return matchesSearch && matchesTab;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: mockOrders.length,
|
||||
pending: mockOrders.filter(o => o.status === 'pending').length,
|
||||
inProgress: mockOrders.filter(o => o.status === 'in_progress').length,
|
||||
completed: mockOrders.filter(o => o.status === 'completed').length,
|
||||
totalRevenue: mockOrders.reduce((sum, order) => sum + order.total, 0),
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'all', label: 'Todos', count: stats.total },
|
||||
{ id: 'pending', label: 'Pendientes', count: stats.pending },
|
||||
{ id: 'in_progress', label: 'En Proceso', count: stats.inProgress },
|
||||
{ id: 'ready', label: 'Listos', count: 0 },
|
||||
{ id: 'completed', label: 'Completados', count: stats.completed },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Pedidos"
|
||||
description="Administra y controla todos los pedidos de tu panadería"
|
||||
action={
|
||||
<Button onClick={() => setShowForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Pedido
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Pedidos</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
|
||||
</div>
|
||||
<Package className="h-8 w-8 text-blue-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Pendientes</p>
|
||||
<p className="text-2xl font-bold text-orange-600">{stats.pending}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-orange-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">En Proceso</p>
|
||||
<p className="text-2xl font-bold text-blue-600">{stats.inProgress}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Completados</p>
|
||||
<p className="text-2xl font-bold text-green-600">{stats.completed}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-green-600" 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>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Ingresos</p>
|
||||
<p className="text-2xl font-bold text-purple-600">€{stats.totalRevenue.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${
|
||||
activeTab === tab.id
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.count > 0 && (
|
||||
<span className="ml-2 bg-gray-100 text-gray-900 py-0.5 px-2.5 rounded-full text-xs">
|
||||
{tab.count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar pedidos por cliente, ID o email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
Fecha
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Orders Table */}
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Pedido
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Cliente
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Prioridad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Fecha Pedido
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Entrega
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Total
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Pago
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">{order.id}</div>
|
||||
<div className="text-xs text-gray-500">{order.deliveryMethod === 'delivery' ? 'Entrega' : 'Recogida'}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<User className="h-4 w-4 text-gray-400 mr-2" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{order.customerName}</div>
|
||||
<div className="text-xs text-gray-500">{order.customerEmail}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(order.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPriorityBadge(order.priority)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(order.orderDate).toLocaleDateString('es-ES')}
|
||||
<div className="text-xs text-gray-500">
|
||||
{new Date(order.orderDate).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
|
||||
<div className="text-xs text-gray-500">
|
||||
{new Date(order.deliveryDate).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">€{order.total.toFixed(2)}</div>
|
||||
<div className="text-xs text-gray-500">{order.items.length} artículos</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPaymentStatusBadge(order.paymentStatus)}
|
||||
<div className="text-xs text-gray-500 capitalize">{order.paymentMethod}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedOrder(order);
|
||||
setShowForm(true);
|
||||
}}
|
||||
>
|
||||
Ver
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Editar
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Order Form Modal */}
|
||||
{showForm && (
|
||||
<OrderForm
|
||||
order={selectedOrder}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedOrder(null);
|
||||
}}
|
||||
onSave={(order) => {
|
||||
// Handle save logic
|
||||
console.log('Saving order:', order);
|
||||
setShowForm(false);
|
||||
setSelectedOrder(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersPage;
|
||||
1
frontend/src/pages/app/operations/orders/index.ts
Normal file
1
frontend/src/pages/app/operations/orders/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as OrdersPage } from './OrdersPage';
|
||||
368
frontend/src/pages/app/operations/pos/POSPage.tsx
Normal file
368
frontend/src/pages/app/operations/pos/POSPage.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const POSPage: React.FC = () => {
|
||||
const [cart, setCart] = useState<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
category: string;
|
||||
}>>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [customerInfo, setCustomerInfo] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
});
|
||||
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
|
||||
const [cashReceived, setCashReceived] = useState('');
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Pan de Molde Integral',
|
||||
price: 4.50,
|
||||
category: 'bread',
|
||||
stock: 25,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Croissants de Mantequilla',
|
||||
price: 1.50,
|
||||
category: 'pastry',
|
||||
stock: 32,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Baguette Francesa',
|
||||
price: 2.80,
|
||||
category: 'bread',
|
||||
stock: 18,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Tarta de Chocolate',
|
||||
price: 25.00,
|
||||
category: 'cake',
|
||||
stock: 8,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Magdalenas',
|
||||
price: 0.75,
|
||||
category: 'pastry',
|
||||
stock: 48,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Empanadas',
|
||||
price: 2.50,
|
||||
category: 'other',
|
||||
stock: 24,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', name: 'Todos' },
|
||||
{ id: 'bread', name: 'Panes' },
|
||||
{ id: 'pastry', name: 'Bollería' },
|
||||
{ id: 'cake', name: 'Tartas' },
|
||||
{ id: 'other', name: 'Otros' },
|
||||
];
|
||||
|
||||
const filteredProducts = products.filter(product =>
|
||||
selectedCategory === 'all' || product.category === selectedCategory
|
||||
);
|
||||
|
||||
const addToCart = (product: typeof products[0]) => {
|
||||
setCart(prevCart => {
|
||||
const existingItem = prevCart.find(item => item.id === product.id);
|
||||
if (existingItem) {
|
||||
return prevCart.map(item =>
|
||||
item.id === product.id
|
||||
? { ...item, quantity: item.quantity + 1 }
|
||||
: item
|
||||
);
|
||||
} else {
|
||||
return [...prevCart, {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
quantity: 1,
|
||||
category: product.category,
|
||||
}];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateQuantity = (id: string, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
setCart(prevCart => prevCart.filter(item => item.id !== id));
|
||||
} else {
|
||||
setCart(prevCart =>
|
||||
prevCart.map(item =>
|
||||
item.id === id ? { ...item, quantity } : item
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const clearCart = () => {
|
||||
setCart([]);
|
||||
};
|
||||
|
||||
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const taxRate = 0.21; // 21% IVA
|
||||
const tax = subtotal * taxRate;
|
||||
const total = subtotal + tax;
|
||||
const change = cashReceived ? Math.max(0, parseFloat(cashReceived) - total) : 0;
|
||||
|
||||
const processPayment = () => {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
// Process payment logic here
|
||||
console.log('Processing payment:', {
|
||||
cart,
|
||||
customerInfo,
|
||||
paymentMethod,
|
||||
total,
|
||||
cashReceived: paymentMethod === 'cash' ? parseFloat(cashReceived) : undefined,
|
||||
change: paymentMethod === 'cash' ? change : undefined,
|
||||
});
|
||||
|
||||
// Clear cart after successful payment
|
||||
setCart([]);
|
||||
setCustomerInfo({ name: '', email: '', phone: '' });
|
||||
setCashReceived('');
|
||||
|
||||
alert('Venta procesada exitosamente');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 h-screen flex flex-col">
|
||||
<PageHeader
|
||||
title="Punto de Venta"
|
||||
description="Sistema de ventas integrado"
|
||||
/>
|
||||
|
||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
|
||||
{/* Products Section */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Categories */}
|
||||
<div className="flex space-x-2 overflow-x-auto">
|
||||
{categories.map(category => (
|
||||
<Button
|
||||
key={category.id}
|
||||
variant={selectedCategory === category.id ? 'default' : 'outline'}
|
||||
onClick={() => setSelectedCategory(category.id)}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{category.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{filteredProducts.map(product => (
|
||||
<Card
|
||||
key={product.id}
|
||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
|
||||
onClick={() => addToCart(product)}
|
||||
>
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="w-full h-20 object-cover rounded mb-3"
|
||||
/>
|
||||
<h3 className="font-medium text-sm mb-1 line-clamp-2">{product.name}</h3>
|
||||
<p className="text-lg font-bold text-[var(--color-success)]">€{product.price.toFixed(2)}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Stock: {product.stock}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cart and Checkout Section */}
|
||||
<div className="space-y-6">
|
||||
{/* Cart */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold flex items-center">
|
||||
<ShoppingCart className="w-5 h-5 mr-2" />
|
||||
Carrito ({cart.length})
|
||||
</h3>
|
||||
{cart.length > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={clearCart}>
|
||||
Limpiar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-64 overflow-y-auto">
|
||||
{cart.length === 0 ? (
|
||||
<p className="text-[var(--text-tertiary)] text-center py-8">Carrito vacío</p>
|
||||
) : (
|
||||
cart.map(item => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 bg-[var(--bg-secondary)] rounded">
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium">{item.name}</h4>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">€{item.price.toFixed(2)} c/u</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
updateQuantity(item.id, item.quantity - 1);
|
||||
}}
|
||||
>
|
||||
<Minus className="w-3 h-3" />
|
||||
</Button>
|
||||
<span className="w-8 text-center text-sm">{item.quantity}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
updateQuantity(item.id, item.quantity + 1);
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="ml-4 text-right">
|
||||
<p className="text-sm font-medium">€{(item.price * item.quantity).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cart.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal:</span>
|
||||
<span>€{subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>IVA (21%):</span>
|
||||
<span>€{tax.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-lg font-bold border-t pt-2">
|
||||
<span>Total:</span>
|
||||
<span>€{total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Customer Info */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<User className="w-5 h-5 mr-2" />
|
||||
Cliente (Opcional)
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
placeholder="Nombre"
|
||||
value={customerInfo.name}
|
||||
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
value={customerInfo.email}
|
||||
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Teléfono"
|
||||
value={customerInfo.phone}
|
||||
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Payment */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<Calculator className="w-5 h-5 mr-2" />
|
||||
Método de Pago
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
variant={paymentMethod === 'cash' ? 'default' : 'outline'}
|
||||
onClick={() => setPaymentMethod('cash')}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Banknote className="w-4 h-4 mr-1" />
|
||||
Efectivo
|
||||
</Button>
|
||||
<Button
|
||||
variant={paymentMethod === 'card' ? 'default' : 'outline'}
|
||||
onClick={() => setPaymentMethod('card')}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<CreditCard className="w-4 h-4 mr-1" />
|
||||
Tarjeta
|
||||
</Button>
|
||||
<Button
|
||||
variant={paymentMethod === 'transfer' ? 'default' : 'outline'}
|
||||
onClick={() => setPaymentMethod('transfer')}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
Transferencia
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{paymentMethod === 'cash' && (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="Efectivo recibido"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={cashReceived}
|
||||
onChange={(e) => setCashReceived(e.target.value)}
|
||||
/>
|
||||
{cashReceived && parseFloat(cashReceived) >= total && (
|
||||
<div className="p-2 bg-green-50 rounded text-center">
|
||||
<p className="text-sm text-[var(--color-success)]">
|
||||
Cambio: <span className="font-bold">€{change.toFixed(2)}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={processPayment}
|
||||
disabled={cart.length === 0 || (paymentMethod === 'cash' && (!cashReceived || parseFloat(cashReceived) < total))}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
<Receipt className="w-5 h-5 mr-2" />
|
||||
Procesar Venta - €{total.toFixed(2)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default POSPage;
|
||||
368
frontend/src/pages/app/operations/pos/POSPage.tsx.backup
Normal file
368
frontend/src/pages/app/operations/pos/POSPage.tsx.backup
Normal file
@@ -0,0 +1,368 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const POSPage: React.FC = () => {
|
||||
const [cart, setCart] = useState<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
category: string;
|
||||
}>>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [customerInfo, setCustomerInfo] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
});
|
||||
const [paymentMethod, setPaymentMethod] = useState<'cash' | 'card' | 'transfer'>('cash');
|
||||
const [cashReceived, setCashReceived] = useState('');
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Pan de Molde Integral',
|
||||
price: 4.50,
|
||||
category: 'bread',
|
||||
stock: 25,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Croissants de Mantequilla',
|
||||
price: 1.50,
|
||||
category: 'pastry',
|
||||
stock: 32,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Baguette Francesa',
|
||||
price: 2.80,
|
||||
category: 'bread',
|
||||
stock: 18,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Tarta de Chocolate',
|
||||
price: 25.00,
|
||||
category: 'cake',
|
||||
stock: 8,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Magdalenas',
|
||||
price: 0.75,
|
||||
category: 'pastry',
|
||||
stock: 48,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Empanadas',
|
||||
price: 2.50,
|
||||
category: 'other',
|
||||
stock: 24,
|
||||
image: '/api/placeholder/100/100',
|
||||
},
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', name: 'Todos' },
|
||||
{ id: 'bread', name: 'Panes' },
|
||||
{ id: 'pastry', name: 'Bollería' },
|
||||
{ id: 'cake', name: 'Tartas' },
|
||||
{ id: 'other', name: 'Otros' },
|
||||
];
|
||||
|
||||
const filteredProducts = products.filter(product =>
|
||||
selectedCategory === 'all' || product.category === selectedCategory
|
||||
);
|
||||
|
||||
const addToCart = (product: typeof products[0]) => {
|
||||
setCart(prevCart => {
|
||||
const existingItem = prevCart.find(item => item.id === product.id);
|
||||
if (existingItem) {
|
||||
return prevCart.map(item =>
|
||||
item.id === product.id
|
||||
? { ...item, quantity: item.quantity + 1 }
|
||||
: item
|
||||
);
|
||||
} else {
|
||||
return [...prevCart, {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
quantity: 1,
|
||||
category: product.category,
|
||||
}];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateQuantity = (id: string, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
setCart(prevCart => prevCart.filter(item => item.id !== id));
|
||||
} else {
|
||||
setCart(prevCart =>
|
||||
prevCart.map(item =>
|
||||
item.id === id ? { ...item, quantity } : item
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const clearCart = () => {
|
||||
setCart([]);
|
||||
};
|
||||
|
||||
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const taxRate = 0.21; // 21% IVA
|
||||
const tax = subtotal * taxRate;
|
||||
const total = subtotal + tax;
|
||||
const change = cashReceived ? Math.max(0, parseFloat(cashReceived) - total) : 0;
|
||||
|
||||
const processPayment = () => {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
// Process payment logic here
|
||||
console.log('Processing payment:', {
|
||||
cart,
|
||||
customerInfo,
|
||||
paymentMethod,
|
||||
total,
|
||||
cashReceived: paymentMethod === 'cash' ? parseFloat(cashReceived) : undefined,
|
||||
change: paymentMethod === 'cash' ? change : undefined,
|
||||
});
|
||||
|
||||
// Clear cart after successful payment
|
||||
setCart([]);
|
||||
setCustomerInfo({ name: '', email: '', phone: '' });
|
||||
setCashReceived('');
|
||||
|
||||
alert('Venta procesada exitosamente');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 h-screen flex flex-col">
|
||||
<PageHeader
|
||||
title="Punto de Venta"
|
||||
description="Sistema de ventas integrado"
|
||||
/>
|
||||
|
||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
|
||||
{/* Products Section */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Categories */}
|
||||
<div className="flex space-x-2 overflow-x-auto">
|
||||
{categories.map(category => (
|
||||
<Button
|
||||
key={category.id}
|
||||
variant={selectedCategory === category.id ? 'default' : 'outline'}
|
||||
onClick={() => setSelectedCategory(category.id)}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{category.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{filteredProducts.map(product => (
|
||||
<Card
|
||||
key={product.id}
|
||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
|
||||
onClick={() => addToCart(product)}
|
||||
>
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="w-full h-20 object-cover rounded mb-3"
|
||||
/>
|
||||
<h3 className="font-medium text-sm mb-1 line-clamp-2">{product.name}</h3>
|
||||
<p className="text-lg font-bold text-green-600">€{product.price.toFixed(2)}</p>
|
||||
<p className="text-xs text-gray-500">Stock: {product.stock}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cart and Checkout Section */}
|
||||
<div className="space-y-6">
|
||||
{/* Cart */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold flex items-center">
|
||||
<ShoppingCart className="w-5 h-5 mr-2" />
|
||||
Carrito ({cart.length})
|
||||
</h3>
|
||||
{cart.length > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={clearCart}>
|
||||
Limpiar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-64 overflow-y-auto">
|
||||
{cart.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">Carrito vacío</p>
|
||||
) : (
|
||||
cart.map(item => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium">{item.name}</h4>
|
||||
<p className="text-xs text-gray-500">€{item.price.toFixed(2)} c/u</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
updateQuantity(item.id, item.quantity - 1);
|
||||
}}
|
||||
>
|
||||
<Minus className="w-3 h-3" />
|
||||
</Button>
|
||||
<span className="w-8 text-center text-sm">{item.quantity}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
updateQuantity(item.id, item.quantity + 1);
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="ml-4 text-right">
|
||||
<p className="text-sm font-medium">€{(item.price * item.quantity).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cart.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal:</span>
|
||||
<span>€{subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>IVA (21%):</span>
|
||||
<span>€{tax.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-lg font-bold border-t pt-2">
|
||||
<span>Total:</span>
|
||||
<span>€{total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Customer Info */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<User className="w-5 h-5 mr-2" />
|
||||
Cliente (Opcional)
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
placeholder="Nombre"
|
||||
value={customerInfo.name}
|
||||
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
value={customerInfo.email}
|
||||
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Teléfono"
|
||||
value={customerInfo.phone}
|
||||
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Payment */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<Calculator className="w-5 h-5 mr-2" />
|
||||
Método de Pago
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
variant={paymentMethod === 'cash' ? 'default' : 'outline'}
|
||||
onClick={() => setPaymentMethod('cash')}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Banknote className="w-4 h-4 mr-1" />
|
||||
Efectivo
|
||||
</Button>
|
||||
<Button
|
||||
variant={paymentMethod === 'card' ? 'default' : 'outline'}
|
||||
onClick={() => setPaymentMethod('card')}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<CreditCard className="w-4 h-4 mr-1" />
|
||||
Tarjeta
|
||||
</Button>
|
||||
<Button
|
||||
variant={paymentMethod === 'transfer' ? 'default' : 'outline'}
|
||||
onClick={() => setPaymentMethod('transfer')}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
Transferencia
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{paymentMethod === 'cash' && (
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
placeholder="Efectivo recibido"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={cashReceived}
|
||||
onChange={(e) => setCashReceived(e.target.value)}
|
||||
/>
|
||||
{cashReceived && parseFloat(cashReceived) >= total && (
|
||||
<div className="p-2 bg-green-50 rounded text-center">
|
||||
<p className="text-sm text-green-600">
|
||||
Cambio: <span className="font-bold">€{change.toFixed(2)}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={processPayment}
|
||||
disabled={cart.length === 0 || (paymentMethod === 'cash' && (!cashReceived || parseFloat(cashReceived) < total))}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
<Receipt className="w-5 h-5 mr-2" />
|
||||
Procesar Venta - €{total.toFixed(2)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default POSPage;
|
||||
1
frontend/src/pages/app/operations/pos/index.ts
Normal file
1
frontend/src/pages/app/operations/pos/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as POSPage } from './POSPage';
|
||||
@@ -0,0 +1,449 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const ProcurementPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('orders');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const mockPurchaseOrders = [
|
||||
{
|
||||
id: 'PO-2024-001',
|
||||
supplier: 'Molinos del Sur',
|
||||
status: 'pending',
|
||||
orderDate: '2024-01-25',
|
||||
deliveryDate: '2024-01-28',
|
||||
totalAmount: 1250.00,
|
||||
items: [
|
||||
{ name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 },
|
||||
{ name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 },
|
||||
],
|
||||
paymentStatus: 'pending',
|
||||
notes: 'Entrega en horario de mañana',
|
||||
},
|
||||
{
|
||||
id: 'PO-2024-002',
|
||||
supplier: 'Levaduras SA',
|
||||
status: 'delivered',
|
||||
orderDate: '2024-01-20',
|
||||
deliveryDate: '2024-01-23',
|
||||
totalAmount: 425.50,
|
||||
items: [
|
||||
{ name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 },
|
||||
{ name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 },
|
||||
],
|
||||
paymentStatus: 'paid',
|
||||
notes: '',
|
||||
},
|
||||
{
|
||||
id: 'PO-2024-003',
|
||||
supplier: 'Lácteos Frescos',
|
||||
status: 'in_transit',
|
||||
orderDate: '2024-01-24',
|
||||
deliveryDate: '2024-01-26',
|
||||
totalAmount: 320.75,
|
||||
items: [
|
||||
{ name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 },
|
||||
{ name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 },
|
||||
],
|
||||
paymentStatus: 'pending',
|
||||
notes: 'Producto refrigerado',
|
||||
},
|
||||
];
|
||||
|
||||
const mockSuppliers = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Molinos del Sur',
|
||||
contact: 'Juan Pérez',
|
||||
email: 'juan@molinosdelsur.com',
|
||||
phone: '+34 91 234 5678',
|
||||
category: 'Harinas',
|
||||
rating: 4.8,
|
||||
totalOrders: 24,
|
||||
totalSpent: 15600.00,
|
||||
paymentTerms: '30 días',
|
||||
leadTime: '2-3 días',
|
||||
location: 'Sevilla',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Levaduras SA',
|
||||
contact: 'María González',
|
||||
email: 'maria@levaduras.com',
|
||||
phone: '+34 93 456 7890',
|
||||
category: 'Levaduras',
|
||||
rating: 4.6,
|
||||
totalOrders: 18,
|
||||
totalSpent: 8450.00,
|
||||
paymentTerms: '15 días',
|
||||
leadTime: '1-2 días',
|
||||
location: 'Barcelona',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Lácteos Frescos',
|
||||
contact: 'Carlos Ruiz',
|
||||
email: 'carlos@lacteosfrescos.com',
|
||||
phone: '+34 96 789 0123',
|
||||
category: 'Lácteos',
|
||||
rating: 4.4,
|
||||
totalOrders: 32,
|
||||
totalSpent: 12300.00,
|
||||
paymentTerms: '20 días',
|
||||
leadTime: '1 día',
|
||||
location: 'Valencia',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
approved: { color: 'blue', text: 'Aprobado' },
|
||||
in_transit: { color: 'purple', text: 'En Tránsito' },
|
||||
delivered: { color: 'green', text: 'Entregado' },
|
||||
cancelled: { color: 'red', text: 'Cancelado' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const getPaymentStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
paid: { color: 'green', text: 'Pagado' },
|
||||
overdue: { color: 'red', text: 'Vencido' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const stats = {
|
||||
totalOrders: mockPurchaseOrders.length,
|
||||
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
|
||||
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
|
||||
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Compras"
|
||||
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
|
||||
action={
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Orden de Compra
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Órdenes Totales</p>
|
||||
<p className="text-3xl font-bold text-[var(--text-primary)]">{stats.totalOrders}</p>
|
||||
</div>
|
||||
<ShoppingCart className="h-12 w-12 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Órdenes Pendientes</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-primary)]">{stats.pendingOrders}</p>
|
||||
</div>
|
||||
<Calendar className="h-12 w-12 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Gasto Total</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">€{stats.totalSpent.toLocaleString()}</p>
|
||||
</div>
|
||||
<DollarSign className="h-12 w-12 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Proveedores Activos</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{stats.activeSuppliers}</p>
|
||||
</div>
|
||||
<Truck className="h-12 w-12 text-purple-600" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-[var(--border-primary)]">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('orders')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'orders'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Órdenes de Compra
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('suppliers')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'suppliers'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Proveedores
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'analytics'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Análisis
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
placeholder={`Buscar ${activeTab === 'orders' ? 'órdenes' : 'proveedores'}...`}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'orders' && (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-[var(--bg-secondary)]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Orden
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Proveedor
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Fecha Pedido
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Fecha Entrega
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Monto Total
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Pago
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{mockPurchaseOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-[var(--bg-secondary)]">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">{order.id}</div>
|
||||
{order.notes && (
|
||||
<div className="text-xs text-[var(--text-tertiary)]">{order.notes}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{order.supplier}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(order.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{new Date(order.orderDate).toLocaleDateString('es-ES')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-[var(--text-primary)]">
|
||||
€{order.totalAmount.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPaymentStatusBadge(order.paymentStatus)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">Ver</Button>
|
||||
<Button variant="outline" size="sm">Editar</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'suppliers' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{mockSuppliers.map((supplier) => (
|
||||
<Card key={supplier.id} className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{supplier.name}</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">{supplier.category}</p>
|
||||
</div>
|
||||
<Badge variant="green">Activo</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-[var(--text-secondary)]">Contacto:</span>
|
||||
<span className="font-medium">{supplier.contact}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-[var(--text-secondary)]">Email:</span>
|
||||
<span className="font-medium">{supplier.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-[var(--text-secondary)]">Teléfono:</span>
|
||||
<span className="font-medium">{supplier.phone}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-[var(--text-secondary)]">Ubicación:</span>
|
||||
<span className="font-medium">{supplier.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Valoración</p>
|
||||
<p className="text-sm font-medium flex items-center">
|
||||
<span className="text-yellow-500">★</span>
|
||||
<span className="ml-1">{supplier.rating}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Pedidos</p>
|
||||
<p className="text-sm font-medium">{supplier.totalOrders}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Total Gastado</p>
|
||||
<p className="text-sm font-medium">€{supplier.totalSpent.toLocaleString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Tiempo Entrega</p>
|
||||
<p className="text-sm font-medium">{supplier.leadTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-xs text-[var(--text-secondary)]">Condiciones de Pago</p>
|
||||
<p className="text-sm font-medium">{supplier.paymentTerms}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Ver Detalles
|
||||
</Button>
|
||||
<Button size="sm" className="flex-1">
|
||||
Nuevo Pedido
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'analytics' && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Mes</h3>
|
||||
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-[var(--text-tertiary)]">Gráfico de gastos mensuales</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Top Proveedores</h3>
|
||||
<div className="space-y-3">
|
||||
{mockSuppliers
|
||||
.sort((a, b) => b.totalSpent - a.totalSpent)
|
||||
.slice(0, 5)
|
||||
.map((supplier, index) => (
|
||||
<div key={supplier.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm font-medium text-[var(--text-tertiary)] w-4">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<span className="ml-3 text-sm text-[var(--text-primary)]">{supplier.name}</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
€{supplier.totalSpent.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Gastos por Categoría</h3>
|
||||
<div className="h-64 flex items-center justify-center bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-[var(--text-tertiary)]">Gráfico de gastos por categoría</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcurementPage;
|
||||
@@ -0,0 +1,449 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Download, ShoppingCart, Truck, DollarSign, Calendar } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const ProcurementPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('orders');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const mockPurchaseOrders = [
|
||||
{
|
||||
id: 'PO-2024-001',
|
||||
supplier: 'Molinos del Sur',
|
||||
status: 'pending',
|
||||
orderDate: '2024-01-25',
|
||||
deliveryDate: '2024-01-28',
|
||||
totalAmount: 1250.00,
|
||||
items: [
|
||||
{ name: 'Harina de Trigo', quantity: 50, unit: 'kg', price: 1.20, total: 60.00 },
|
||||
{ name: 'Harina Integral', quantity: 100, unit: 'kg', price: 1.30, total: 130.00 },
|
||||
],
|
||||
paymentStatus: 'pending',
|
||||
notes: 'Entrega en horario de mañana',
|
||||
},
|
||||
{
|
||||
id: 'PO-2024-002',
|
||||
supplier: 'Levaduras SA',
|
||||
status: 'delivered',
|
||||
orderDate: '2024-01-20',
|
||||
deliveryDate: '2024-01-23',
|
||||
totalAmount: 425.50,
|
||||
items: [
|
||||
{ name: 'Levadura Fresca', quantity: 5, unit: 'kg', price: 8.50, total: 42.50 },
|
||||
{ name: 'Mejorante', quantity: 10, unit: 'kg', price: 12.30, total: 123.00 },
|
||||
],
|
||||
paymentStatus: 'paid',
|
||||
notes: '',
|
||||
},
|
||||
{
|
||||
id: 'PO-2024-003',
|
||||
supplier: 'Lácteos Frescos',
|
||||
status: 'in_transit',
|
||||
orderDate: '2024-01-24',
|
||||
deliveryDate: '2024-01-26',
|
||||
totalAmount: 320.75,
|
||||
items: [
|
||||
{ name: 'Mantequilla', quantity: 20, unit: 'kg', price: 5.80, total: 116.00 },
|
||||
{ name: 'Nata', quantity: 15, unit: 'L', price: 3.25, total: 48.75 },
|
||||
],
|
||||
paymentStatus: 'pending',
|
||||
notes: 'Producto refrigerado',
|
||||
},
|
||||
];
|
||||
|
||||
const mockSuppliers = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Molinos del Sur',
|
||||
contact: 'Juan Pérez',
|
||||
email: 'juan@molinosdelsur.com',
|
||||
phone: '+34 91 234 5678',
|
||||
category: 'Harinas',
|
||||
rating: 4.8,
|
||||
totalOrders: 24,
|
||||
totalSpent: 15600.00,
|
||||
paymentTerms: '30 días',
|
||||
leadTime: '2-3 días',
|
||||
location: 'Sevilla',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Levaduras SA',
|
||||
contact: 'María González',
|
||||
email: 'maria@levaduras.com',
|
||||
phone: '+34 93 456 7890',
|
||||
category: 'Levaduras',
|
||||
rating: 4.6,
|
||||
totalOrders: 18,
|
||||
totalSpent: 8450.00,
|
||||
paymentTerms: '15 días',
|
||||
leadTime: '1-2 días',
|
||||
location: 'Barcelona',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Lácteos Frescos',
|
||||
contact: 'Carlos Ruiz',
|
||||
email: 'carlos@lacteosfrescos.com',
|
||||
phone: '+34 96 789 0123',
|
||||
category: 'Lácteos',
|
||||
rating: 4.4,
|
||||
totalOrders: 32,
|
||||
totalSpent: 12300.00,
|
||||
paymentTerms: '20 días',
|
||||
leadTime: '1 día',
|
||||
location: 'Valencia',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
approved: { color: 'blue', text: 'Aprobado' },
|
||||
in_transit: { color: 'purple', text: 'En Tránsito' },
|
||||
delivered: { color: 'green', text: 'Entregado' },
|
||||
cancelled: { color: 'red', text: 'Cancelado' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const getPaymentStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
paid: { color: 'green', text: 'Pagado' },
|
||||
overdue: { color: 'red', text: 'Vencido' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config?.color as any}>{config?.text || status}</Badge>;
|
||||
};
|
||||
|
||||
const stats = {
|
||||
totalOrders: mockPurchaseOrders.length,
|
||||
pendingOrders: mockPurchaseOrders.filter(o => o.status === 'pending').length,
|
||||
totalSpent: mockPurchaseOrders.reduce((sum, order) => sum + order.totalAmount, 0),
|
||||
activeSuppliers: mockSuppliers.filter(s => s.status === 'active').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Compras"
|
||||
description="Administra órdenes de compra, proveedores y seguimiento de entregas"
|
||||
action={
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Orden de Compra
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Órdenes Totales</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{stats.totalOrders}</p>
|
||||
</div>
|
||||
<ShoppingCart className="h-12 w-12 text-blue-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Órdenes Pendientes</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{stats.pendingOrders}</p>
|
||||
</div>
|
||||
<Calendar className="h-12 w-12 text-orange-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Gasto Total</p>
|
||||
<p className="text-3xl font-bold text-green-600">€{stats.totalSpent.toLocaleString()}</p>
|
||||
</div>
|
||||
<DollarSign className="h-12 w-12 text-green-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Proveedores Activos</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{stats.activeSuppliers}</p>
|
||||
</div>
|
||||
<Truck className="h-12 w-12 text-purple-600" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('orders')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'orders'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Órdenes de Compra
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('suppliers')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'suppliers'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Proveedores
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'analytics'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Análisis
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder={`Buscar ${activeTab === 'orders' ? 'órdenes' : 'proveedores'}...`}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'orders' && (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Orden
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Proveedor
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Fecha Pedido
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Fecha Entrega
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Monto Total
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Pago
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{mockPurchaseOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">{order.id}</div>
|
||||
{order.notes && (
|
||||
<div className="text-xs text-gray-500">{order.notes}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{order.supplier}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(order.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(order.orderDate).toLocaleDateString('es-ES')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(order.deliveryDate).toLocaleDateString('es-ES')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
€{order.totalAmount.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPaymentStatusBadge(order.paymentStatus)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">Ver</Button>
|
||||
<Button variant="outline" size="sm">Editar</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'suppliers' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{mockSuppliers.map((supplier) => (
|
||||
<Card key={supplier.id} className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{supplier.name}</h3>
|
||||
<p className="text-sm text-gray-600">{supplier.category}</p>
|
||||
</div>
|
||||
<Badge variant="green">Activo</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Contacto:</span>
|
||||
<span className="font-medium">{supplier.contact}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Email:</span>
|
||||
<span className="font-medium">{supplier.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Teléfono:</span>
|
||||
<span className="font-medium">{supplier.phone}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Ubicación:</span>
|
||||
<span className="font-medium">{supplier.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Valoración</p>
|
||||
<p className="text-sm font-medium flex items-center">
|
||||
<span className="text-yellow-500">★</span>
|
||||
<span className="ml-1">{supplier.rating}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Pedidos</p>
|
||||
<p className="text-sm font-medium">{supplier.totalOrders}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Total Gastado</p>
|
||||
<p className="text-sm font-medium">€{supplier.totalSpent.toLocaleString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Tiempo Entrega</p>
|
||||
<p className="text-sm font-medium">{supplier.leadTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-xs text-gray-600">Condiciones de Pago</p>
|
||||
<p className="text-sm font-medium">{supplier.paymentTerms}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Ver Detalles
|
||||
</Button>
|
||||
<Button size="sm" className="flex-1">
|
||||
Nuevo Pedido
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'analytics' && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gastos por Mes</h3>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-500">Gráfico de gastos mensuales</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Top Proveedores</h3>
|
||||
<div className="space-y-3">
|
||||
{mockSuppliers
|
||||
.sort((a, b) => b.totalSpent - a.totalSpent)
|
||||
.slice(0, 5)
|
||||
.map((supplier, index) => (
|
||||
<div key={supplier.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm font-medium text-gray-500 w-4">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<span className="ml-3 text-sm text-gray-900">{supplier.name}</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
€{supplier.totalSpent.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Gastos por Categoría</h3>
|
||||
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
||||
<p className="text-gray-500">Gráfico de gastos por categoría</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcurementPage;
|
||||
1
frontend/src/pages/app/operations/procurement/index.ts
Normal file
1
frontend/src/pages/app/operations/procurement/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ProcurementPage } from './ProcurementPage';
|
||||
315
frontend/src/pages/app/operations/production/ProductionPage.tsx
Normal file
315
frontend/src/pages/app/operations/production/ProductionPage.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Calendar, Clock, Users, AlertCircle } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
|
||||
|
||||
const ProductionPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('schedule');
|
||||
|
||||
const mockProductionStats = {
|
||||
dailyTarget: 150,
|
||||
completed: 85,
|
||||
inProgress: 12,
|
||||
pending: 53,
|
||||
efficiency: 78,
|
||||
quality: 94,
|
||||
};
|
||||
|
||||
const mockProductionOrders = [
|
||||
{
|
||||
id: '1',
|
||||
recipeName: 'Pan de Molde Integral',
|
||||
quantity: 20,
|
||||
status: 'in_progress',
|
||||
priority: 'high',
|
||||
assignedTo: 'Juan Panadero',
|
||||
startTime: '2024-01-26T06:00:00Z',
|
||||
estimatedCompletion: '2024-01-26T10:00:00Z',
|
||||
progress: 65,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
recipeName: 'Croissants de Mantequilla',
|
||||
quantity: 50,
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
assignedTo: 'María González',
|
||||
startTime: '2024-01-26T08:00:00Z',
|
||||
estimatedCompletion: '2024-01-26T12:00:00Z',
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
recipeName: 'Baguettes Francesas',
|
||||
quantity: 30,
|
||||
status: 'completed',
|
||||
priority: 'medium',
|
||||
assignedTo: 'Carlos Ruiz',
|
||||
startTime: '2024-01-26T04:00:00Z',
|
||||
estimatedCompletion: '2024-01-26T08:00:00Z',
|
||||
progress: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
in_progress: { color: 'blue', text: 'En Proceso' },
|
||||
completed: { color: 'green', text: 'Completado' },
|
||||
cancelled: { color: 'red', text: 'Cancelado' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority: string) => {
|
||||
const priorityConfig = {
|
||||
low: { color: 'gray', text: 'Baja' },
|
||||
medium: { color: 'yellow', text: 'Media' },
|
||||
high: { color: 'orange', text: 'Alta' },
|
||||
urgent: { color: 'red', text: 'Urgente' },
|
||||
};
|
||||
|
||||
const config = priorityConfig[priority as keyof typeof priorityConfig];
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Producción"
|
||||
description="Planifica y controla la producción diaria de tu panadería"
|
||||
action={
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Orden de Producción
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Production Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Meta Diaria</p>
|
||||
<p className="text-2xl font-bold text-[var(--text-primary)]">{mockProductionStats.dailyTarget}</p>
|
||||
</div>
|
||||
<Calendar className="h-8 w-8 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Completado</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">{mockProductionStats.completed}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-[var(--color-success)]" 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>
|
||||
</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)]">En Proceso</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">{mockProductionStats.inProgress}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-[var(--color-info)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Pendiente</p>
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">{mockProductionStats.pending}</p>
|
||||
</div>
|
||||
<AlertCircle className="h-8 w-8 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Eficiencia</p>
|
||||
<p className="text-2xl font-bold text-purple-600">{mockProductionStats.efficiency}%</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</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)]">Calidad</p>
|
||||
<p className="text-2xl font-bold text-indigo-600">{mockProductionStats.quality}%</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-[var(--border-primary)]">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('schedule')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'schedule'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Programación
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('batches')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'batches'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Lotes de Producción
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('quality')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'quality'
|
||||
? 'border-orange-500 text-[var(--color-primary)]'
|
||||
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:border-[var(--border-secondary)]'
|
||||
}`}
|
||||
>
|
||||
Control de Calidad
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'schedule' && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)]">Órdenes de Producción</h3>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Vista Calendario
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-[var(--bg-secondary)]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Receta
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Cantidad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Prioridad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Asignado a
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Progreso
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Tiempo Estimado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{mockProductionOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-[var(--bg-secondary)]">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">{order.recipeName}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{order.quantity} unidades
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(order.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPriorityBadge(order.priority)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<Users className="h-4 w-4 text-[var(--text-tertiary)] mr-2" />
|
||||
<span className="text-sm text-[var(--text-primary)]">{order.assignedTo}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="w-full bg-[var(--bg-quaternary)] rounded-full h-2 mr-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${order.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--text-primary)]">{order.progress}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Button variant="outline" size="sm" className="mr-2">
|
||||
Ver
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Editar
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'batches' && (
|
||||
<BatchTracker />
|
||||
)}
|
||||
|
||||
{activeTab === 'quality' && (
|
||||
<QualityControl />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionPage;
|
||||
@@ -0,0 +1,315 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Calendar, Clock, Users, AlertCircle } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { ProductionSchedule, BatchTracker, QualityControl } from '../../../../components/domain/production';
|
||||
|
||||
const ProductionPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('schedule');
|
||||
|
||||
const mockProductionStats = {
|
||||
dailyTarget: 150,
|
||||
completed: 85,
|
||||
inProgress: 12,
|
||||
pending: 53,
|
||||
efficiency: 78,
|
||||
quality: 94,
|
||||
};
|
||||
|
||||
const mockProductionOrders = [
|
||||
{
|
||||
id: '1',
|
||||
recipeName: 'Pan de Molde Integral',
|
||||
quantity: 20,
|
||||
status: 'in_progress',
|
||||
priority: 'high',
|
||||
assignedTo: 'Juan Panadero',
|
||||
startTime: '2024-01-26T06:00:00Z',
|
||||
estimatedCompletion: '2024-01-26T10:00:00Z',
|
||||
progress: 65,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
recipeName: 'Croissants de Mantequilla',
|
||||
quantity: 50,
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
assignedTo: 'María González',
|
||||
startTime: '2024-01-26T08:00:00Z',
|
||||
estimatedCompletion: '2024-01-26T12:00:00Z',
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
recipeName: 'Baguettes Francesas',
|
||||
quantity: 30,
|
||||
status: 'completed',
|
||||
priority: 'medium',
|
||||
assignedTo: 'Carlos Ruiz',
|
||||
startTime: '2024-01-26T04:00:00Z',
|
||||
estimatedCompletion: '2024-01-26T08:00:00Z',
|
||||
progress: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
pending: { color: 'yellow', text: 'Pendiente' },
|
||||
in_progress: { color: 'blue', text: 'En Proceso' },
|
||||
completed: { color: 'green', text: 'Completado' },
|
||||
cancelled: { color: 'red', text: 'Cancelado' },
|
||||
};
|
||||
|
||||
const config = statusConfig[status as keyof typeof statusConfig];
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority: string) => {
|
||||
const priorityConfig = {
|
||||
low: { color: 'gray', text: 'Baja' },
|
||||
medium: { color: 'yellow', text: 'Media' },
|
||||
high: { color: 'orange', text: 'Alta' },
|
||||
urgent: { color: 'red', text: 'Urgente' },
|
||||
};
|
||||
|
||||
const config = priorityConfig[priority as keyof typeof priorityConfig];
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Producción"
|
||||
description="Planifica y controla la producción diaria de tu panadería"
|
||||
action={
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Orden de Producción
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Production Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Meta Diaria</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{mockProductionStats.dailyTarget}</p>
|
||||
</div>
|
||||
<Calendar className="h-8 w-8 text-blue-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Completado</p>
|
||||
<p className="text-2xl font-bold text-green-600">{mockProductionStats.completed}</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-green-600" 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>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">En Proceso</p>
|
||||
<p className="text-2xl font-bold text-blue-600">{mockProductionStats.inProgress}</p>
|
||||
</div>
|
||||
<Clock className="h-8 w-8 text-blue-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Pendiente</p>
|
||||
<p className="text-2xl font-bold text-orange-600">{mockProductionStats.pending}</p>
|
||||
</div>
|
||||
<AlertCircle className="h-8 w-8 text-orange-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Eficiencia</p>
|
||||
<p className="text-2xl font-bold text-purple-600">{mockProductionStats.efficiency}%</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Calidad</p>
|
||||
<p className="text-2xl font-bold text-indigo-600">{mockProductionStats.quality}%</p>
|
||||
</div>
|
||||
<div className="h-8 w-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs Navigation */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('schedule')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'schedule'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Programación
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('batches')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'batches'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Lotes de Producción
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('quality')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'quality'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Control de Calidad
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'schedule' && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-lg font-medium text-gray-900">Órdenes de Producción</h3>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Filtros
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Vista Calendario
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Receta
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Cantidad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Prioridad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Asignado a
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Progreso
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Tiempo Estimado
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{mockProductionOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">{order.recipeName}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{order.quantity} unidades
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(order.status)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getPriorityBadge(order.priority)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<Users className="h-4 w-4 text-gray-400 mr-2" />
|
||||
<span className="text-sm text-gray-900">{order.assignedTo}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mr-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${order.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-900">{order.progress}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(order.estimatedCompletion).toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Button variant="outline" size="sm" className="mr-2">
|
||||
Ver
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Editar
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeTab === 'batches' && (
|
||||
<BatchTracker />
|
||||
)}
|
||||
|
||||
{activeTab === 'quality' && (
|
||||
<QualityControl />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionPage;
|
||||
1
frontend/src/pages/app/operations/production/index.ts
Normal file
1
frontend/src/pages/app/operations/production/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ProductionPage } from './ProductionPage';
|
||||
412
frontend/src/pages/app/operations/recipes/RecipesPage.tsx
Normal file
412
frontend/src/pages/app/operations/recipes/RecipesPage.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Star, Clock, Users, DollarSign } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const RecipesPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [selectedDifficulty, setSelectedDifficulty] = useState('all');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
|
||||
const mockRecipes = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Pan de Molde Integral',
|
||||
category: 'bread',
|
||||
difficulty: 'medium',
|
||||
prepTime: 120,
|
||||
bakingTime: 35,
|
||||
yield: 1,
|
||||
rating: 4.8,
|
||||
cost: 2.50,
|
||||
price: 4.50,
|
||||
profit: 2.00,
|
||||
image: '/api/placeholder/300/200',
|
||||
tags: ['integral', 'saludable', 'artesanal'],
|
||||
description: 'Pan integral artesanal con semillas, perfecto para desayunos saludables.',
|
||||
ingredients: [
|
||||
{ name: 'Harina integral', quantity: 500, unit: 'g' },
|
||||
{ name: 'Agua', quantity: 300, unit: 'ml' },
|
||||
{ name: 'Levadura', quantity: 10, unit: 'g' },
|
||||
{ name: 'Sal', quantity: 8, unit: 'g' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Croissants de Mantequilla',
|
||||
category: 'pastry',
|
||||
difficulty: 'hard',
|
||||
prepTime: 480,
|
||||
bakingTime: 20,
|
||||
yield: 12,
|
||||
rating: 4.9,
|
||||
cost: 8.50,
|
||||
price: 18.00,
|
||||
profit: 9.50,
|
||||
image: '/api/placeholder/300/200',
|
||||
tags: ['francés', 'mantequilla', 'hojaldrado'],
|
||||
description: 'Croissants franceses tradicionales con laminado de mantequilla.',
|
||||
ingredients: [
|
||||
{ name: 'Harina de fuerza', quantity: 500, unit: 'g' },
|
||||
{ name: 'Mantequilla', quantity: 250, unit: 'g' },
|
||||
{ name: 'Leche', quantity: 150, unit: 'ml' },
|
||||
{ name: 'Azúcar', quantity: 50, unit: 'g' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Tarta de Manzana',
|
||||
category: 'cake',
|
||||
difficulty: 'easy',
|
||||
prepTime: 45,
|
||||
bakingTime: 40,
|
||||
yield: 8,
|
||||
rating: 4.6,
|
||||
cost: 4.20,
|
||||
price: 12.00,
|
||||
profit: 7.80,
|
||||
image: '/api/placeholder/300/200',
|
||||
tags: ['frutal', 'casera', 'temporada'],
|
||||
description: 'Tarta casera de manzana con canela y masa quebrada.',
|
||||
ingredients: [
|
||||
{ name: 'Manzanas', quantity: 1000, unit: 'g' },
|
||||
{ name: 'Harina', quantity: 250, unit: 'g' },
|
||||
{ name: 'Mantequilla', quantity: 125, unit: 'g' },
|
||||
{ name: 'Azúcar', quantity: 100, unit: 'g' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'Todas las categorías' },
|
||||
{ value: 'bread', label: 'Panes' },
|
||||
{ value: 'pastry', label: 'Bollería' },
|
||||
{ value: 'cake', label: 'Tartas' },
|
||||
{ value: 'cookie', label: 'Galletas' },
|
||||
{ value: 'other', label: 'Otros' },
|
||||
];
|
||||
|
||||
const difficulties = [
|
||||
{ value: 'all', label: 'Todas las dificultades' },
|
||||
{ value: 'easy', label: 'Fácil' },
|
||||
{ value: 'medium', label: 'Medio' },
|
||||
{ value: 'hard', label: 'Difícil' },
|
||||
];
|
||||
|
||||
const getCategoryBadge = (category: string) => {
|
||||
const categoryConfig = {
|
||||
bread: { color: 'brown', text: 'Pan' },
|
||||
pastry: { color: 'yellow', text: 'Bollería' },
|
||||
cake: { color: 'pink', text: 'Tarta' },
|
||||
cookie: { color: 'orange', text: 'Galleta' },
|
||||
other: { color: 'gray', text: 'Otro' },
|
||||
};
|
||||
|
||||
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
const getDifficultyBadge = (difficulty: string) => {
|
||||
const difficultyConfig = {
|
||||
easy: { color: 'green', text: 'Fácil' },
|
||||
medium: { color: 'yellow', text: 'Medio' },
|
||||
hard: { color: 'red', text: 'Difícil' },
|
||||
};
|
||||
|
||||
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
const formatTime = (minutes: number) => {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
};
|
||||
|
||||
const filteredRecipes = mockRecipes.filter(recipe => {
|
||||
const matchesSearch = recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
recipe.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
recipe.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
const matchesCategory = selectedCategory === 'all' || recipe.category === selectedCategory;
|
||||
const matchesDifficulty = selectedDifficulty === 'all' || recipe.difficulty === selectedDifficulty;
|
||||
|
||||
return matchesSearch && matchesCategory && matchesDifficulty;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Recetas"
|
||||
description="Administra y organiza todas las recetas de tu panadería"
|
||||
action={
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Receta
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Recetas</p>
|
||||
<p className="text-3xl font-bold text-[var(--text-primary)]">{mockRecipes.length}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-[var(--color-info)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Más Populares</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">
|
||||
{mockRecipes.filter(r => r.rating > 4.7).length}
|
||||
</p>
|
||||
</div>
|
||||
<Star className="h-12 w-12 text-yellow-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Costo Promedio</p>
|
||||
<p className="text-3xl font-bold text-[var(--color-success)]">
|
||||
€{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<DollarSign className="h-12 w-12 text-[var(--color-success)]" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-secondary)]">Margen Promedio</p>
|
||||
<p className="text-3xl font-bold text-purple-600">
|
||||
€{(mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar recetas por nombre, ingredientes o etiquetas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.value} value={cat.value}>{cat.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={selectedDifficulty}
|
||||
onChange={(e) => setSelectedDifficulty(e.target.value)}
|
||||
className="px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
||||
>
|
||||
{difficulties.map(diff => (
|
||||
<option key={diff.value} value={diff.value}>{diff.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
|
||||
>
|
||||
{viewMode === 'grid' ? 'Vista Lista' : 'Vista Cuadrícula'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recipes Grid/List */}
|
||||
{viewMode === 'grid' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredRecipes.map((recipe) => (
|
||||
<Card key={recipe.id} className="overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div className="aspect-w-16 aspect-h-9">
|
||||
<img
|
||||
src={recipe.image}
|
||||
alt={recipe.name}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] line-clamp-1">
|
||||
{recipe.name}
|
||||
</h3>
|
||||
<div className="flex items-center ml-2">
|
||||
<Star className="h-4 w-4 text-yellow-400 fill-current" />
|
||||
<span className="text-sm text-[var(--text-secondary)] ml-1">{recipe.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[var(--text-secondary)] text-sm mb-3 line-clamp-2">
|
||||
{recipe.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{getCategoryBadge(recipe.category)}
|
||||
{getDifficultyBadge(recipe.difficulty)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4 text-sm text-[var(--text-secondary)]">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-1" />
|
||||
<span>{formatTime(recipe.prepTime + recipe.bakingTime)}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span>{recipe.yield} porciones</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-[var(--text-secondary)]">Costo: </span>
|
||||
<span className="font-medium">€{recipe.cost.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-[var(--text-secondary)]">Precio: </span>
|
||||
<span className="font-medium text-[var(--color-success)]">€{recipe.price.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Ver Receta
|
||||
</Button>
|
||||
<Button size="sm" className="flex-1">
|
||||
Producir
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-[var(--bg-secondary)]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Receta
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Categoría
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Dificultad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Tiempo Total
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Rendimiento
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Costo
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Precio
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Margen
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredRecipes.map((recipe) => (
|
||||
<tr key={recipe.id} className="hover:bg-[var(--bg-secondary)]">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={recipe.image}
|
||||
alt={recipe.name}
|
||||
className="h-10 w-10 rounded-full mr-4"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-[var(--text-primary)]">{recipe.name}</div>
|
||||
<div className="flex items-center">
|
||||
<Star className="h-3 w-3 text-yellow-400 fill-current" />
|
||||
<span className="text-xs text-[var(--text-tertiary)] ml-1">{recipe.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getCategoryBadge(recipe.category)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getDifficultyBadge(recipe.difficulty)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{formatTime(recipe.prepTime + recipe.bakingTime)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
{recipe.yield} porciones
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
|
||||
€{recipe.cost.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--color-success)] font-medium">
|
||||
€{recipe.price.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-purple-600 font-medium">
|
||||
€{recipe.profit.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">Ver</Button>
|
||||
<Button size="sm">Producir</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecipesPage;
|
||||
412
frontend/src/pages/app/operations/recipes/RecipesPage.tsx.backup
Normal file
412
frontend/src/pages/app/operations/recipes/RecipesPage.tsx.backup
Normal file
@@ -0,0 +1,412 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Search, Filter, Star, Clock, Users, DollarSign } from 'lucide-react';
|
||||
import { Button, Input, Card, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const RecipesPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [selectedDifficulty, setSelectedDifficulty] = useState('all');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
|
||||
const mockRecipes = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Pan de Molde Integral',
|
||||
category: 'bread',
|
||||
difficulty: 'medium',
|
||||
prepTime: 120,
|
||||
bakingTime: 35,
|
||||
yield: 1,
|
||||
rating: 4.8,
|
||||
cost: 2.50,
|
||||
price: 4.50,
|
||||
profit: 2.00,
|
||||
image: '/api/placeholder/300/200',
|
||||
tags: ['integral', 'saludable', 'artesanal'],
|
||||
description: 'Pan integral artesanal con semillas, perfecto para desayunos saludables.',
|
||||
ingredients: [
|
||||
{ name: 'Harina integral', quantity: 500, unit: 'g' },
|
||||
{ name: 'Agua', quantity: 300, unit: 'ml' },
|
||||
{ name: 'Levadura', quantity: 10, unit: 'g' },
|
||||
{ name: 'Sal', quantity: 8, unit: 'g' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Croissants de Mantequilla',
|
||||
category: 'pastry',
|
||||
difficulty: 'hard',
|
||||
prepTime: 480,
|
||||
bakingTime: 20,
|
||||
yield: 12,
|
||||
rating: 4.9,
|
||||
cost: 8.50,
|
||||
price: 18.00,
|
||||
profit: 9.50,
|
||||
image: '/api/placeholder/300/200',
|
||||
tags: ['francés', 'mantequilla', 'hojaldrado'],
|
||||
description: 'Croissants franceses tradicionales con laminado de mantequilla.',
|
||||
ingredients: [
|
||||
{ name: 'Harina de fuerza', quantity: 500, unit: 'g' },
|
||||
{ name: 'Mantequilla', quantity: 250, unit: 'g' },
|
||||
{ name: 'Leche', quantity: 150, unit: 'ml' },
|
||||
{ name: 'Azúcar', quantity: 50, unit: 'g' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Tarta de Manzana',
|
||||
category: 'cake',
|
||||
difficulty: 'easy',
|
||||
prepTime: 45,
|
||||
bakingTime: 40,
|
||||
yield: 8,
|
||||
rating: 4.6,
|
||||
cost: 4.20,
|
||||
price: 12.00,
|
||||
profit: 7.80,
|
||||
image: '/api/placeholder/300/200',
|
||||
tags: ['frutal', 'casera', 'temporada'],
|
||||
description: 'Tarta casera de manzana con canela y masa quebrada.',
|
||||
ingredients: [
|
||||
{ name: 'Manzanas', quantity: 1000, unit: 'g' },
|
||||
{ name: 'Harina', quantity: 250, unit: 'g' },
|
||||
{ name: 'Mantequilla', quantity: 125, unit: 'g' },
|
||||
{ name: 'Azúcar', quantity: 100, unit: 'g' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: 'Todas las categorías' },
|
||||
{ value: 'bread', label: 'Panes' },
|
||||
{ value: 'pastry', label: 'Bollería' },
|
||||
{ value: 'cake', label: 'Tartas' },
|
||||
{ value: 'cookie', label: 'Galletas' },
|
||||
{ value: 'other', label: 'Otros' },
|
||||
];
|
||||
|
||||
const difficulties = [
|
||||
{ value: 'all', label: 'Todas las dificultades' },
|
||||
{ value: 'easy', label: 'Fácil' },
|
||||
{ value: 'medium', label: 'Medio' },
|
||||
{ value: 'hard', label: 'Difícil' },
|
||||
];
|
||||
|
||||
const getCategoryBadge = (category: string) => {
|
||||
const categoryConfig = {
|
||||
bread: { color: 'brown', text: 'Pan' },
|
||||
pastry: { color: 'yellow', text: 'Bollería' },
|
||||
cake: { color: 'pink', text: 'Tarta' },
|
||||
cookie: { color: 'orange', text: 'Galleta' },
|
||||
other: { color: 'gray', text: 'Otro' },
|
||||
};
|
||||
|
||||
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
const getDifficultyBadge = (difficulty: string) => {
|
||||
const difficultyConfig = {
|
||||
easy: { color: 'green', text: 'Fácil' },
|
||||
medium: { color: 'yellow', text: 'Medio' },
|
||||
hard: { color: 'red', text: 'Difícil' },
|
||||
};
|
||||
|
||||
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
|
||||
return <Badge variant={config.color as any}>{config.text}</Badge>;
|
||||
};
|
||||
|
||||
const formatTime = (minutes: number) => {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
};
|
||||
|
||||
const filteredRecipes = mockRecipes.filter(recipe => {
|
||||
const matchesSearch = recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
recipe.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
recipe.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
const matchesCategory = selectedCategory === 'all' || recipe.category === selectedCategory;
|
||||
const matchesDifficulty = selectedDifficulty === 'all' || recipe.difficulty === selectedDifficulty;
|
||||
|
||||
return matchesSearch && matchesCategory && matchesDifficulty;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<PageHeader
|
||||
title="Gestión de Recetas"
|
||||
description="Administra y organiza todas las recetas de tu panadería"
|
||||
action={
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nueva Receta
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Recetas</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{mockRecipes.length}</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Más Populares</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">
|
||||
{mockRecipes.filter(r => r.rating > 4.7).length}
|
||||
</p>
|
||||
</div>
|
||||
<Star className="h-12 w-12 text-yellow-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Costo Promedio</p>
|
||||
<p className="text-3xl font-bold text-green-600">
|
||||
€{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<DollarSign className="h-12 w-12 text-green-600" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Margen Promedio</p>
|
||||
<p className="text-3xl font-bold text-purple-600">
|
||||
€{(mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Buscar recetas por nombre, ingredientes o etiquetas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.value} value={cat.value}>{cat.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={selectedDifficulty}
|
||||
onChange={(e) => setSelectedDifficulty(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
{difficulties.map(diff => (
|
||||
<option key={diff.value} value={diff.value}>{diff.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
|
||||
>
|
||||
{viewMode === 'grid' ? 'Vista Lista' : 'Vista Cuadrícula'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recipes Grid/List */}
|
||||
{viewMode === 'grid' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredRecipes.map((recipe) => (
|
||||
<Card key={recipe.id} className="overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div className="aspect-w-16 aspect-h-9">
|
||||
<img
|
||||
src={recipe.image}
|
||||
alt={recipe.name}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1">
|
||||
{recipe.name}
|
||||
</h3>
|
||||
<div className="flex items-center ml-2">
|
||||
<Star className="h-4 w-4 text-yellow-400 fill-current" />
|
||||
<span className="text-sm text-gray-600 ml-1">{recipe.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
|
||||
{recipe.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{getCategoryBadge(recipe.category)}
|
||||
{getDifficultyBadge(recipe.difficulty)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4 text-sm text-gray-600">
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-4 w-4 mr-1" />
|
||||
<span>{formatTime(recipe.prepTime + recipe.bakingTime)}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span>{recipe.yield} porciones</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600">Costo: </span>
|
||||
<span className="font-medium">€{recipe.cost.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600">Precio: </span>
|
||||
<span className="font-medium text-green-600">€{recipe.price.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Ver Receta
|
||||
</Button>
|
||||
<Button size="sm" className="flex-1">
|
||||
Producir
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Receta
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Categoría
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Dificultad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Tiempo Total
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Rendimiento
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Costo
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Precio
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Margen
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredRecipes.map((recipe) => (
|
||||
<tr key={recipe.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={recipe.image}
|
||||
alt={recipe.name}
|
||||
className="h-10 w-10 rounded-full mr-4"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{recipe.name}</div>
|
||||
<div className="flex items-center">
|
||||
<Star className="h-3 w-3 text-yellow-400 fill-current" />
|
||||
<span className="text-xs text-gray-500 ml-1">{recipe.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getCategoryBadge(recipe.category)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getDifficultyBadge(recipe.difficulty)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatTime(recipe.prepTime + recipe.bakingTime)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{recipe.yield} porciones
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
€{recipe.cost.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-green-600 font-medium">
|
||||
€{recipe.price.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-purple-600 font-medium">
|
||||
€{recipe.profit.toFixed(2)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">Ver</Button>
|
||||
<Button size="sm">Producir</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecipesPage;
|
||||
1
frontend/src/pages/app/operations/recipes/index.ts
Normal file
1
frontend/src/pages/app/operations/recipes/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as RecipesPage } from './RecipesPage';
|
||||
Reference in New Issue
Block a user