Refactor Commerce module
This commit is contained in:
45
src/App.tsx
45
src/App.tsx
@@ -39,15 +39,13 @@ import Security from "./pages/dashboard/Security";
|
|||||||
import VehicleManagement from "./pages/dashboard/VehicleManagement";
|
import VehicleManagement from "./pages/dashboard/VehicleManagement";
|
||||||
import Sustainability from "./pages/dashboard/Sustainability";
|
import Sustainability from "./pages/dashboard/Sustainability";
|
||||||
import Establishments from "./pages/dashboard/Establishments";
|
import Establishments from "./pages/dashboard/Establishments";
|
||||||
// Commerce pages
|
// Commerce pages (for retail stores)
|
||||||
import CommerceEstablishments from "./pages/dashboard/commerce/Establishments";
|
import CommerceStore from "./pages/dashboard/commerce/Store";
|
||||||
import CommercePOS from "./pages/dashboard/commerce/POSTerminal";
|
import CommercePOS from "./pages/dashboard/commerce/POSTerminal";
|
||||||
import CommerceOrders from "./pages/dashboard/commerce/Orders";
|
import CommerceCustomers from "./pages/dashboard/commerce/Customers";
|
||||||
import CommerceMenu from "./pages/dashboard/commerce/Menu";
|
|
||||||
import CommerceHotel from "./pages/dashboard/commerce/Hotel";
|
|
||||||
import CommerceReservations from "./pages/dashboard/commerce/Reservations";
|
|
||||||
import CommerceInventory from "./pages/dashboard/commerce/Inventory";
|
import CommerceInventory from "./pages/dashboard/commerce/Inventory";
|
||||||
import CommerceStaff from "./pages/dashboard/commerce/Staff";
|
import CommerceStaff from "./pages/dashboard/commerce/Staff";
|
||||||
|
import CommerceCashier from "./pages/dashboard/commerce/Cashier";
|
||||||
import CommerceReports from "./pages/dashboard/commerce/Reports";
|
import CommerceReports from "./pages/dashboard/commerce/Reports";
|
||||||
// Hotel pages
|
// Hotel pages
|
||||||
import HotelRooms from "./pages/dashboard/hotel/Rooms";
|
import HotelRooms from "./pages/dashboard/hotel/Rooms";
|
||||||
@@ -304,10 +302,10 @@ const AppRouter = () => (
|
|||||||
} />
|
} />
|
||||||
|
|
||||||
{/* Commerce Routes */}
|
{/* Commerce Routes */}
|
||||||
<Route path="/dashboard/commerce/establishments" element={
|
<Route path="/dashboard/commerce/store" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<CommerceEstablishments />
|
<CommerceStore />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
@@ -320,34 +318,18 @@ const AppRouter = () => (
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/dashboard/commerce/orders" element={
|
<Route path="/dashboard/commerce/customers" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<CommerceOrders />
|
<CommerceCustomers />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/dashboard/commerce/menu" element={
|
<Route path="/dashboard/commerce/cashier" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<CommerceMenu />
|
<CommerceCashier />
|
||||||
</DashboardLayout>
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/dashboard/commerce/hotel" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<DashboardLayout>
|
|
||||||
<CommerceHotel />
|
|
||||||
</DashboardLayout>
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/dashboard/commerce/reservations" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<DashboardLayout>
|
|
||||||
<CommerceReservations />
|
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
@@ -384,13 +366,6 @@ const AppRouter = () => (
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/dashboard/commerce/tables" element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<DashboardLayout>
|
|
||||||
<CommerceMenu />
|
|
||||||
</DashboardLayout>
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
|
|
||||||
<Route path="/dashboard/hotel-management" element={
|
<Route path="/dashboard/hotel-management" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
|
|||||||
@@ -165,15 +165,12 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
label: 'Comercios',
|
label: 'Comercios',
|
||||||
path: '/dashboard/commerce',
|
path: '/dashboard/commerce',
|
||||||
subItems: [
|
subItems: [
|
||||||
{ icon: Store, label: 'Establecimientos', path: '/dashboard/commerce/establishments' },
|
{ icon: Store, label: 'Mi Comercio', path: '/dashboard/commerce/store' },
|
||||||
{ icon: CreditCard, label: 'POS Terminal', path: '/dashboard/commerce/pos' },
|
{ icon: CreditCard, label: 'POS Ventas', path: '/dashboard/commerce/pos' },
|
||||||
{ icon: Receipt, label: 'Pedidos', path: '/dashboard/commerce/orders' },
|
|
||||||
{ icon: UtensilsCrossed, label: 'Menú', path: '/dashboard/commerce/menu' },
|
|
||||||
{ icon: Grid3x3, label: 'Mesas', path: '/dashboard/commerce/tables' },
|
|
||||||
{ icon: Hotel, label: 'Hotel', path: '/dashboard/commerce/hotel' },
|
|
||||||
{ icon: BookOpen, label: 'Reservaciones', path: '/dashboard/commerce/reservations' },
|
|
||||||
{ icon: Package, label: 'Inventario', path: '/dashboard/commerce/inventory' },
|
{ icon: Package, label: 'Inventario', path: '/dashboard/commerce/inventory' },
|
||||||
|
{ icon: Users, label: 'Clientes', path: '/dashboard/commerce/customers' },
|
||||||
{ icon: Users, label: 'Personal', path: '/dashboard/commerce/staff' },
|
{ icon: Users, label: 'Personal', path: '/dashboard/commerce/staff' },
|
||||||
|
{ icon: Receipt, label: 'Caja', path: '/dashboard/commerce/cashier' },
|
||||||
{ icon: BarChart3, label: 'Reportes', path: '/dashboard/commerce/reports' },
|
{ icon: BarChart3, label: 'Reportes', path: '/dashboard/commerce/reports' },
|
||||||
{ icon: DollarSign, label: 'Ventas', path: '/dashboard/commerce/sales' }
|
{ icon: DollarSign, label: 'Ventas', path: '/dashboard/commerce/sales' }
|
||||||
]
|
]
|
||||||
|
|||||||
172
src/pages/dashboard/commerce/Cashier.tsx
Normal file
172
src/pages/dashboard/commerce/Cashier.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Receipt, DollarSign, TrendingUp, TrendingDown, Clock } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
const Cashier = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [cashierData] = useState({
|
||||||
|
openingBalance: 1000.00,
|
||||||
|
currentBalance: 2450.50,
|
||||||
|
totalSales: 1450.50,
|
||||||
|
totalExpenses: 0,
|
||||||
|
transactionsCount: 25,
|
||||||
|
lastTransaction: new Date().toLocaleTimeString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const [newTransaction, setNewTransaction] = useState({
|
||||||
|
type: 'sale',
|
||||||
|
amount: 0,
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenCashier = () => {
|
||||||
|
toast({ title: 'Caja Abierta', description: 'Caja abierta con saldo inicial' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseCashier = () => {
|
||||||
|
toast({ title: 'Caja Cerrada', description: 'Caja cerrada. Generando reporte...' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddTransaction = () => {
|
||||||
|
if (newTransaction.amount <= 0) {
|
||||||
|
toast({ title: 'Error', description: 'Ingresa un monto válido', variant: 'destructive' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast({ title: 'Éxito', description: 'Transacción registrada' });
|
||||||
|
setNewTransaction({ type: 'sale', amount: 0, description: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Receipt className="w-8 h-8 text-orange-600" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Control de Caja</h1>
|
||||||
|
<p className="text-gray-600">Gestiona el flujo de efectivo de tu tienda</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleOpenCashier}>Abrir Caja</Button>
|
||||||
|
<Button variant="outline" onClick={handleCloseCashier}>Cerrar Caja</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-4 mb-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Saldo Inicial</CardTitle>
|
||||||
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">${cashierData.openingBalance.toFixed(2)}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Saldo Actual</CardTitle>
|
||||||
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-green-600">${cashierData.currentBalance.toFixed(2)}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Ventas del Día</CardTitle>
|
||||||
|
<TrendingUp className="h-4 w-4 text-green-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">${cashierData.totalSales.toFixed(2)}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{cashierData.transactionsCount} transacciones</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Gastos</CardTitle>
|
||||||
|
<TrendingDown className="h-4 w-4 text-red-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-red-600">${cashierData.totalExpenses.toFixed(2)}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
{/* Quick Transaction */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Registrar Transacción</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Tipo de Transacción</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||||
|
<Button
|
||||||
|
variant={newTransaction.type === 'sale' ? 'default' : 'outline'}
|
||||||
|
onClick={() => setNewTransaction({ ...newTransaction, type: 'sale' })}
|
||||||
|
>
|
||||||
|
Venta
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={newTransaction.type === 'expense' ? 'default' : 'outline'}
|
||||||
|
onClick={() => setNewTransaction({ ...newTransaction, type: 'expense' })}
|
||||||
|
>
|
||||||
|
Gasto
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Monto</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newTransaction.amount}
|
||||||
|
onChange={(e) => setNewTransaction({ ...newTransaction, amount: parseFloat(e.target.value) })}
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Descripción</Label>
|
||||||
|
<Input
|
||||||
|
value={newTransaction.description}
|
||||||
|
onChange={(e) => setNewTransaction({ ...newTransaction, description: e.target.value })}
|
||||||
|
placeholder="Detalle de la transacción"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" onClick={handleAddTransaction}>
|
||||||
|
Registrar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Transactions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Transacciones Recientes</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
No hay transacciones registradas hoy
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cashier;
|
||||||
173
src/pages/dashboard/commerce/Customers.tsx
Normal file
173
src/pages/dashboard/commerce/Customers.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Users, Plus, Edit, Trash2, Mail, Phone, MapPin, DollarSign } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
const Customers = () => {
|
||||||
|
const [customers, setCustomers] = useState<any[]>([]);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
toast({ title: 'Éxito', description: 'Cliente agregado correctamente' });
|
||||||
|
setFormData({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
notes: ''
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({ title: 'Error', description: error?.message || 'No se pudo agregar el cliente', variant: 'destructive' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<Users className="w-8 h-8 text-orange-600" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Gestión de Clientes</h1>
|
||||||
|
<p className="text-gray-600">Administra tu base de clientes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar clientes..."
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Nuevo Cliente
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Agregar Cliente</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Nombre</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
|
||||||
|
placeholder="Nombre"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Apellido</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
|
||||||
|
placeholder="Apellido"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
placeholder="email@ejemplo.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Teléfono</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||||
|
placeholder="+1 809 123 4567"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Dirección</Label>
|
||||||
|
<Input
|
||||||
|
value={formData.address}
|
||||||
|
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
||||||
|
placeholder="Dirección del cliente"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleSubmit}>Agregar Cliente</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{customers.length === 0 ? (
|
||||||
|
<div className="col-span-full text-center py-12 text-muted-foreground">
|
||||||
|
No hay clientes registrados. Agrega tu primer cliente.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
customers.map((customer) => (
|
||||||
|
<Card key={customer.id}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-base">{customer.firstName} {customer.lastName}</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">Cliente desde {customer.createdAt}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Mail className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span>{customer.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Phone className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span>{customer.phone}</span>
|
||||||
|
</div>
|
||||||
|
{customer.address && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="truncate">{customer.address}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t">
|
||||||
|
<span className="text-muted-foreground">Total compras:</span>
|
||||||
|
<div className="flex items-center gap-1 font-bold">
|
||||||
|
<DollarSign className="w-4 h-4" />
|
||||||
|
{customer.totalPurchases || 0}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Customers;
|
||||||
@@ -1,46 +1,29 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { CreditCard, Plus, Minus, Trash2, DollarSign } from 'lucide-react';
|
import { CreditCard, Plus, Minus, Trash2, DollarSign, Package, Barcode } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { apiClient } from '@/services/adminApi';
|
import { apiClient } from '@/services/adminApi';
|
||||||
|
|
||||||
const POSTerminal = () => {
|
const POSTerminal = () => {
|
||||||
const [establishments, setEstablishments] = useState<any[]>([]);
|
const [products, setProducts] = useState<any[]>([]);
|
||||||
const [selectedEstablishment, setSelectedEstablishment] = useState('');
|
|
||||||
const [menuItems, setMenuItems] = useState<any[]>([]);
|
|
||||||
const [cart, setCart] = useState<any[]>([]);
|
const [cart, setCart] = useState<any[]>([]);
|
||||||
const [tableNumber, setTableNumber] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadEstablishments();
|
loadProducts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadProducts = async () => {
|
||||||
if (selectedEstablishment) {
|
|
||||||
loadMenu();
|
|
||||||
}
|
|
||||||
}, [selectedEstablishment]);
|
|
||||||
|
|
||||||
const loadEstablishments = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/commerce/establishments');
|
// Load inventory products for POS
|
||||||
setEstablishments(Array.isArray(response) ? response : (response as any)?.establishments || []);
|
// For now using mock data
|
||||||
|
setProducts([]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading establishments:', error);
|
console.error('Error loading products:', error);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadMenu = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiClient.get(`/restaurant/establishments/${selectedEstablishment}/menu`);
|
|
||||||
setMenuItems(Array.isArray(response) ? response : (response as any)?.items || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading menu:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,30 +57,24 @@ const POSTerminal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckout = async () => {
|
const handleCheckout = async () => {
|
||||||
if (!tableNumber) {
|
if (cart.length === 0) {
|
||||||
toast({ title: 'Error', description: 'Ingresa el número de mesa', variant: 'destructive' });
|
toast({ title: 'Error', description: 'El carrito está vacío', variant: 'destructive' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClient.post('/restaurant/orders', {
|
toast({ title: 'Éxito', description: `Venta procesada: $${getTotal().toFixed(2)}` });
|
||||||
establishmentId: parseInt(selectedEstablishment),
|
|
||||||
tableNumber,
|
|
||||||
items: cart.map(item => ({
|
|
||||||
menuItemId: item.id,
|
|
||||||
quantity: item.quantity,
|
|
||||||
price: item.price
|
|
||||||
})),
|
|
||||||
total: getTotal()
|
|
||||||
});
|
|
||||||
toast({ title: 'Éxito', description: 'Orden creada correctamente' });
|
|
||||||
setCart([]);
|
setCart([]);
|
||||||
setTableNumber('');
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({ title: 'Error', description: error?.message || 'No se pudo crear la orden', variant: 'destructive' });
|
toast({ title: 'Error', description: error?.message || 'No se pudo procesar la venta', variant: 'destructive' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredProducts = products.filter(p =>
|
||||||
|
p.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
p.barcode?.includes(searchTerm)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-6">
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
@@ -110,57 +87,59 @@ const POSTerminal = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Select value={selectedEstablishment} onValueChange={setSelectedEstablishment}>
|
<Input
|
||||||
<SelectTrigger className="w-64">
|
placeholder="Buscar producto por nombre o escanear código de barras..."
|
||||||
<SelectValue placeholder="Selecciona establecimiento" />
|
value={searchTerm}
|
||||||
</SelectTrigger>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
<SelectContent>
|
className="max-w-md"
|
||||||
{establishments.map((est) => (
|
/>
|
||||||
<SelectItem key={est.id} value={est.id.toString()}>
|
|
||||||
{est.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedEstablishment && (
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
{/* Products */}
|
||||||
{/* Menu Items */}
|
<div className="lg:col-span-2 grid gap-4 md:grid-cols-2">
|
||||||
<div className="lg:col-span-2 grid gap-4 md:grid-cols-2">
|
{filteredProducts.length === 0 ? (
|
||||||
{menuItems.map((item) => (
|
<div className="col-span-2 text-center py-12">
|
||||||
|
<Package className="w-16 h-16 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<p className="text-muted-foreground">No hay productos en el inventario</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Agrega productos desde la sección de Inventario</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredProducts.map((item) => (
|
||||||
<Card key={item.id} className="cursor-pointer hover:shadow-lg transition-shadow" onClick={() => addToCart(item)}>
|
<Card key={item.id} className="cursor-pointer hover:shadow-lg transition-shadow" onClick={() => addToCart(item)}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">{item.name}</CardTitle>
|
<CardTitle className="text-base">{item.name}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-muted-foreground mb-2">{item.description}</p>
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
{item.barcode && (
|
||||||
<Badge variant="outline">{item.category}</Badge>
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span className="text-lg font-bold">${item.price}</span>
|
<Barcode className="w-3 h-3" />
|
||||||
|
{item.barcode}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Badge variant="outline">{item.category}</Badge>
|
||||||
|
<span className="text-lg font-bold">${item.price}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Stock: {item.stock}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Cart */}
|
{/* Cart */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<Card className="sticky top-6">
|
<Card className="sticky top-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Orden Actual</CardTitle>
|
<CardTitle>Venta Actual</CardTitle>
|
||||||
<div className="mt-2">
|
|
||||||
<Input
|
|
||||||
placeholder="Número de mesa"
|
|
||||||
value={tableNumber}
|
|
||||||
onChange={(e) => setTableNumber(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{cart.length === 0 ? (
|
{cart.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">
|
<p className="text-sm text-muted-foreground text-center py-8">
|
||||||
No hay ítems en la orden
|
No hay productos en la venta
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -202,7 +181,6 @@ const POSTerminal = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
241
src/pages/dashboard/commerce/Store.tsx
Normal file
241
src/pages/dashboard/commerce/Store.tsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Store as StoreIcon, Edit, MapPin, Phone, Globe, Clock } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { apiClient } from '@/services/adminApi';
|
||||||
|
|
||||||
|
const Store = () => {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [storeData, setStoreData] = useState<any>({
|
||||||
|
name: 'Mi Tienda',
|
||||||
|
description: 'Descripción de mi comercio',
|
||||||
|
type: 'store',
|
||||||
|
category: 'retail',
|
||||||
|
address: 'Dirección del comercio',
|
||||||
|
phone: '+1 809 123 4567',
|
||||||
|
email: 'contacto@mitienda.com',
|
||||||
|
website: '',
|
||||||
|
openingHours: {
|
||||||
|
monday: '9:00-18:00',
|
||||||
|
tuesday: '9:00-18:00',
|
||||||
|
wednesday: '9:00-18:00',
|
||||||
|
thursday: '9:00-18:00',
|
||||||
|
friday: '9:00-18:00',
|
||||||
|
saturday: '9:00-14:00',
|
||||||
|
sunday: 'Cerrado'
|
||||||
|
},
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadStoreData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadStoreData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Load the user's store data
|
||||||
|
const response = await apiClient.get('/commerce/establishments');
|
||||||
|
const stores = Array.isArray(response) ? response : (response as any)?.establishments || [];
|
||||||
|
if (stores.length > 0) {
|
||||||
|
setStoreData(stores[0]); // Load first store
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading store:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
if (storeData.id) {
|
||||||
|
await apiClient.patch(`/commerce/establishments/${storeData.id}`, storeData);
|
||||||
|
} else {
|
||||||
|
await apiClient.post('/commerce/establishments', storeData);
|
||||||
|
}
|
||||||
|
toast({ title: 'Éxito', description: 'Información de la tienda actualizada' });
|
||||||
|
setEditing(false);
|
||||||
|
loadStoreData();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({ title: 'Error', description: error?.message || 'No se pudo guardar', variant: 'destructive' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<StoreIcon className="w-8 h-8 text-orange-600" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Mi Comercio</h1>
|
||||||
|
<p className="text-gray-600">Información y configuración de tu tienda</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setEditing(!editing)} variant={editing ? 'outline' : 'default'}>
|
||||||
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
|
{editing ? 'Cancelar' : 'Editar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Store Status */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle>Estado del Comercio</CardTitle>
|
||||||
|
<Badge variant={storeData.isActive ? 'default' : 'secondary'}>
|
||||||
|
{storeData.isActive ? 'Activo' : 'Inactivo'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Basic Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Información Básica</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Nombre del Comercio</Label>
|
||||||
|
<Input
|
||||||
|
value={storeData.name}
|
||||||
|
onChange={(e) => setStoreData({ ...storeData, name: e.target.value })}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="Nombre de tu tienda"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Descripción</Label>
|
||||||
|
<Textarea
|
||||||
|
value={storeData.description}
|
||||||
|
onChange={(e) => setStoreData({ ...storeData, description: e.target.value })}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="Describe tu negocio..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Categoría</Label>
|
||||||
|
<Input
|
||||||
|
value={storeData.category}
|
||||||
|
onChange={(e) => setStoreData({ ...storeData, category: e.target.value })}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="Ej: Boutique, Gift Shop, Electrónica"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Contact Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Información de Contacto</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
Dirección
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={storeData.address}
|
||||||
|
onChange={(e) => setStoreData({ ...storeData, address: e.target.value })}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="Dirección completa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
Teléfono
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={storeData.phone}
|
||||||
|
onChange={(e) => setStoreData({ ...storeData, phone: e.target.value })}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="+1 809 123 4567"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={storeData.email}
|
||||||
|
onChange={(e) => setStoreData({ ...storeData, email: e.target.value })}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="contacto@tienda.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="flex items-center gap-2">
|
||||||
|
<Globe className="w-4 h-4" />
|
||||||
|
Sitio Web (Opcional)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={storeData.website}
|
||||||
|
onChange={(e) => setStoreData({ ...storeData, website: e.target.value })}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Opening Hours */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Clock className="w-5 h-5" />
|
||||||
|
Horario de Atención
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries(storeData.openingHours).map(([day, hours]) => (
|
||||||
|
<div key={day} className="grid grid-cols-2 gap-4 items-center">
|
||||||
|
<Label className="capitalize">{day === 'monday' ? 'Lunes' : day === 'tuesday' ? 'Martes' : day === 'wednesday' ? 'Miércoles' : day === 'thursday' ? 'Jueves' : day === 'friday' ? 'Viernes' : day === 'saturday' ? 'Sábado' : 'Domingo'}</Label>
|
||||||
|
<Input
|
||||||
|
value={hours as string}
|
||||||
|
onChange={(e) => setStoreData({
|
||||||
|
...storeData,
|
||||||
|
openingHours: { ...storeData.openingHours, [day]: e.target.value }
|
||||||
|
})}
|
||||||
|
disabled={!editing}
|
||||||
|
placeholder="9:00-18:00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{editing && (
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setEditing(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
Guardar Cambios
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Store;
|
||||||
Reference in New Issue
Block a user