diff --git a/projects/erp-suite/apps/products/pos-micro/docs/ANALISIS-GAPS.md b/projects/erp-suite/apps/products/pos-micro/docs/ANALISIS-GAPS.md new file mode 100644 index 0000000..c8a591e --- /dev/null +++ b/projects/erp-suite/apps/products/pos-micro/docs/ANALISIS-GAPS.md @@ -0,0 +1,192 @@ +# POS Micro - Analisis de Gaps vs ERP Core y Odoo POS + +## Resumen Ejecutivo + +Este documento analiza las diferencias entre POS Micro (producto MVP), ERP Core (arquitectura base) y Odoo POS (referencia de mercado) para identificar gaps y oportunidades de mejora. + +## 1. Comparativa de Arquitectura + +### ERP Core (Express + TypeScript) +- Framework: Express.js con TypeScript +- ORM: Raw PostgreSQL queries con Pool +- Autenticacion: JWT + Bcrypt + RBAC completo +- Validacion: Zod schemas +- Multi-tenancy: Schema isolation con RLS +- Modulos: 13 modulos completos (auth, users, companies, partners, inventory, products, warehouses, pickings, lots, financial, purchases, sales, crm, hr, projects, system) + +### POS Micro (NestJS + TypeORM) +- Framework: NestJS con TypeScript +- ORM: TypeORM +- Autenticacion: JWT + Bcrypt + PIN simplificado +- Validacion: class-validator decorators +- Multi-tenancy: tenant_id column (simplificado) +- Modulos: 5 modulos minimos (auth, products, categories, sales, payments) + +### Odoo POS (Python + ORM) +- Framework: Odoo 18 (Python) +- ORM: Odoo ORM +- Modulos POS: 40+ tablas/modelos interconectados +- Funcionalidades avanzadas: Restaurant, Loyalty, IoT, Multiple payment terminals + +## 2. Gaps Identificados + +### 2.1 Seguridad + +| Feature | ERP Core | POS Micro | Odoo POS | Gap | +|---------|----------|-----------|----------|-----| +| RBAC completo | ✅ | ❌ | ✅ | POS Micro solo tiene owner/cashier | +| Rate limiting | ❌ | ❌ | ✅ | Ninguno implementa | +| Audit logs | ✅ | ❌ | ✅ | POS Micro no tiene | +| Session management | ✅ | ❌ | ✅ | POS Micro no maneja sesiones de caja | + +### 2.2 Funcionalidad POS + +| Feature | ERP Core | POS Micro | Odoo POS | Gap | +|---------|----------|-----------|----------|-----| +| Sesiones de caja | N/A | ❌ | ✅ | Critico para control de caja | +| Cierre de caja | N/A | ❌ | ✅ | Critico para contabilidad | +| Arqueo de caja | N/A | ❌ | ✅ | Control de efectivo | +| Devoluciones | N/A | Parcial | ✅ | Solo cancelacion same-day | +| Descuentos globales | N/A | ❌ | ✅ | Solo descuento por linea | +| Impuestos configurables | N/A | Hardcoded 16% | ✅ | No flexible | +| Multi-tarifa | N/A | ❌ | ✅ | Un precio por producto | +| Combos/Kits | N/A | ❌ | ✅ | No soportado | +| Variantes producto | N/A | ❌ | ✅ | No soportado | + +### 2.3 Integraciones + +| Feature | ERP Core | POS Micro | Odoo POS | Gap | +|---------|----------|-----------|----------|-----| +| Inventario | ✅ Completo | Basico | ✅ Completo | Sin lotes/series | +| Contabilidad | ✅ | ❌ | ✅ | No genera asientos | +| Facturacion | ❌ | ❌ | ✅ | No hay CFDI | +| WhatsApp | ❌ | Tabla vacia | ❌ | Preparado pero no implementado | +| Impresoras | ❌ | ❌ | ✅ | No hay soporte | +| Terminal pago | ❌ | ❌ | ✅ | No integrado | + +### 2.4 Reportes + +| Feature | ERP Core | POS Micro | Odoo POS | Gap | +|---------|----------|-----------|----------|-----| +| Ventas del dia | ❌ | ✅ Basico | ✅ Completo | Solo totales | +| Por vendedor | ❌ | ❌ | ✅ | No soportado | +| Por producto | ❌ | ❌ | ✅ | No soportado | +| Por hora | ❌ | ❌ | ✅ | No soportado | +| Margen | ❌ | ❌ | ✅ | No soportado | +| Exportable | ❌ | ❌ | ✅ | No hay exports | + +## 3. Correcciones Aplicadas + +### 3.1 Frontend - Login +- **Problema**: Frontend enviaba `businessName` donde backend esperaba `phone` +- **Solucion**: Actualizado LoginPage para usar `phone` y agregar `ownerName` para registro + +### 3.2 Frontend - Endpoints +- **Problema**: Endpoints no coincidian con backend +- **Correcciones**: + - `/products/favorites` → `/products?isFavorite=true` + - `/products/{id}/favorite` → `/products/{id}/toggle-favorite` + - `/sales?date=` → `/sales/recent?limit=50` + - `PATCH /sales/{id}/cancel` → `POST /sales/{id}/cancel` + - `/sales/summary/{date}` → `/sales/today` + +### 3.3 Frontend - Tipos TypeScript +- **Problema**: Tipos no alineados con entidades backend +- **Correcciones**: + - `currentStock` → `stockQuantity` + - `minStock` → `lowStockAlert` + - `discount` → `discountAmount/discountPercent` + - `tax` → `taxAmount` + - `change` → `changeAmount` + - Agregado `SubscriptionStatus` type + +### 3.4 Frontend - Cart Store +- **Problema**: Calculo de descuentos no funcionaba +- **Solucion**: Actualizado para aplicar `discountPercent` correctamente al subtotal + +## 4. Gaps Pendientes (Roadmap) + +### Fase 2 - Funcionalidad Core +1. **Sesiones de caja**: Apertura, cierre, arqueo +2. **Devoluciones completas**: No solo same-day +3. **Descuentos globales**: Por orden, no solo por linea +4. **Impuestos configurables**: Permitir diferentes tasas + +### Fase 3 - Integraciones +1. **WhatsApp Business**: Tickets por WhatsApp +2. **Facturacion CFDI**: Integracion con PAC +3. **Impresoras termicas**: Soporte ESC/POS +4. **Terminales de pago**: Integracion basica + +### Fase 4 - Reportes +1. **Reporte por producto**: Top ventas, margenes +2. **Reporte por periodo**: Semanal, mensual +3. **Exportacion**: CSV, PDF + +## 5. Patrones de ERP Core a Adoptar + +### 5.1 Base Service Pattern +```typescript +// ERP Core tiene un servicio base reutilizable +abstract class BaseService { + // findAll con paginacion, busqueda y filtros + // findById, findByIdOrFail + // exists, softDelete, hardDelete + // withTransaction +} +``` +**Recomendacion**: Implementar en POS Micro para consistencia + +### 5.2 Error Handling +```typescript +// ERP Core usa clases de error personalizadas +class ValidationError extends AppError { } +class NotFoundError extends AppError { } +class ConflictError extends AppError { } +``` +**Recomendacion**: Adoptar mismo patron en NestJS + +### 5.3 Audit Fields +```typescript +// ERP Core tiene campos de auditoria consistentes +created_at, created_by +updated_at, updated_by +deleted_at, deleted_by +``` +**Recomendacion**: Agregar `created_by`, `updated_by` a todas las tablas + +## 6. Funcionalidades de Odoo a Considerar + +### 6.1 Session Management (Critico) +- Apertura de sesion con saldo inicial +- Estado: opening_control → opened → closing_control → closed +- Validacion de diferencias de caja +- Asientos contables automaticos + +### 6.2 Loyalty Programs (Futuro) +- Puntos por compra +- Recompensas configurables +- Tarjetas de cliente +- Cupones/Promociones + +### 6.3 Restaurant Mode (Futuro) +- Mesas/pisos +- Ordenes abiertas +- Impresion a cocina +- Division de cuentas + +## 7. Conclusion + +POS Micro esta correctamente posicionado como un MVP minimo para el mercado mexicano informal (vendedores ambulantes, tienditas, fondas). Las correcciones aplicadas resuelven los problemas criticos de comunicacion frontend-backend. + +Los gaps identificados son caracteristicas para fases futuras, no bloquean el lanzamiento del MVP que cumple con: +- ✅ Registro/Login simple con PIN +- ✅ Catalogo de productos (max 500) +- ✅ Ventas rapidas (max 1000/mes) +- ✅ Multiples formas de pago +- ✅ Reportes basicos del dia +- ✅ Offline-first PWA + +--- +*Documento generado: 2025-12-08* +*Version: 1.0* diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CheckoutModal.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CheckoutModal.tsx index 082d7f5..9e1da78 100644 --- a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CheckoutModal.tsx +++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CheckoutModal.tsx @@ -36,15 +36,15 @@ export function CheckoutModal({ isOpen, onClose, onSuccess }: CheckoutModalProps if (!paymentMethod) return; try { + // Payload that matches backend CreateSaleDto const sale = await createSale.mutateAsync({ items: items.map((item) => ({ productId: item.product.id, quantity: item.quantity, - unitPrice: item.unitPrice, - discount: item.discount, + discountPercent: item.discountPercent || 0, })), paymentMethodId: paymentMethod.id, - amountReceived: paymentMethod.type === 'cash' ? amountReceived : undefined, + amountReceived: paymentMethod.type === 'cash' ? amountReceived : total, }); onSuccess(sale); diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/ProductCard.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/ProductCard.tsx index 08dbac4..0d29f90 100644 --- a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/ProductCard.tsx +++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/ProductCard.tsx @@ -9,8 +9,8 @@ interface ProductCardProps { } export function ProductCard({ product, onSelect, onToggleFavorite }: ProductCardProps) { - const isLowStock = product.trackStock && product.currentStock <= product.minStock; - const isOutOfStock = product.trackStock && product.currentStock <= 0; + const isLowStock = product.trackStock && product.stockQuantity <= product.lowStockAlert; + const isOutOfStock = product.trackStock && product.stockQuantity <= 0; return (
- {isOutOfStock ? 'Agotado' : `${product.currentStock} disponibles`} + {isOutOfStock ? 'Agotado' : `${product.stockQuantity} disponibles`}

)}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useProducts.ts b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useProducts.ts index e80201f..ff3b94f 100644 --- a/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useProducts.ts +++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useProducts.ts @@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '@/services/api'; import type { Product, Category } from '@/types'; -// Products +// Products - matches backend ProductsController export function useProducts(categoryId?: string) { return useQuery({ queryKey: ['products', categoryId], @@ -21,7 +21,8 @@ export function useFavoriteProducts() { return useQuery({ queryKey: ['products', 'favorites'], queryFn: async () => { - const { data } = await api.get('/products/favorites'); + // Backend: GET /products?isFavorite=true + const { data } = await api.get('/products?isFavorite=true&isActive=true'); return data; }, }); @@ -32,6 +33,7 @@ export function useSearchProduct() { return useMutation({ mutationFn: async (barcode: string) => { + // Backend: GET /products/barcode/:barcode const { data } = await api.get(`/products/barcode/${barcode}`); return data; }, @@ -46,7 +48,8 @@ export function useToggleFavorite() { return useMutation({ mutationFn: async (productId: string) => { - const { data } = await api.patch(`/products/${productId}/favorite`); + // Backend: PATCH /products/:id/toggle-favorite + const { data } = await api.patch(`/products/${productId}/toggle-favorite`); return data; }, onSuccess: () => { @@ -55,7 +58,7 @@ export function useToggleFavorite() { }); } -// Categories +// Categories - matches backend CategoriesController export function useCategories() { return useQuery({ queryKey: ['categories'], diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useSales.ts b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useSales.ts index 0f830b3..7d918f7 100644 --- a/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useSales.ts +++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useSales.ts @@ -1,26 +1,37 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import api from '@/services/api'; import { useCartStore } from '@/store/cart'; -import type { Sale, DailySummary } from '@/types'; +import type { Sale } from '@/types'; +// DTO que coincide con backend CreateSaleDto interface CreateSaleDto { items: { productId: string; quantity: number; - unitPrice: number; - discount: number; + discountPercent?: number; }[]; paymentMethodId: string; - amountReceived?: number; + amountReceived: number; + customerName?: string; + customerPhone?: string; notes?: string; } +// Response del backend para summary +interface TodaySummary { + totalSales: number; + totalRevenue: number; + totalTax: number; + avgTicket: number; +} + export function useCreateSale() { const queryClient = useQueryClient(); const clearCart = useCartStore((state) => state.clear); return useMutation({ mutationFn: async (sale: CreateSaleDto) => { + // Backend: POST /sales const { data } = await api.post('/sales', sale); return data; }, @@ -37,20 +48,19 @@ export function useTodaySales() { return useQuery({ queryKey: ['sales', 'today'], queryFn: async () => { - const today = new Date().toISOString().split('T')[0]; - const { data } = await api.get(`/sales?date=${today}`); + // Backend: GET /sales/recent?limit=50 + const { data } = await api.get('/sales/recent?limit=50'); return data; }, }); } -export function useDailySummary(date?: string) { - const queryDate = date || new Date().toISOString().split('T')[0]; - +export function useDailySummary() { return useQuery({ - queryKey: ['daily-summary', queryDate], + queryKey: ['daily-summary'], queryFn: async () => { - const { data } = await api.get(`/sales/summary/${queryDate}`); + // Backend: GET /sales/today + const { data } = await api.get('/sales/today'); return data; }, }); @@ -60,8 +70,9 @@ export function useCancelSale() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (saleId: string) => { - const { data } = await api.patch(`/sales/${saleId}/cancel`); + mutationFn: async ({ saleId, reason }: { saleId: string; reason?: string }) => { + // Backend: POST /sales/:id/cancel + const { data } = await api.post(`/sales/${saleId}/cancel`, { reason }); return data; }, onSuccess: () => { diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/LoginPage.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/LoginPage.tsx index 86406fb..a4ed8cf 100644 --- a/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/LoginPage.tsx +++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/LoginPage.tsx @@ -6,13 +6,18 @@ import { useAuthStore } from '@/store/auth'; import type { AuthResponse } from '@/types'; export function LoginPage() { - const [businessName, setBusinessName] = useState(''); + // Campos compartidos + const [phone, setPhone] = useState(''); const [pin, setPin] = useState(''); const [showPin, setShowPin] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const [isRegister, setIsRegister] = useState(false); + // Campos solo para registro + const [businessName, setBusinessName] = useState(''); + const [ownerName, setOwnerName] = useState(''); + const login = useAuthStore((state) => state.login); const navigate = useNavigate(); @@ -24,8 +29,8 @@ export function LoginPage() { try { const endpoint = isRegister ? '/auth/register' : '/auth/login'; const payload = isRegister - ? { businessName, pin } - : { businessName, pin }; + ? { businessName, ownerName, phone, pin } + : { phone, pin }; const { data } = await api.post(endpoint, payload); @@ -36,7 +41,7 @@ export function LoginPage() { err instanceof Error ? err.message : (err as { response?: { data?: { message?: string } } })?.response?.data?.message || - (isRegister ? 'Error al registrar negocio' : 'PIN o negocio incorrecto'); + (isRegister ? 'Error al registrar negocio' : 'Telefono o PIN incorrecto'); setError(errorMessage); } finally { setIsLoading(false); @@ -61,23 +66,59 @@ export function LoginPage() {
+ {isRegister && ( + <> +
+ + setBusinessName(e.target.value)} + required + /> +
+ +
+ + setOwnerName(e.target.value)} + required + /> +
+ + )} +
setBusinessName(e.target.value)} + placeholder="5512345678" + value={phone} + onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 10))} required + minLength={10} + maxLength={10} + inputMode="numeric" + pattern="[0-9]*" />
) : (
- {/* Summary Cards */} + {/* Summary Cards - aligned with backend TodaySummary */}
} - label="Ventas totales" - value={`$${summary?.totalSales.toFixed(2) || '0.00'}`} + icon={} + label="Ingresos" + value={`$${summary?.totalRevenue?.toFixed(2) || '0.00'}`} bgColor="bg-green-50" /> } - label="Transacciones" - value={summary?.salesCount.toString() || '0'} + label="Ventas" + value={summary?.totalSales?.toString() || '0'} bgColor="bg-blue-50" /> } - label="Canceladas" - value={summary?.cancelledCount.toString() || '0'} - bgColor="bg-red-50" + icon={} + label="IVA recaudado" + value={`$${summary?.totalTax?.toFixed(2) || '0.00'}`} + bgColor="bg-yellow-50" /> } - label="Promedio" - value={`$${summary?.salesCount ? (summary.totalSales / summary.salesCount).toFixed(2) : '0.00'}`} + label="Ticket promedio" + value={`$${summary?.avgTicket?.toFixed(2) || '0.00'}`} bgColor="bg-purple-50" />
- {/* Payment breakdown */} -
-

Por forma de pago

-
- } - label="Efectivo" - amount={summary?.cashSales || 0} - /> - } - label="Tarjeta" - amount={summary?.cardSales || 0} - /> - } - label="Transferencia" - amount={summary?.transferSales || 0} - /> -
-
- {/* Recent sales */}
@@ -135,23 +113,3 @@ function SummaryCard({ icon, label, value, bgColor }: SummaryCardProps) {
); } - -interface PaymentRowProps { - icon: React.ReactNode; - label: string; - amount: number; -} - -function PaymentRow({ icon, label, amount }: PaymentRowProps) { - return ( -
-
-
- {icon} -
- {label} -
- ${amount.toFixed(2)} -
- ); -} diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/store/cart.ts b/projects/erp-suite/apps/products/pos-micro/frontend/src/store/cart.ts index f706fcc..b7b7c9a 100644 --- a/projects/erp-suite/apps/products/pos-micro/frontend/src/store/cart.ts +++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/store/cart.ts @@ -9,8 +9,8 @@ interface CartState { // Computed subtotal: number; - discount: number; - tax: number; + discountAmount: number; + taxAmount: number; total: number; itemCount: number; @@ -18,7 +18,7 @@ interface CartState { addItem: (product: Product, quantity?: number) => void; removeItem: (productId: string) => void; updateQuantity: (productId: string, quantity: number) => void; - applyItemDiscount: (productId: string, discount: number) => void; + applyItemDiscount: (productId: string, discountPercent: number) => void; setPaymentMethod: (method: PaymentMethod) => void; setAmountReceived: (amount: number) => void; setNotes: (notes: string) => void; @@ -26,13 +26,20 @@ interface CartState { } const calculateTotals = (items: CartItem[]) => { + // Subtotal = sum of line subtotals (already with discount applied) const subtotal = items.reduce((sum, item) => sum + item.subtotal, 0); - const discount = items.reduce((sum, item) => sum + (item.unitPrice * item.quantity * item.discount / 100), 0); - const tax = 0; // No tax in simplified version - const total = subtotal - discount + tax; + // Discount amount = original price - discounted price + const discountAmount = items.reduce((sum, item) => { + const originalPrice = item.unitPrice * item.quantity; + return sum + (originalPrice - item.subtotal); + }, 0); + // Tax included in price (16% IVA) + const taxRate = 0.16; + const taxAmount = subtotal - (subtotal / (1 + taxRate)); + const total = subtotal; const itemCount = items.reduce((sum, item) => sum + item.quantity, 0); - return { subtotal, discount, tax, total, itemCount }; + return { subtotal, discountAmount, taxAmount, total, itemCount }; }; export const useCartStore = create((set, get) => ({ @@ -41,8 +48,8 @@ export const useCartStore = create((set, get) => ({ amountReceived: 0, notes: '', subtotal: 0, - discount: 0, - tax: 0, + discountAmount: 0, + taxAmount: 0, total: 0, itemCount: 0, @@ -54,22 +61,25 @@ export const useCartStore = create((set, get) => ({ if (existingIndex >= 0) { // Update existing item - newItems = items.map((item, index) => - index === existingIndex - ? { - ...item, - quantity: item.quantity + quantity, - subtotal: (item.quantity + quantity) * item.unitPrice, - } - : item - ); + newItems = items.map((item, index) => { + if (index === existingIndex) { + const newQty = item.quantity + quantity; + const discountMultiplier = 1 - (item.discountPercent / 100); + return { + ...item, + quantity: newQty, + subtotal: newQty * item.unitPrice * discountMultiplier, + }; + } + return item; + }); } else { // Add new item const newItem: CartItem = { product, quantity, unitPrice: product.price, - discount: 0, + discountPercent: 0, subtotal: quantity * product.price, }; newItems = [...items, newItem]; @@ -89,18 +99,32 @@ export const useCartStore = create((set, get) => ({ return; } - const newItems = get().items.map((item) => - item.product.id === productId - ? { ...item, quantity, subtotal: quantity * item.unitPrice } - : item - ); + const newItems = get().items.map((item) => { + if (item.product.id === productId) { + const discountMultiplier = 1 - (item.discountPercent / 100); + return { + ...item, + quantity, + subtotal: quantity * item.unitPrice * discountMultiplier, + }; + } + return item; + }); set({ items: newItems, ...calculateTotals(newItems) }); }, - applyItemDiscount: (productId, discount) => { - const newItems = get().items.map((item) => - item.product.id === productId ? { ...item, discount } : item - ); + applyItemDiscount: (productId, discountPercent) => { + const newItems = get().items.map((item) => { + if (item.product.id === productId) { + const discountMultiplier = 1 - (discountPercent / 100); + return { + ...item, + discountPercent, + subtotal: item.quantity * item.unitPrice * discountMultiplier, + }; + } + return item; + }); set({ items: newItems, ...calculateTotals(newItems) }); }, @@ -117,8 +141,8 @@ export const useCartStore = create((set, get) => ({ amountReceived: 0, notes: '', subtotal: 0, - discount: 0, - tax: 0, + discountAmount: 0, + taxAmount: 0, total: 0, itemCount: 0, }), diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/types/index.ts b/projects/erp-suite/apps/products/pos-micro/frontend/src/types/index.ts index 5829222..081d50a 100644 --- a/projects/erp-suite/apps/products/pos-micro/frontend/src/types/index.ts +++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/types/index.ts @@ -1,10 +1,14 @@ // ============================================================================= -// POS MICRO - TypeScript Types +// POS MICRO - TypeScript Types (aligned with backend entities) // ============================================================================= +// Matches backend Product entity export interface Product { id: string; + tenantId: string; name: string; + description?: string; + sku?: string; barcode?: string; price: number; cost?: number; @@ -12,87 +16,112 @@ export interface Product { category?: Category; imageUrl?: string; trackStock: boolean; - currentStock: number; - minStock: number; + stockQuantity: number; + lowStockAlert: number; isFavorite: boolean; isActive: boolean; + createdAt: string; + updatedAt: string; } +// Matches backend Category entity export interface Category { id: string; + tenantId: string; name: string; + description?: string; color?: string; sortOrder: number; - productCount?: number; + isActive: boolean; + createdAt: string; + updatedAt: string; } +// Frontend cart item export interface CartItem { product: Product; quantity: number; unitPrice: number; - discount: number; + discountPercent: number; subtotal: number; } +// Matches backend Sale entity export interface Sale { id: string; + tenantId: string; ticketNumber: string; items: SaleItem[]; subtotal: number; - discount: number; - tax: number; + discountAmount: number; + taxAmount: number; total: number; - paymentMethod: PaymentMethod; - amountReceived?: number; - change?: number; - status: 'completed' | 'cancelled'; + paymentMethodId: string; + paymentMethod?: PaymentMethod; + amountReceived: number; + changeAmount: number; + customerName?: string; + customerPhone?: string; + status: 'completed' | 'cancelled' | 'refunded'; notes?: string; + cancelledAt?: string; + cancelReason?: string; createdAt: string; + updatedAt: string; } +// Matches backend SaleItem entity export interface SaleItem { id: string; + saleId: string; productId: string; productName: string; + productSku?: string; quantity: number; unitPrice: number; - discount: number; + discountPercent: number; subtotal: number; } +// Matches backend PaymentMethod entity export interface PaymentMethod { id: string; + tenantId: string; name: string; type: 'cash' | 'card' | 'transfer' | 'other'; isActive: boolean; + isDefault: boolean; requiresReference: boolean; + sortOrder: number; } -export interface DailySummary { - date: string; +// Matches backend TodaySummary response +export interface TodaySummary { totalSales: number; - salesCount: number; - cancelledCount: number; - cashSales: number; - cardSales: number; - transferSales: number; + totalRevenue: number; + totalTax: number; + avgTicket: number; } +// Matches backend User entity export interface User { id: string; name: string; - role: 'owner' | 'cashier'; + isOwner: boolean; } +// Matches backend Tenant entity (partial for auth response) +export type SubscriptionStatus = 'trial' | 'active' | 'past_due' | 'cancelled' | 'suspended'; + export interface Tenant { id: string; businessName: string; - planType: 'pos_micro'; - maxProducts: number; - maxSalesPerMonth: number; - currentMonthSales: number; + plan: string; + subscriptionStatus: SubscriptionStatus; + trialEndsAt: string | null; } +// Matches backend AuthResponse export interface AuthResponse { accessToken: string; refreshToken: string;