# PLAN DE IMPLEMENTACION - FRONTEND **Fecha:** 2025-12-18 **Fase:** 3 - Plan de Implementaciones **Capa:** Frontend (React + TypeScript + Vite + Tailwind CSS) --- ## 1. RESUMEN EJECUTIVO ### 1.1 Alcance - **Aplicaciones:** 3 (Backoffice, POS, Storefront) - **Paginas totales:** ~65 - **Componentes compartidos:** ~40 - **PWA:** Si (POS con offline-first) ### 1.2 Arquitectura de Aplicaciones ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ RETAIL FRONTEND APPS │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────┐ ┌────────────────┐ ┌────────────────────┐ │ │ │ BACKOFFICE │ │ POS │ │ STOREFRONT │ │ │ │ (Admin Dashboard) │ │ (Punto Venta) │ │ (E-commerce) │ │ │ ├─────────────────────┤ ├────────────────┤ ├────────────────────┤ │ │ │ - Dashboard │ │ - Ventas │ │ - Catalogo │ │ │ │ - Inventario │ │ - Cobro │ │ - Carrito │ │ │ │ - Productos │ │ - Caja │ │ - Checkout │ │ │ │ - Clientes │ │ - Offline │ │ - Mi cuenta │ │ │ │ - Promociones │ │ │ │ │ │ │ │ - Reportes │ │ │ │ │ │ │ │ - Configuracion │ │ │ │ │ │ │ └─────────────────────┘ └────────────────┘ └────────────────────┘ │ │ │ │ │ │ │ └──────────────────────┴──────────────────────┘ │ │ │ │ │ ┌───────────────────────────────┴─────────────────────────────────┐ │ │ │ SHARED LIBRARIES │ │ │ │ @retail/ui-components @retail/api-client @retail/hooks │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 2. ESTRUCTURA DE PROYECTO ### 2.1 Monorepo Structure ``` retail/frontend/ ├── packages/ │ ├── ui-components/ # Componentes compartidos │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── Button/ │ │ │ │ ├── Input/ │ │ │ │ ├── Modal/ │ │ │ │ ├── Table/ │ │ │ │ ├── Card/ │ │ │ │ ├── Badge/ │ │ │ │ ├── Toast/ │ │ │ │ ├── Spinner/ │ │ │ │ └── index.ts │ │ │ ├── layouts/ │ │ │ │ ├── AdminLayout/ │ │ │ │ ├── POSLayout/ │ │ │ │ └── StoreLayout/ │ │ │ └── index.ts │ │ └── package.json │ │ │ ├── api-client/ # Cliente API tipado │ │ ├── src/ │ │ │ ├── client.ts │ │ │ ├── endpoints/ │ │ │ │ ├── auth.ts │ │ │ │ ├── products.ts │ │ │ │ ├── orders.ts │ │ │ │ ├── customers.ts │ │ │ │ └── ... │ │ │ └── types/ │ │ │ └── index.ts │ │ └── package.json │ │ │ └── hooks/ # Hooks compartidos │ ├── src/ │ │ ├── useAuth.ts │ │ ├── useTenant.ts │ │ ├── useBranch.ts │ │ ├── useToast.ts │ │ ├── useDebounce.ts │ │ └── index.ts │ └── package.json │ ├── apps/ │ ├── backoffice/ # App Admin │ │ ├── src/ │ │ │ ├── pages/ │ │ │ ├── components/ │ │ │ ├── store/ │ │ │ ├── routes/ │ │ │ └── main.tsx │ │ ├── public/ │ │ ├── index.html │ │ └── vite.config.ts │ │ │ ├── pos/ # App POS (PWA) │ │ ├── src/ │ │ │ ├── pages/ │ │ │ ├── components/ │ │ │ ├── store/ │ │ │ ├── offline/ │ │ │ │ ├── db.ts # IndexedDB │ │ │ │ ├── sync.ts │ │ │ │ └── queue.ts │ │ │ ├── hardware/ │ │ │ │ ├── printer.ts │ │ │ │ ├── scanner.ts │ │ │ │ └── drawer.ts │ │ │ ├── sw.ts # Service Worker │ │ │ └── main.tsx │ │ ├── public/ │ │ │ ├── manifest.json │ │ │ └── icons/ │ │ └── vite.config.ts │ │ │ └── storefront/ # App E-commerce │ ├── src/ │ │ ├── pages/ │ │ ├── components/ │ │ ├── store/ │ │ └── main.tsx │ └── vite.config.ts │ ├── package.json ├── pnpm-workspace.yaml └── tsconfig.base.json ``` --- ## 3. APP: BACKOFFICE (Admin Dashboard) ### 3.1 Paginas por Modulo #### Dashboard (RT-008) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Dashboard | `/` | KPIs, graficos, alertas | #### Inventario (RT-003) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Stock por Sucursal | `/inventory` | Vista de stock multi-sucursal | | Transferencias | `/inventory/transfers` | Lista de transferencias | | Nueva Transferencia | `/inventory/transfers/new` | Crear transferencia | | Detalle Transferencia | `/inventory/transfers/:id` | Ver/editar transferencia | | Ajustes | `/inventory/adjustments` | Lista de ajustes | | Nuevo Ajuste | `/inventory/adjustments/new` | Crear ajuste | #### Productos (heredado core) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Lista Productos | `/products` | Catalogo de productos | | Nuevo Producto | `/products/new` | Crear producto | | Detalle Producto | `/products/:id` | Ver/editar producto | | Categorias | `/products/categories` | Gestionar categorias | #### Clientes (RT-005) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Lista Clientes | `/customers` | Lista de clientes | | Nuevo Cliente | `/customers/new` | Crear cliente | | Detalle Cliente | `/customers/:id` | Ver/editar cliente | | Programa Lealtad | `/customers/loyalty` | Config programa | | Niveles Membresia | `/customers/loyalty/levels` | Gestionar niveles | #### Precios (RT-006) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Listas de Precios | `/pricing/pricelists` | Gestionar listas | | Promociones | `/pricing/promotions` | Lista promociones | | Nueva Promocion | `/pricing/promotions/new` | Crear promocion | | Detalle Promocion | `/pricing/promotions/:id` | Ver/editar | | Cupones | `/pricing/coupons` | Lista cupones | | Generar Cupones | `/pricing/coupons/generate` | Generar batch | #### Compras (RT-004) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Sugerencias Compra | `/purchases/suggestions` | Ver sugerencias | | Ordenes Proveedor | `/purchases/orders` | Lista ordenes | | Nueva Orden | `/purchases/orders/new` | Crear orden | | Recepciones | `/purchases/receipts` | Lista recepciones | | Nueva Recepcion | `/purchases/receipts/new` | Crear recepcion | #### E-commerce (RT-009) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Pedidos Online | `/ecommerce/orders` | Lista pedidos | | Detalle Pedido | `/ecommerce/orders/:id` | Ver/gestionar | | Tarifas Envio | `/ecommerce/shipping` | Configurar envios | #### Facturacion (RT-010) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Facturas | `/invoicing` | Lista facturas | | Detalle Factura | `/invoicing/:id` | Ver factura | | Configuracion CFDI | `/invoicing/config` | Config emisor/PAC | #### Reportes (RT-008) | Pagina | Ruta | Descripcion | |--------|------|-------------| | Reporte Ventas | `/reports/sales` | Reporte de ventas | | Reporte Productos | `/reports/products` | Top vendidos, ABC | | Reporte Inventario | `/reports/inventory` | Stock, valoracion | | Reporte Clientes | `/reports/customers` | Metricas clientes | | Reporte Caja | `/reports/cash` | Cortes, diferencias | #### Configuracion | Pagina | Ruta | Descripcion | |--------|------|-------------| | Sucursales | `/settings/branches` | Gestionar sucursales | | Cajas | `/settings/registers` | Cajas registradoras | | Usuarios | `/settings/users` | Usuarios y permisos | ### 3.2 Componentes Especificos ```typescript // Dashboard Components components/ ├── dashboard/ │ ├── KPICard.tsx │ ├── SalesChart.tsx │ ├── TopProductsChart.tsx │ ├── PaymentMethodPie.tsx │ ├── AlertsBadge.tsx │ └── QuickActions.tsx // Inventory Components ├── inventory/ │ ├── StockTable.tsx │ ├── BranchStockCard.tsx │ ├── TransferForm.tsx │ ├── TransferTimeline.tsx │ ├── AdjustmentForm.tsx │ └── ProductSelector.tsx // Customer Components ├── customers/ │ ├── CustomerForm.tsx │ ├── MembershipCard.tsx │ ├── PointsHistory.tsx │ ├── LoyaltyProgramForm.tsx │ └── LevelBadge.tsx // Pricing Components ├── pricing/ │ ├── PromotionForm.tsx │ ├── PromotionCard.tsx │ ├── CouponGenerator.tsx │ ├── CouponCard.tsx │ └── PricelistEditor.tsx // Reports Components ├── reports/ │ ├── DateRangePicker.tsx │ ├── BranchSelector.tsx │ ├── ExportButtons.tsx │ ├── ReportTable.tsx │ └── ChartWrapper.tsx ``` ### 3.3 Estado Global (Zustand) ```typescript // stores/index.ts export { useAuthStore } from './authStore'; export { useTenantStore } from './tenantStore'; export { useBranchStore } from './branchStore'; export { useUIStore } from './uiStore'; // stores/branchStore.ts interface BranchState { currentBranch: Branch | null; branches: Branch[]; setBranch: (branch: Branch) => void; loadBranches: () => Promise; } export const useBranchStore = create((set) => ({ currentBranch: null, branches: [], setBranch: (branch) => { set({ currentBranch: branch }); localStorage.setItem('currentBranchId', branch.id); api.setHeader('x-branch-id', branch.id); }, loadBranches: async () => { const branches = await api.branches.list(); set({ branches }); }, })); ``` --- ## 4. APP: POS (Punto de Venta - PWA) ### 4.1 Paginas | Pagina | Ruta | Descripcion | |--------|------|-------------| | Login | `/login` | Autenticacion | | Seleccion Caja | `/select-register` | Elegir caja | | Apertura | `/open-session` | Abrir caja | | Ventas | `/sales` | Pantalla principal POS | | Cobro | `/sales/checkout` | Pantalla de cobro | | Buscar Producto | `/sales/search` | Busqueda rapida | | Movimientos Caja | `/cash/movements` | Entradas/salidas | | Arqueo | `/cash/count` | Arqueo parcial | | Cierre | `/cash/close` | Corte de caja | | Historial | `/history` | Ventas del dia | | Detalle Venta | `/history/:id` | Ver venta | | Facturar | `/invoice/:orderId` | Generar factura | | Offline Queue | `/offline` | Cola de sincronizacion | ### 4.2 Layout POS ```tsx // layouts/POSLayout.tsx const POSLayout: React.FC = ({ children }) => { const { session, isOffline } = usePOSStore(); const { syncPending } = useOfflineStore(); return (
{/* Header */}
{session?.branch.name} Caja: {session?.register.name}
{/* Indicador Offline */} {isOffline && ( Modo Offline )} {/* Pendientes de sync */} {syncPending > 0 && ( {syncPending} pendientes )}
{/* Main content */}
{children}
{/* Footer con acciones rapidas */}
} label="Historial" to="/history" /> } label="Movimientos" to="/cash/movements" /> } label="Arqueo" to="/cash/count" />
); }; ``` ### 4.3 Pantalla Principal de Ventas ```tsx // pages/Sales.tsx const SalesPage: React.FC = () => { const { currentOrder, addLine, removeLine, setCustomer } = usePOSStore(); return (
{/* Panel izquierdo - Busqueda y productos */}
{/* Barra de busqueda */}
{/* Grid de categorias y productos */}
{/* Teclado numerico (opcional, para touch) */} {showNumpad && }
{/* Panel derecho - Carrito */}
{/* Cliente */}
{/* Lineas de orden */}
{/* Totales */}
{/* Botones de accion */}
); }; ``` ### 4.4 Pantalla de Cobro ```tsx // pages/Checkout.tsx const CheckoutPage: React.FC = () => { const { currentOrder, processPayment } = usePOSStore(); const [payments, setPayments] = useState([]); const [amountDue, setAmountDue] = useState(currentOrder?.total || 0); const handleAddPayment = (method: PaymentMethod, amount: number) => { setPayments([...payments, { method, amount }]); setAmountDue(prev => Math.max(0, prev - amount)); }; const handleComplete = async () => { await processPayment(payments); // Imprimir ticket await printer.printReceipt(currentOrder); // Abrir cajon await drawer.open(); // Regresar a ventas navigate('/sales'); }; return (
{/* Resumen de orden */}

Resumen

Pagos

Por pagar: 0 ? 'text-yellow-400' : 'text-green-400'}> ${amountDue.toFixed(2)}
{/* Metodos de pago */}

Metodo de Pago

} label="Efectivo" onClick={() => setSelectedMethod('cash')} selected={selectedMethod === 'cash'} /> } label="Tarjeta" onClick={() => setSelectedMethod('card')} selected={selectedMethod === 'card'} /> } label="Transferencia" onClick={() => setSelectedMethod('transfer')} selected={selectedMethod === 'transfer'} /> } label="Puntos" onClick={() => setSelectedMethod('points')} selected={selectedMethod === 'points'} disabled={!currentOrder?.customer} />
{/* Input de monto o teclado numerico */} {selectedMethod === 'cash' && ( handleAddPayment('cash', amount)} /> )} {selectedMethod === 'card' && ( handleAddPayment('card', amount, ref)} /> )} {selectedMethod === 'points' && ( handleAddPayment('points', discount)} /> )} {/* Boton finalizar */}
); }; ``` ### 4.5 Offline Support (PWA) ```typescript // offline/db.ts - IndexedDB con Dexie import Dexie, { Table } from 'dexie'; export interface OfflineOrder { offlineId: string; orderData: any; createdAt: Date; syncStatus: 'pending' | 'syncing' | 'synced' | 'error'; errorMessage?: string; } export interface CachedProduct { id: string; data: any; updatedAt: Date; } class RetailDatabase extends Dexie { offlineOrders!: Table; cachedProducts!: Table; cachedCustomers!: Table; constructor() { super('RetailPOS'); this.version(1).stores({ offlineOrders: 'offlineId, syncStatus, createdAt', cachedProducts: 'id, updatedAt', cachedCustomers: 'id, phone, updatedAt', }); } } export const db = new RetailDatabase(); // offline/sync.ts - Sincronizacion export class SyncService { private ws: WebSocket | null = null; private isOnline = navigator.onLine; constructor() { window.addEventListener('online', () => this.handleOnline()); window.addEventListener('offline', () => this.handleOffline()); } async syncPendingOrders(): Promise { const pendingOrders = await db.offlineOrders .where('syncStatus') .equals('pending') .toArray(); const results: SyncResult = { synced: [], failed: [] }; for (const order of pendingOrders) { try { await db.offlineOrders.update(order.offlineId, { syncStatus: 'syncing' }); const response = await api.pos.syncOrder(order); await db.offlineOrders.update(order.offlineId, { syncStatus: 'synced', serverOrderId: response.orderId, }); results.synced.push(order.offlineId); } catch (error) { await db.offlineOrders.update(order.offlineId, { syncStatus: 'error', errorMessage: error.message, }); results.failed.push({ offlineId: order.offlineId, error: error.message }); } } return results; } private handleOnline() { this.isOnline = true; this.syncPendingOrders(); this.connectWebSocket(); } private handleOffline() { this.isOnline = false; this.ws?.close(); } } // offline/queue.ts - Cola de operaciones offline export async function saveOfflineOrder(orderData: any): Promise { const offlineId = `offline_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; await db.offlineOrders.add({ offlineId, orderData, createdAt: new Date(), syncStatus: 'pending', }); return offlineId; } // sw.ts - Service Worker /// declare const self: ServiceWorkerGlobalScope; import { precacheAndRoute } from 'workbox-precaching'; import { registerRoute } from 'workbox-routing'; import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies'; // Precache static assets precacheAndRoute(self.__WB_MANIFEST); // Cache API responses registerRoute( ({ url }) => url.pathname.startsWith('/api/products'), new StaleWhileRevalidate({ cacheName: 'products-cache', }) ); // Cache images registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: 'images-cache', plugins: [ { cacheWillUpdate: async ({ response }) => { if (response.ok) return response; return null; }, }, ], }) ); // Background sync for orders self.addEventListener('sync', (event) => { if (event.tag === 'sync-orders') { event.waitUntil(syncOrders()); } }); async function syncOrders() { const db = await openDB(); const pendingOrders = await db.getAll('offlineOrders'); for (const order of pendingOrders) { try { await fetch('/api/pos/sync', { method: 'POST', body: JSON.stringify(order), headers: { 'Content-Type': 'application/json' }, }); await db.delete('offlineOrders', order.offlineId); } catch (e) { console.error('Sync failed:', e); } } } ``` ### 4.6 Hardware Integration ```typescript // hardware/printer.ts - Impresion ESC/POS export class ThermalPrinter { private encoder: EscPosEncoder; constructor() { this.encoder = new EscPosEncoder(); } async printReceipt(order: POSOrder, config: PrintConfig): Promise { const receiptData = this.encoder .initialize() .align('center') .bold(true) .text(config.storeName) .bold(false) .text(config.storeAddress) .text(`Tel: ${config.storePhone}`) .text(`RFC: ${config.storeRFC}`) .newline() .align('left') .text(`Ticket: ${order.orderNumber}`) .text(`Fecha: ${formatDate(order.orderDate)}`) .text(`Cajero: ${order.cashierName}`) .text(order.customer ? `Cliente: ${order.customer.name}` : '') .newline() .text('='.repeat(32)) .newline(); // Lineas for (const line of order.lines) { receiptData .text(`${line.quantity} x ${line.productName}`) .align('right') .text(`$${line.total.toFixed(2)}`) .align('left'); } receiptData .newline() .text('-'.repeat(32)) .align('right') .text(`Subtotal: $${order.subtotal.toFixed(2)}`) .text(`IVA: $${order.taxAmount.toFixed(2)}`) .bold(true) .text(`TOTAL: $${order.total.toFixed(2)}`) .bold(false) .newline(); // Pagos for (const payment of order.payments) { receiptData.text(`${payment.method}: $${payment.amount.toFixed(2)}`); } if (order.changeAmount > 0) { receiptData.text(`Cambio: $${order.changeAmount.toFixed(2)}`); } receiptData .newline() .align('center') .text('Gracias por su compra!') .qrcode(order.ticketUrl, 1, 4, 'l') .cut(); // Enviar a impresora const data = receiptData.encode(); await this.send(data); } private async send(data: Uint8Array): Promise { // USB if (this.connection === 'usb') { const device = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x04b8 }] // Epson }); await device.open(); await device.transferOut(1, data); } // Bluetooth if (this.connection === 'bluetooth') { const device = await navigator.bluetooth.requestDevice({ filters: [{ services: ['000018f0-0000-1000-8000-00805f9b34fb'] }] }); const server = await device.gatt.connect(); const service = await server.getPrimaryService('000018f0-0000-1000-8000-00805f9b34fb'); const characteristic = await service.getCharacteristic('00002af1-0000-1000-8000-00805f9b34fb'); await characteristic.writeValue(data); } // Network if (this.connection === 'network') { await fetch(`http://${this.printerIp}/print`, { method: 'POST', body: data, }); } } } // hardware/scanner.ts - Lector de codigo de barras export class BarcodeScanner { private buffer = ''; private timeoutId: number | null = null; constructor(private onScan: (barcode: string) => void) { document.addEventListener('keydown', this.handleKeyDown.bind(this)); } private handleKeyDown(e: KeyboardEvent) { // Los scanners envian caracteres muy rapido seguidos de Enter if (e.key === 'Enter' && this.buffer.length > 0) { this.onScan(this.buffer); this.buffer = ''; e.preventDefault(); return; } // Acumular caracteres if (e.key.length === 1) { this.buffer += e.key; // Limpiar buffer despues de timeout (input manual) if (this.timeoutId) clearTimeout(this.timeoutId); this.timeoutId = window.setTimeout(() => { this.buffer = ''; }, 100); } } destroy() { document.removeEventListener('keydown', this.handleKeyDown); } } // hardware/drawer.ts - Cajon de dinero export class CashDrawer { async open(): Promise { // Comando ESC/POS para abrir cajon const openCommand = new Uint8Array([0x1B, 0x70, 0x00, 0x19, 0xFA]); // Enviar via impresora (el cajon se conecta a la impresora) await printer.sendRaw(openCommand); } } ``` ### 4.7 Estado POS (Zustand) ```typescript // store/posStore.ts interface POSState { session: POSSession | null; currentOrder: POSOrder | null; heldOrders: POSOrder[]; isOffline: boolean; // Session actions openSession: (dto: OpenSessionDto) => Promise; closeSession: (dto: CloseSessionDto) => Promise; // Order actions createOrder: () => void; addLine: (product: Product, quantity: number) => void; updateLineQty: (lineId: string, quantity: number) => void; removeLine: (lineId: string) => void; setCustomer: (customer: Customer | null) => void; applyDiscount: (type: 'percent' | 'amount', value: number) => void; applyCoupon: (code: string) => Promise; holdOrder: () => void; recallOrder: (orderId: string) => void; cancelOrder: () => void; // Payment processPayment: (payments: Payment[]) => Promise; } export const usePOSStore = create((set, get) => ({ session: null, currentOrder: null, heldOrders: [], isOffline: !navigator.onLine, addLine: (product, quantity) => { const { currentOrder } = get(); if (!currentOrder) return; const existingLine = currentOrder.lines.find(l => l.productId === product.id); if (existingLine) { // Incrementar cantidad set(state => ({ currentOrder: { ...state.currentOrder!, lines: state.currentOrder!.lines.map(l => l.id === existingLine.id ? { ...l, quantity: l.quantity + quantity, total: (l.quantity + quantity) * l.unitPrice } : l ), }, })); } else { // Nueva linea const newLine: POSOrderLine = { id: crypto.randomUUID(), productId: product.id, productName: product.name, quantity, unitPrice: product.salePrice, discountPercent: 0, discountAmount: 0, taxAmount: product.salePrice * quantity * 0.16, // IVA total: product.salePrice * quantity * 1.16, }; set(state => ({ currentOrder: { ...state.currentOrder!, lines: [...state.currentOrder!.lines, newLine], }, })); } // Recalcular totales get().recalculateTotals(); }, processPayment: async (payments) => { const { currentOrder, session, isOffline } = get(); if (!currentOrder || !session) throw new Error('No order'); const orderData = { ...currentOrder, payments, status: 'done', }; if (isOffline) { // Guardar offline const offlineId = await saveOfflineOrder(orderData); orderData.offlineId = offlineId; // Limpiar orden actual set({ currentOrder: null }); get().createOrder(); return orderData; } // Online - enviar al servidor const confirmedOrder = await api.pos.confirmOrder(session.id, orderData); set({ currentOrder: null }); get().createOrder(); return confirmedOrder; }, recalculateTotals: () => { set(state => { if (!state.currentOrder) return state; const subtotal = state.currentOrder.lines.reduce((sum, l) => sum + (l.unitPrice * l.quantity), 0); const discountAmount = state.currentOrder.lines.reduce((sum, l) => sum + l.discountAmount, 0); const taxAmount = state.currentOrder.lines.reduce((sum, l) => sum + l.taxAmount, 0); const total = subtotal - discountAmount + taxAmount; return { currentOrder: { ...state.currentOrder, subtotal, discountAmount, taxAmount, total, }, }; }); }, })); ``` --- ## 5. APP: STOREFRONT (E-commerce) ### 5.1 Paginas | Pagina | Ruta | Descripcion | |--------|------|-------------| | Home | `/` | Landing con destacados | | Catalogo | `/products` | Lista de productos | | Producto | `/products/:id` | Detalle de producto | | Categoria | `/category/:slug` | Productos por categoria | | Busqueda | `/search` | Resultados de busqueda | | Carrito | `/cart` | Ver carrito | | Checkout | `/checkout` | Proceso de compra | | Confirmacion | `/checkout/confirm` | Confirmacion de orden | | Login | `/login` | Iniciar sesion | | Registro | `/register` | Crear cuenta | | Mi Cuenta | `/account` | Dashboard cliente | | Mis Pedidos | `/account/orders` | Lista de pedidos | | Detalle Pedido | `/account/orders/:id` | Ver pedido | | Direcciones | `/account/addresses` | Gestionar direcciones | | Mis Puntos | `/account/loyalty` | Puntos de lealtad | ### 5.2 Componentes E-commerce ```typescript components/ ├── storefront/ │ ├── Navbar.tsx │ ├── Footer.tsx │ ├── SearchBar.tsx │ ├── CategoryNav.tsx │ ├── ProductCard.tsx │ ├── ProductGrid.tsx │ ├── ProductGallery.tsx │ ├── PriceDisplay.tsx # Muestra precio con promociones │ ├── StockIndicator.tsx │ ├── AddToCartButton.tsx │ ├── CartDrawer.tsx │ ├── CartItem.tsx │ ├── CartSummary.tsx │ ├── CheckoutSteps.tsx │ ├── AddressForm.tsx │ ├── ShippingOptions.tsx │ ├── PaymentForm.tsx │ ├── OrderSummary.tsx │ ├── PromoBanner.tsx │ └── LoyaltyWidget.tsx ``` ### 5.3 Layout Storefront ```tsx // layouts/StoreLayout.tsx const StoreLayout: React.FC = ({ children }) => { const { itemCount } = useCartStore(); const { customer } = useAuthStore(); return (
{/* Promo banner */} {/* Header */}
{/* Categoria nav */}
{/* Main */}
{children}
{/* Footer */}
{/* Cart drawer (side panel) */}
); }; ``` ### 5.4 Pagina de Producto ```tsx // pages/ProductDetail.tsx const ProductDetailPage: React.FC = () => { const { id } = useParams(); const { data: product, isLoading } = useProduct(id); const { addItem } = useCartStore(); const [quantity, setQuantity] = useState(1); const [selectedVariant, setSelectedVariant] = useState(null); if (isLoading) return ; return (
{/* Galeria de imagenes */} {/* Informacion */}

{product.name}

{/* Rating */}
({product.reviewCount} resenas)
{/* Precio */}
{/* Stock */} {/* Variantes */} {product.variants?.length > 0 && ( )} {/* Cantidad */}
{/* Botones */}
{/* Descripcion */}

Descripcion

{product.description}

{/* Especificaciones */}

Especificaciones

{product.attributes?.map(attr => (
{attr.name}
{attr.value}
))}
{/* Productos relacionados */}

Productos relacionados

); }; ``` ### 5.5 Checkout Flow ```tsx // pages/Checkout.tsx const CheckoutPage: React.FC = () => { const [step, setStep] = useState<'address' | 'shipping' | 'payment' | 'review'>('address'); const { items, subtotal } = useCartStore(); const { customer } = useAuthStore(); const [checkoutData, setCheckoutData] = useState({}); const steps = [ { id: 'address', label: 'Direccion', icon: MapPin }, { id: 'shipping', label: 'Envio', icon: Truck }, { id: 'payment', label: 'Pago', icon: CreditCard }, { id: 'review', label: 'Confirmar', icon: CheckCircle }, ]; return (
{/* Progress steps */}
{/* Main content */}
{step === 'address' && ( setCheckoutData({ ...checkoutData, addressId })} onNewAddress={(address) => {/* save */}} onContinue={() => setStep('shipping')} /> )} {step === 'shipping' && ( setCheckoutData({ ...checkoutData, shippingRateId: rateId })} onBack={() => setStep('address')} onContinue={() => setStep('payment')} /> )} {step === 'payment' && ( setCheckoutData({ ...checkoutData, paymentMethod: method })} onBack={() => setStep('shipping')} onContinue={() => setStep('review')} /> )} {step === 'review' && ( setStep('payment')} onConfirm={handleConfirm} /> )}
{/* Order summary sidebar */}

Resumen del pedido

{/* Items */}
{items.map(item => (

{item.product.name}

Cant: {item.quantity}

${item.total.toFixed(2)}

))}
{/* Totals */}
Subtotal ${subtotal.toFixed(2)}
{checkoutData.shippingRate && (
Envio ${checkoutData.shippingRate.price.toFixed(2)}
)}
Total ${total.toFixed(2)}
); }; ``` --- ## 6. COMPONENTES COMPARTIDOS ### 6.1 UI Components Library ```typescript // packages/ui-components/src/components/ // Button.tsx export interface ButtonProps extends React.ButtonHTMLAttributes { variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success'; size?: 'sm' | 'md' | 'lg' | 'xl'; loading?: boolean; icon?: React.ReactNode; } // Input.tsx export interface InputProps extends React.InputHTMLAttributes { label?: string; error?: string; helperText?: string; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; } // Table.tsx export interface TableProps { data: T[]; columns: Column[]; loading?: boolean; pagination?: PaginationConfig; sorting?: SortingConfig; selection?: SelectionConfig; emptyState?: React.ReactNode; } // Modal.tsx export interface ModalProps { open: boolean; onClose: () => void; title?: string; description?: string; size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; children: React.ReactNode; footer?: React.ReactNode; } // Toast.tsx (con Zustand store) export interface ToastProps { id: string; type: 'success' | 'error' | 'warning' | 'info'; title: string; description?: string; duration?: number; } // Select.tsx export interface SelectProps { options: T[]; value: T | null; onChange: (value: T) => void; getLabel: (item: T) => string; getValue: (item: T) => string; placeholder?: string; searchable?: boolean; loading?: boolean; error?: string; } // DatePicker.tsx export interface DatePickerProps { value: Date | null; onChange: (date: Date | null) => void; minDate?: Date; maxDate?: Date; format?: string; placeholder?: string; } // DateRangePicker.tsx export interface DateRangePickerProps { value: { from: Date; to: Date } | null; onChange: (range: { from: Date; to: Date } | null) => void; presets?: DateRangePreset[]; } ``` ### 6.2 Hooks Compartidos ```typescript // packages/hooks/src/ // useAuth.ts export function useAuth() { const { user, token, login, logout, isAuthenticated } = useAuthStore(); const loginMutation = useMutation({ mutationFn: api.auth.login, onSuccess: (data) => { login(data.user, data.token); }, }); return { user, isAuthenticated, login: loginMutation.mutate, logout, isLoading: loginMutation.isLoading, }; } // usePagination.ts export function usePagination( fetcher: (params: PaginationParams) => Promise>, initialParams?: Partial ) { const [page, setPage] = useState(initialParams?.page || 1); const [limit, setLimit] = useState(initialParams?.limit || 20); const query = useQuery({ queryKey: ['paginated', fetcher.name, page, limit], queryFn: () => fetcher({ page, limit }), }); return { data: query.data?.items || [], total: query.data?.total || 0, page, limit, totalPages: Math.ceil((query.data?.total || 0) / limit), setPage, setLimit, isLoading: query.isLoading, refetch: query.refetch, }; } // useDebounce.ts export function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } // useOffline.ts export function useOffline() { const [isOffline, setIsOffline] = useState(!navigator.onLine); useEffect(() => { const handleOnline = () => setIsOffline(false); const handleOffline = () => setIsOffline(true); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); return isOffline; } ``` ### 6.3 API Client ```typescript // packages/api-client/src/client.ts import axios, { AxiosInstance } from 'axios'; class ApiClient { private client: AxiosInstance; private tenantId: string | null = null; private branchId: string | null = null; constructor() { this.client = axios.create({ baseURL: import.meta.env.VITE_API_URL, }); // Request interceptor this.client.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } if (this.tenantId) { config.headers['x-tenant-id'] = this.tenantId; } if (this.branchId) { config.headers['x-branch-id'] = this.branchId; } return config; }); // Response interceptor this.client.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // Token expired, logout localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); } ); } setTenant(tenantId: string) { this.tenantId = tenantId; } setBranch(branchId: string) { this.branchId = branchId; } // Auth auth = { login: (email: string, password: string) => this.client.post('/auth/login', { email, password }).then(r => r.data), register: (data: RegisterDto) => this.client.post('/auth/register', data).then(r => r.data), me: () => this.client.get('/auth/me').then(r => r.data), }; // Products products = { list: (params?: ProductFilters) => this.client.get('/products', { params }).then(r => r.data), get: (id: string) => this.client.get(`/products/${id}`).then(r => r.data), create: (data: CreateProductDto) => this.client.post('/products', data).then(r => r.data), update: (id: string, data: UpdateProductDto) => this.client.put(`/products/${id}`, data).then(r => r.data), }; // POS pos = { openSession: (data: OpenSessionDto) => this.client.post('/pos/sessions/open', data).then(r => r.data), getSession: () => this.client.get('/pos/sessions/active').then(r => r.data), closeSession: (data: CloseSessionDto) => this.client.post('/pos/sessions/close', data).then(r => r.data), createOrder: (sessionId: string) => this.client.post(`/pos/sessions/${sessionId}/orders`).then(r => r.data), confirmOrder: (orderId: string, data: ConfirmOrderDto) => this.client.post(`/pos/orders/${orderId}/confirm`, data).then(r => r.data), syncOrder: (data: OfflineOrderDto) => this.client.post('/pos/sync', data).then(r => r.data), }; // ... mas endpoints } export const api = new ApiClient(); ``` --- ## 7. CHECKLIST DE IMPLEMENTACION ### Packages Compartidos - [ ] @retail/ui-components - [ ] Button, Input, Select - [ ] Table, Modal, Toast - [ ] Card, Badge, Spinner - [ ] DatePicker, DateRangePicker - [ ] @retail/api-client - [ ] @retail/hooks ### App Backoffice - [ ] Layout y navegacion - [ ] Dashboard - [ ] Modulo Inventario - [ ] Modulo Productos - [ ] Modulo Clientes - [ ] Modulo Precios - [ ] Modulo Compras - [ ] Modulo E-commerce Admin - [ ] Modulo Facturacion - [ ] Modulo Reportes - [ ] Modulo Configuracion ### App POS - [ ] Layout POS - [ ] Login y seleccion de caja - [ ] Apertura de caja - [ ] Pantalla de ventas - [ ] Pantalla de cobro - [ ] Movimientos de caja - [ ] Cierre de caja - [ ] PWA y Service Worker - [ ] IndexedDB y offline queue - [ ] Sincronizacion - [ ] Integracion impresora - [ ] Integracion scanner ### App Storefront - [ ] Layout tienda - [ ] Home - [ ] Catalogo y busqueda - [ ] Detalle de producto - [ ] Carrito - [ ] Checkout flow - [ ] Area de cliente - [ ] Responsive design --- **Estado:** PLAN COMPLETO **Total paginas:** ~65 **Total componentes:** ~80