diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/.env.example b/projects/erp-suite/apps/products/pos-micro/frontend/.env.example
new file mode 100644
index 0000000..4ef686d
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/.env.example
@@ -0,0 +1,6 @@
+# =============================================================================
+# POS MICRO - Frontend Environment Variables
+# =============================================================================
+
+# API URL
+VITE_API_URL=http://localhost:3000/api/v1
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/Dockerfile b/projects/erp-suite/apps/products/pos-micro/frontend/Dockerfile
new file mode 100644
index 0000000..001fc2c
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/Dockerfile
@@ -0,0 +1,58 @@
+# =============================================================================
+# POS MICRO - Frontend Dockerfile
+# =============================================================================
+
+# Build stage
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci
+
+# Copy source code
+COPY . .
+
+# Build application
+RUN npm run build
+
+# Production stage - serve with nginx
+FROM nginx:alpine AS production
+
+# Copy built files
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Copy nginx config
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+# Expose port
+EXPOSE 80
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
+
+CMD ["nginx", "-g", "daemon off;"]
+
+# Development stage
+FROM node:20-alpine AS development
+
+WORKDIR /app
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies
+RUN npm ci
+
+# Copy source code
+COPY . .
+
+# Expose port
+EXPOSE 5173
+
+# Start in development mode
+CMD ["npm", "run", "dev"]
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/index.html b/projects/erp-suite/apps/products/pos-micro/frontend/index.html
new file mode 100644
index 0000000..6c90275
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ POS Micro
+
+
+
+
+
+
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/nginx.conf b/projects/erp-suite/apps/products/pos-micro/frontend/nginx.conf
new file mode 100644
index 0000000..ba3160c
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/nginx.conf
@@ -0,0 +1,35 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_proxied expired no-cache no-store private auth;
+ gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript;
+
+ # Cache static assets
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # PWA manifest and service worker - no cache
+ location ~* (manifest\.json|sw\.js)$ {
+ expires -1;
+ add_header Cache-Control "no-store, no-cache, must-revalidate";
+ }
+
+ # SPA fallback - all routes go to index.html
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/package.json b/projects/erp-suite/apps/products/pos-micro/frontend/package.json
new file mode 100644
index 0000000..c3c674c
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "pos-micro-frontend",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.20.0",
+ "zustand": "^4.4.7",
+ "@tanstack/react-query": "^5.8.4",
+ "axios": "^1.6.2",
+ "lucide-react": "^0.294.0",
+ "clsx": "^2.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@typescript-eslint/eslint-plugin": "^6.10.0",
+ "@typescript-eslint/parser": "^6.10.0",
+ "@vitejs/plugin-react": "^4.2.0",
+ "autoprefixer": "^10.4.16",
+ "eslint": "^8.53.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.4",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.5",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0",
+ "vite-plugin-pwa": "^0.17.4",
+ "workbox-window": "^7.0.0"
+ }
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/postcss.config.js b/projects/erp-suite/apps/products/pos-micro/frontend/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/App.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/App.tsx
new file mode 100644
index 0000000..1815871
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/App.tsx
@@ -0,0 +1,62 @@
+import { Routes, Route, Navigate } from 'react-router-dom';
+import { useAuthStore } from '@/store/auth';
+import { POSPage } from '@/pages/POSPage';
+import { LoginPage } from '@/pages/LoginPage';
+import { ReportsPage } from '@/pages/ReportsPage';
+
+function ProtectedRoute({ children }: { children: React.ReactNode }) {
+ const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return <>{children}>;
+}
+
+function PublicRoute({ children }: { children: React.ReactNode }) {
+ const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+
+ if (isAuthenticated) {
+ return ;
+ }
+
+ return <>{children}>;
+}
+
+export default function App() {
+ return (
+
+ {/* Public routes */}
+
+
+
+ }
+ />
+
+ {/* Protected routes */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+ {/* Fallback */}
+ } />
+
+ );
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CartPanel.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CartPanel.tsx
new file mode 100644
index 0000000..8eb26a8
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CartPanel.tsx
@@ -0,0 +1,103 @@
+import { Minus, Plus, Trash2, ShoppingCart } from 'lucide-react';
+import { useCartStore } from '@/store/cart';
+import type { CartItem } from '@/types';
+
+interface CartPanelProps {
+ onCheckout: () => void;
+}
+
+export function CartPanel({ onCheckout }: CartPanelProps) {
+ const { items, total, itemCount, updateQuantity, removeItem } = useCartStore();
+
+ if (items.length === 0) {
+ return (
+
+
+
Carrito vacio
+
Selecciona productos para agregar
+
+ );
+ }
+
+ return (
+
+ {/* Cart items */}
+
+ {items.map((item) => (
+ updateQuantity(item.product.id, qty)}
+ onRemove={() => removeItem(item.product.id)}
+ />
+ ))}
+
+
+ {/* Cart summary */}
+
+
+ Total ({itemCount} productos)
+ ${total.toFixed(2)}
+
+
+
+
+
+ );
+}
+
+interface CartItemRowProps {
+ item: CartItem;
+ onUpdateQuantity: (quantity: number) => void;
+ onRemove: () => void;
+}
+
+function CartItemRow({ item, onUpdateQuantity, onRemove }: CartItemRowProps) {
+ return (
+
+
+
{item.product.name}
+
+ ${item.unitPrice.toFixed(2)} c/u
+
+
+
+
+ {/* Quantity controls */}
+
+
+
{item.quantity}
+
+
+
+ {/* Subtotal */}
+
+ ${item.subtotal.toFixed(2)}
+
+
+ {/* Remove button */}
+
+
+
+ );
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CategoryTabs.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CategoryTabs.tsx
new file mode 100644
index 0000000..9b2d8a7
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CategoryTabs.tsx
@@ -0,0 +1,58 @@
+import clsx from 'clsx';
+import type { Category } from '@/types';
+
+interface CategoryTabsProps {
+ categories: Category[];
+ selectedCategoryId: string | null;
+ onSelect: (categoryId: string | null) => void;
+}
+
+export function CategoryTabs({ categories, selectedCategoryId, onSelect }: CategoryTabsProps) {
+ return (
+
+
+
+
+
+ {categories.map((category) => (
+
+ ))}
+
+ );
+}
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
new file mode 100644
index 0000000..082d7f5
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/CheckoutModal.tsx
@@ -0,0 +1,306 @@
+import { useState } from 'react';
+import { X, CreditCard, Banknote, Smartphone, Check } from 'lucide-react';
+import clsx from 'clsx';
+import { useCartStore } from '@/store/cart';
+import { usePaymentMethods } from '@/hooks/usePayments';
+import { useCreateSale } from '@/hooks/useSales';
+import type { PaymentMethod, Sale } from '@/types';
+
+interface CheckoutModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSuccess: (sale: Sale) => void;
+}
+
+const QUICK_AMOUNTS = [20, 50, 100, 200, 500];
+
+export function CheckoutModal({ isOpen, onClose, onSuccess }: CheckoutModalProps) {
+ const { items, total, paymentMethod, setPaymentMethod, amountReceived, setAmountReceived } = useCartStore();
+ const { data: paymentMethods } = usePaymentMethods();
+ const createSale = useCreateSale();
+
+ const [step, setStep] = useState<'payment' | 'amount' | 'confirm'>('payment');
+
+ const change = paymentMethod?.type === 'cash' ? Math.max(0, amountReceived - total) : 0;
+
+ const handleSelectPayment = (method: PaymentMethod) => {
+ setPaymentMethod(method);
+ if (method.type === 'cash') {
+ setStep('amount');
+ } else {
+ setStep('confirm');
+ }
+ };
+
+ const handleConfirm = async () => {
+ if (!paymentMethod) return;
+
+ try {
+ const sale = await createSale.mutateAsync({
+ items: items.map((item) => ({
+ productId: item.product.id,
+ quantity: item.quantity,
+ unitPrice: item.unitPrice,
+ discount: item.discount,
+ })),
+ paymentMethodId: paymentMethod.id,
+ amountReceived: paymentMethod.type === 'cash' ? amountReceived : undefined,
+ });
+
+ onSuccess(sale);
+ setStep('payment');
+ setAmountReceived(0);
+ } catch (error) {
+ console.error('Error creating sale:', error);
+ }
+ };
+
+ const handleClose = () => {
+ setStep('payment');
+ setAmountReceived(0);
+ onClose();
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Header */}
+
+
Cobrar
+
+
+
+ {/* Total */}
+
+
Total a cobrar
+
${total.toFixed(2)}
+
+
+ {/* Content */}
+
+ {step === 'payment' && (
+
+ )}
+
+ {step === 'amount' && (
+
setStep('payment')}
+ onConfirm={() => setStep('confirm')}
+ />
+ )}
+
+ {step === 'confirm' && (
+ setStep(paymentMethod?.type === 'cash' ? 'amount' : 'payment')}
+ onConfirm={handleConfirm}
+ />
+ )}
+
+
+
+ );
+}
+
+interface PaymentStepProps {
+ paymentMethods: PaymentMethod[];
+ onSelect: (method: PaymentMethod) => void;
+}
+
+function PaymentStep({ paymentMethods, onSelect }: PaymentStepProps) {
+ const getIcon = (type: string) => {
+ switch (type) {
+ case 'cash':
+ return Banknote;
+ case 'card':
+ return CreditCard;
+ case 'transfer':
+ return Smartphone;
+ default:
+ return Banknote;
+ }
+ };
+
+ return (
+
+
Selecciona forma de pago
+ {paymentMethods.map((method) => {
+ const Icon = getIcon(method.type);
+ return (
+
+ );
+ })}
+
+ );
+}
+
+interface AmountStepProps {
+ total: number;
+ amountReceived: number;
+ onAmountChange: (amount: number) => void;
+ onBack: () => void;
+ onConfirm: () => void;
+}
+
+function AmountStep({ total, amountReceived, onAmountChange, onBack, onConfirm }: AmountStepProps) {
+ return (
+
+
Cantidad recibida
+
+ {/* Input */}
+
onAmountChange(Number(e.target.value))}
+ placeholder="0.00"
+ autoFocus
+ />
+
+ {/* Quick amounts */}
+
+ {QUICK_AMOUNTS.map((amount) => (
+
+ ))}
+
+
+ {/* Exact amount button */}
+
+
+ {/* Change preview */}
+ {amountReceived >= total && (
+
+
Cambio
+
+ ${(amountReceived - total).toFixed(2)}
+
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+ );
+}
+
+interface ConfirmStepProps {
+ total: number;
+ paymentMethod: PaymentMethod;
+ amountReceived: number;
+ change: number;
+ isLoading: boolean;
+ onBack: () => void;
+ onConfirm: () => void;
+}
+
+function ConfirmStep({
+ total,
+ paymentMethod,
+ amountReceived,
+ change,
+ isLoading,
+ onBack,
+ onConfirm,
+}: ConfirmStepProps) {
+ return (
+
+
+
+ Metodo de pago
+ {paymentMethod.name}
+
+
+ Total
+ ${total.toFixed(2)}
+
+ {paymentMethod.type === 'cash' && (
+ <>
+
+ Recibido
+ ${amountReceived.toFixed(2)}
+
+
+ Cambio
+ ${change.toFixed(2)}
+
+ >
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/Header.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/Header.tsx
new file mode 100644
index 0000000..e1ef78c
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/Header.tsx
@@ -0,0 +1,102 @@
+import { Menu, BarChart3, Package, Settings, LogOut } from 'lucide-react';
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { useAuthStore } from '@/store/auth';
+
+export function Header() {
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const { tenant, logout } = useAuthStore();
+ const navigate = useNavigate();
+
+ const handleLogout = () => {
+ logout();
+ navigate('/login');
+ };
+
+ return (
+ <>
+
+
+
+
+
POS Micro
+
{tenant?.businessName}
+
+
+
+
+ {/* Mobile menu overlay */}
+ {isMenuOpen && (
+
+
setIsMenuOpen(false)}
+ />
+
+
+ )}
+ >
+ );
+}
+
+interface NavLinkProps {
+ to: string;
+ icon: React.ReactNode;
+ label: string;
+ onClick: () => void;
+}
+
+function NavLink({ to, icon, label, onClick }: NavLinkProps) {
+ return (
+
+ {icon}
+ {label}
+
+ );
+}
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
new file mode 100644
index 0000000..08dbac4
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/ProductCard.tsx
@@ -0,0 +1,77 @@
+import { Star } from 'lucide-react';
+import clsx from 'clsx';
+import type { Product } from '@/types';
+
+interface ProductCardProps {
+ product: Product;
+ onSelect: (product: Product) => void;
+ onToggleFavorite?: (productId: string) => void;
+}
+
+export function ProductCard({ product, onSelect, onToggleFavorite }: ProductCardProps) {
+ const isLowStock = product.trackStock && product.currentStock <= product.minStock;
+ const isOutOfStock = product.trackStock && product.currentStock <= 0;
+
+ return (
+
!isOutOfStock && onSelect(product)}
+ >
+ {/* Favorite button */}
+ {onToggleFavorite && (
+
+ )}
+
+ {/* Product image or placeholder */}
+ {product.imageUrl ? (
+

+ ) : (
+
+
+ {product.name.charAt(0).toUpperCase()}
+
+
+ )}
+
+ {/* Product info */}
+
+ {product.name}
+
+
+ ${product.price.toFixed(2)}
+
+
+ {/* Stock indicator */}
+ {product.trackStock && (
+
+ {isOutOfStock ? 'Agotado' : `${product.currentStock} disponibles`}
+
+ )}
+
+ );
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/components/SuccessModal.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/SuccessModal.tsx
new file mode 100644
index 0000000..710dc94
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/components/SuccessModal.tsx
@@ -0,0 +1,114 @@
+import { CheckCircle, Printer, Share2, X } from 'lucide-react';
+import type { Sale } from '@/types';
+
+interface SuccessModalProps {
+ isOpen: boolean;
+ sale: Sale | null;
+ onClose: () => void;
+}
+
+export function SuccessModal({ isOpen, sale, onClose }: SuccessModalProps) {
+ if (!isOpen || !sale) return null;
+
+ const handlePrint = () => {
+ // In a real app, this would trigger receipt printing
+ window.print();
+ };
+
+ const handleShare = async () => {
+ const text = `Venta #${sale.ticketNumber}\nTotal: $${sale.total.toFixed(2)}\nGracias por su compra!`;
+
+ if (navigator.share) {
+ try {
+ await navigator.share({ text });
+ } catch {
+ // User cancelled or error
+ }
+ } else {
+ // Fallback: copy to clipboard
+ await navigator.clipboard.writeText(text);
+ }
+ };
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Close button */}
+
+
+ {/* Success icon */}
+
+
+ {/* Content */}
+
+
Venta completada!
+
Ticket #{sale.ticketNumber}
+
+
+
+ Subtotal
+ ${sale.subtotal.toFixed(2)}
+
+ {sale.discount > 0 && (
+
+ Descuento
+ -${sale.discount.toFixed(2)}
+
+ )}
+
+ Total
+ ${sale.total.toFixed(2)}
+
+ {sale.change && sale.change > 0 && (
+
+ Cambio
+ ${sale.change.toFixed(2)}
+
+ )}
+
+
+ {/* Actions */}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/usePayments.ts b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/usePayments.ts
new file mode 100644
index 0000000..d829855
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/usePayments.ts
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query';
+import api from '@/services/api';
+import type { PaymentMethod } from '@/types';
+
+export function usePaymentMethods() {
+ return useQuery({
+ queryKey: ['payment-methods'],
+ queryFn: async () => {
+ const { data } = await api.get
('/payments/methods');
+ return data;
+ },
+ staleTime: 1000 * 60 * 30, // 30 minutes - payment methods don't change often
+ });
+}
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
new file mode 100644
index 0000000..e80201f
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useProducts.ts
@@ -0,0 +1,67 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import api from '@/services/api';
+import type { Product, Category } from '@/types';
+
+// Products
+export function useProducts(categoryId?: string) {
+ return useQuery({
+ queryKey: ['products', categoryId],
+ queryFn: async () => {
+ const params = new URLSearchParams();
+ if (categoryId) params.append('categoryId', categoryId);
+ params.append('isActive', 'true');
+
+ const { data } = await api.get(`/products?${params}`);
+ return data;
+ },
+ });
+}
+
+export function useFavoriteProducts() {
+ return useQuery({
+ queryKey: ['products', 'favorites'],
+ queryFn: async () => {
+ const { data } = await api.get('/products/favorites');
+ return data;
+ },
+ });
+}
+
+export function useSearchProduct() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (barcode: string) => {
+ const { data } = await api.get(`/products/barcode/${barcode}`);
+ return data;
+ },
+ onSuccess: (product) => {
+ queryClient.setQueryData(['product', product.id], product);
+ },
+ });
+}
+
+export function useToggleFavorite() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (productId: string) => {
+ const { data } = await api.patch(`/products/${productId}/favorite`);
+ return data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['products'] });
+ },
+ });
+}
+
+// Categories
+export function useCategories() {
+ return useQuery({
+ queryKey: ['categories'],
+ queryFn: async () => {
+ const { data } = await api.get('/categories');
+ return data;
+ },
+ });
+}
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
new file mode 100644
index 0000000..0f830b3
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/hooks/useSales.ts
@@ -0,0 +1,73 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import api from '@/services/api';
+import { useCartStore } from '@/store/cart';
+import type { Sale, DailySummary } from '@/types';
+
+interface CreateSaleDto {
+ items: {
+ productId: string;
+ quantity: number;
+ unitPrice: number;
+ discount: number;
+ }[];
+ paymentMethodId: string;
+ amountReceived?: number;
+ notes?: string;
+}
+
+export function useCreateSale() {
+ const queryClient = useQueryClient();
+ const clearCart = useCartStore((state) => state.clear);
+
+ return useMutation({
+ mutationFn: async (sale: CreateSaleDto) => {
+ const { data } = await api.post('/sales', sale);
+ return data;
+ },
+ onSuccess: () => {
+ clearCart();
+ queryClient.invalidateQueries({ queryKey: ['sales'] });
+ queryClient.invalidateQueries({ queryKey: ['daily-summary'] });
+ queryClient.invalidateQueries({ queryKey: ['products'] }); // Stock updated
+ },
+ });
+}
+
+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}`);
+ return data;
+ },
+ });
+}
+
+export function useDailySummary(date?: string) {
+ const queryDate = date || new Date().toISOString().split('T')[0];
+
+ return useQuery({
+ queryKey: ['daily-summary', queryDate],
+ queryFn: async () => {
+ const { data } = await api.get(`/sales/summary/${queryDate}`);
+ return data;
+ },
+ });
+}
+
+export function useCancelSale() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (saleId: string) => {
+ const { data } = await api.patch(`/sales/${saleId}/cancel`);
+ return data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['sales'] });
+ queryClient.invalidateQueries({ queryKey: ['daily-summary'] });
+ queryClient.invalidateQueries({ queryKey: ['products'] }); // Stock restored
+ },
+ });
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/main.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/main.tsx
new file mode 100644
index 0000000..321d557
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/main.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App';
+import './styles/index.css';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ retry: 1,
+ },
+ },
+});
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+
+);
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
new file mode 100644
index 0000000..86406fb
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/LoginPage.tsx
@@ -0,0 +1,142 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { ShoppingBag, Eye, EyeOff } from 'lucide-react';
+import api from '@/services/api';
+import { useAuthStore } from '@/store/auth';
+import type { AuthResponse } from '@/types';
+
+export function LoginPage() {
+ const [businessName, setBusinessName] = useState('');
+ const [pin, setPin] = useState('');
+ const [showPin, setShowPin] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [isRegister, setIsRegister] = useState(false);
+
+ const login = useAuthStore((state) => state.login);
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setIsLoading(true);
+
+ try {
+ const endpoint = isRegister ? '/auth/register' : '/auth/login';
+ const payload = isRegister
+ ? { businessName, pin }
+ : { businessName, pin };
+
+ const { data } = await api.post(endpoint, payload);
+
+ login(data.accessToken, data.refreshToken, data.user, data.tenant);
+ navigate('/');
+ } catch (err: unknown) {
+ const errorMessage =
+ err instanceof Error
+ ? err.message
+ : (err as { response?: { data?: { message?: string } } })?.response?.data?.message ||
+ (isRegister ? 'Error al registrar negocio' : 'PIN o negocio incorrecto');
+ setError(errorMessage);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {/* Logo */}
+
+
+
+
+
POS Micro
+
Punto de venta simple
+
+
+ {/* Login Card */}
+
+
+ {isRegister ? 'Registrar negocio' : 'Iniciar sesion'}
+
+
+
+
+
+
+
+
+
+ {/* Footer */}
+
+ $100 MXN/mes - Cancela cuando quieras
+
+
+ );
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/POSPage.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/POSPage.tsx
new file mode 100644
index 0000000..1af9414
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/POSPage.tsx
@@ -0,0 +1,154 @@
+import { useState } from 'react';
+import { Search } from 'lucide-react';
+import { Header } from '@/components/Header';
+import { ProductCard } from '@/components/ProductCard';
+import { CartPanel } from '@/components/CartPanel';
+import { CategoryTabs } from '@/components/CategoryTabs';
+import { CheckoutModal } from '@/components/CheckoutModal';
+import { SuccessModal } from '@/components/SuccessModal';
+import { useProducts, useFavoriteProducts, useCategories, useToggleFavorite, useSearchProduct } from '@/hooks/useProducts';
+import { useCartStore } from '@/store/cart';
+import type { Product, Sale } from '@/types';
+
+export function POSPage() {
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
+ const [completedSale, setCompletedSale] = useState(null);
+
+ const { data: categories } = useCategories();
+ const { data: products, isLoading: productsLoading } = useProducts(
+ selectedCategory && selectedCategory !== 'favorites' ? selectedCategory : undefined
+ );
+ const { data: favorites } = useFavoriteProducts();
+ const toggleFavorite = useToggleFavorite();
+ const searchProduct = useSearchProduct();
+ const addItem = useCartStore((state) => state.addItem);
+
+ const displayProducts = selectedCategory === 'favorites' ? favorites : products;
+
+ const filteredProducts = displayProducts?.filter((product) =>
+ product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ product.barcode?.includes(searchQuery)
+ );
+
+ const handleSelectProduct = (product: Product) => {
+ addItem(product);
+ };
+
+ const handleBarcodeSearch = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!searchQuery) return;
+
+ try {
+ const product = await searchProduct.mutateAsync(searchQuery);
+ if (product) {
+ addItem(product);
+ setSearchQuery('');
+ }
+ } catch {
+ // Product not found - will show filtered list instead
+ }
+ };
+
+ const handleCheckoutSuccess = (sale: Sale) => {
+ setIsCheckoutOpen(false);
+ setCompletedSale(sale);
+ };
+
+ return (
+
+
+
+
+ {/* Products Panel */}
+
+ {/* Search bar */}
+
+
+ {/* Category tabs */}
+
+
+ {/* Products grid */}
+
+ {productsLoading ? (
+
+ ) : filteredProducts?.length === 0 ? (
+
+
+
No se encontraron productos
+
+ ) : (
+
+ {filteredProducts?.map((product) => (
+
toggleFavorite.mutate(product.id)}
+ />
+ ))}
+
+ )}
+
+
+
+ {/* Cart Panel - Hidden on mobile, shown on larger screens */}
+
+ setIsCheckoutOpen(true)} />
+
+
+
+ {/* Mobile Cart Button */}
+
setIsCheckoutOpen(true)} />
+
+ {/* Checkout Modal */}
+ setIsCheckoutOpen(false)}
+ onSuccess={handleCheckoutSuccess}
+ />
+
+ {/* Success Modal */}
+ setCompletedSale(null)}
+ />
+
+ );
+}
+
+function MobileCartButton({ onCheckout }: { onCheckout: () => void }) {
+ const { itemCount, total } = useCartStore();
+
+ if (itemCount === 0) return null;
+
+ return (
+
+
+
+ );
+}
+
+// Import for the empty state
+import { Package } from 'lucide-react';
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/ReportsPage.tsx b/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/ReportsPage.tsx
new file mode 100644
index 0000000..9ae09c3
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/pages/ReportsPage.tsx
@@ -0,0 +1,157 @@
+import { ArrowLeft, TrendingUp, ShoppingCart, XCircle, Banknote, CreditCard, Smartphone } from 'lucide-react';
+import { Link } from 'react-router-dom';
+import { useDailySummary, useTodaySales } from '@/hooks/useSales';
+
+export function ReportsPage() {
+ const { data: summary, isLoading: summaryLoading } = useDailySummary();
+ const { data: sales, isLoading: salesLoading } = useTodaySales();
+
+ const isLoading = summaryLoading || salesLoading;
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Reporte del dia
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {/* Summary Cards */}
+
+ }
+ label="Ventas totales"
+ value={`$${summary?.totalSales.toFixed(2) || '0.00'}`}
+ bgColor="bg-green-50"
+ />
+ }
+ label="Transacciones"
+ value={summary?.salesCount.toString() || '0'}
+ bgColor="bg-blue-50"
+ />
+ }
+ label="Canceladas"
+ value={summary?.cancelledCount.toString() || '0'}
+ bgColor="bg-red-50"
+ />
+ }
+ label="Promedio"
+ value={`$${summary?.salesCount ? (summary.totalSales / summary.salesCount).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 */}
+
+
+
Ventas de hoy
+
+
+ {sales?.length === 0 ? (
+
+ No hay ventas registradas hoy
+
+ ) : (
+ sales?.slice(0, 10).map((sale) => (
+
+
+
#{sale.ticketNumber}
+
+ {new Date(sale.createdAt).toLocaleTimeString('es-MX', {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+ {' - '}
+ {sale.items.length} productos
+
+
+
+
+ ${sale.total.toFixed(2)}
+
+ {sale.status === 'cancelled' && (
+
Cancelada
+ )}
+
+
+ ))
+ )}
+
+
+
+ )}
+
+ );
+}
+
+interface SummaryCardProps {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+ bgColor: string;
+}
+
+function SummaryCard({ icon, label, value, bgColor }: SummaryCardProps) {
+ return (
+
+
+ {icon}
+
+
{label}
+
{value}
+
+ );
+}
+
+interface PaymentRowProps {
+ icon: React.ReactNode;
+ label: string;
+ amount: number;
+}
+
+function PaymentRow({ icon, label, amount }: PaymentRowProps) {
+ return (
+
+
+
${amount.toFixed(2)}
+
+ );
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/services/api.ts b/projects/erp-suite/apps/products/pos-micro/frontend/src/services/api.ts
new file mode 100644
index 0000000..d1e0b0b
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/services/api.ts
@@ -0,0 +1,61 @@
+import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
+import { useAuthStore } from '@/store/auth';
+
+const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';
+
+export const api = axios.create({
+ baseURL: API_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor - add auth token
+api.interceptors.request.use(
+ (config: InternalAxiosRequestConfig) => {
+ const token = useAuthStore.getState().accessToken;
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => Promise.reject(error)
+);
+
+// Response interceptor - handle token refresh
+api.interceptors.response.use(
+ (response) => response,
+ async (error: AxiosError) => {
+ const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
+
+ // If 401 and we haven't retried yet
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ originalRequest._retry = true;
+
+ try {
+ const refreshToken = useAuthStore.getState().refreshToken;
+ if (!refreshToken) {
+ throw new Error('No refresh token');
+ }
+
+ const response = await axios.post(`${API_URL}/auth/refresh`, {
+ refreshToken,
+ });
+
+ const { accessToken, refreshToken: newRefreshToken } = response.data;
+ useAuthStore.getState().setTokens(accessToken, newRefreshToken);
+
+ originalRequest.headers.Authorization = `Bearer ${accessToken}`;
+ return api(originalRequest);
+ } catch {
+ useAuthStore.getState().logout();
+ window.location.href = '/login';
+ return Promise.reject(error);
+ }
+ }
+
+ return Promise.reject(error);
+ }
+);
+
+export default api;
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/store/auth.ts b/projects/erp-suite/apps/products/pos-micro/frontend/src/store/auth.ts
new file mode 100644
index 0000000..108494d
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/store/auth.ts
@@ -0,0 +1,61 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import type { User, Tenant } from '@/types';
+
+interface AuthState {
+ accessToken: string | null;
+ refreshToken: string | null;
+ user: User | null;
+ tenant: Tenant | null;
+ isAuthenticated: boolean;
+ setTokens: (accessToken: string, refreshToken: string) => void;
+ setUser: (user: User, tenant: Tenant) => void;
+ login: (accessToken: string, refreshToken: string, user: User, tenant: Tenant) => void;
+ logout: () => void;
+}
+
+export const useAuthStore = create()(
+ persist(
+ (set) => ({
+ accessToken: null,
+ refreshToken: null,
+ user: null,
+ tenant: null,
+ isAuthenticated: false,
+
+ setTokens: (accessToken, refreshToken) =>
+ set({ accessToken, refreshToken }),
+
+ setUser: (user, tenant) =>
+ set({ user, tenant }),
+
+ login: (accessToken, refreshToken, user, tenant) =>
+ set({
+ accessToken,
+ refreshToken,
+ user,
+ tenant,
+ isAuthenticated: true,
+ }),
+
+ logout: () =>
+ set({
+ accessToken: null,
+ refreshToken: null,
+ user: null,
+ tenant: null,
+ isAuthenticated: false,
+ }),
+ }),
+ {
+ name: 'pos-micro-auth',
+ partialize: (state) => ({
+ accessToken: state.accessToken,
+ refreshToken: state.refreshToken,
+ user: state.user,
+ tenant: state.tenant,
+ isAuthenticated: state.isAuthenticated,
+ }),
+ }
+ )
+);
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
new file mode 100644
index 0000000..f706fcc
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/store/cart.ts
@@ -0,0 +1,125 @@
+import { create } from 'zustand';
+import type { Product, CartItem, PaymentMethod } from '@/types';
+
+interface CartState {
+ items: CartItem[];
+ paymentMethod: PaymentMethod | null;
+ amountReceived: number;
+ notes: string;
+
+ // Computed
+ subtotal: number;
+ discount: number;
+ tax: number;
+ total: number;
+ itemCount: number;
+
+ // Actions
+ addItem: (product: Product, quantity?: number) => void;
+ removeItem: (productId: string) => void;
+ updateQuantity: (productId: string, quantity: number) => void;
+ applyItemDiscount: (productId: string, discount: number) => void;
+ setPaymentMethod: (method: PaymentMethod) => void;
+ setAmountReceived: (amount: number) => void;
+ setNotes: (notes: string) => void;
+ clear: () => void;
+}
+
+const calculateTotals = (items: CartItem[]) => {
+ 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;
+ const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
+
+ return { subtotal, discount, tax, total, itemCount };
+};
+
+export const useCartStore = create((set, get) => ({
+ items: [],
+ paymentMethod: null,
+ amountReceived: 0,
+ notes: '',
+ subtotal: 0,
+ discount: 0,
+ tax: 0,
+ total: 0,
+ itemCount: 0,
+
+ addItem: (product, quantity = 1) => {
+ const { items } = get();
+ const existingIndex = items.findIndex((item) => item.product.id === product.id);
+
+ let newItems: CartItem[];
+
+ 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
+ );
+ } else {
+ // Add new item
+ const newItem: CartItem = {
+ product,
+ quantity,
+ unitPrice: product.price,
+ discount: 0,
+ subtotal: quantity * product.price,
+ };
+ newItems = [...items, newItem];
+ }
+
+ set({ items: newItems, ...calculateTotals(newItems) });
+ },
+
+ removeItem: (productId) => {
+ const newItems = get().items.filter((item) => item.product.id !== productId);
+ set({ items: newItems, ...calculateTotals(newItems) });
+ },
+
+ updateQuantity: (productId, quantity) => {
+ if (quantity <= 0) {
+ get().removeItem(productId);
+ return;
+ }
+
+ const newItems = get().items.map((item) =>
+ item.product.id === productId
+ ? { ...item, quantity, subtotal: quantity * item.unitPrice }
+ : item
+ );
+ set({ items: newItems, ...calculateTotals(newItems) });
+ },
+
+ applyItemDiscount: (productId, discount) => {
+ const newItems = get().items.map((item) =>
+ item.product.id === productId ? { ...item, discount } : item
+ );
+ set({ items: newItems, ...calculateTotals(newItems) });
+ },
+
+ setPaymentMethod: (method) => set({ paymentMethod: method }),
+
+ setAmountReceived: (amount) => set({ amountReceived: amount }),
+
+ setNotes: (notes) => set({ notes }),
+
+ clear: () =>
+ set({
+ items: [],
+ paymentMethod: null,
+ amountReceived: 0,
+ notes: '',
+ subtotal: 0,
+ discount: 0,
+ tax: 0,
+ total: 0,
+ itemCount: 0,
+ }),
+}));
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/src/styles/index.css b/projects/erp-suite/apps/products/pos-micro/frontend/src/styles/index.css
new file mode 100644
index 0000000..142cf4e
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/styles/index.css
@@ -0,0 +1,82 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ font-family: 'Inter', system-ui, sans-serif;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ body {
+ @apply bg-gray-50 text-gray-900 antialiased;
+ touch-action: manipulation;
+ }
+
+ /* Prevent pull-to-refresh on mobile */
+ html, body {
+ overscroll-behavior-y: contain;
+ }
+}
+
+@layer components {
+ .btn {
+ @apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
+ }
+
+ .btn-primary {
+ @apply btn bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500 active:bg-primary-700;
+ }
+
+ .btn-secondary {
+ @apply btn bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500;
+ }
+
+ .btn-danger {
+ @apply btn bg-red-500 text-white hover:bg-red-600 focus:ring-red-500;
+ }
+
+ .btn-lg {
+ @apply px-6 py-3 text-lg;
+ }
+
+ .card {
+ @apply bg-white rounded-xl shadow-sm border border-gray-100;
+ }
+
+ .input {
+ @apply w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 focus:outline-none transition-colors;
+ }
+
+ .product-card {
+ @apply card p-3 flex flex-col items-center justify-center cursor-pointer hover:shadow-md transition-shadow active:scale-95;
+ min-height: 100px;
+ }
+
+ .cart-item {
+ @apply flex items-center justify-between py-3 border-b border-gray-100 last:border-0;
+ }
+
+ .quantity-btn {
+ @apply w-10 h-10 rounded-full flex items-center justify-center text-lg font-bold transition-colors;
+ }
+}
+
+@layer utilities {
+ .safe-bottom {
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+
+ .safe-top {
+ padding-top: env(safe-area-inset-top);
+ }
+
+ /* Hide scrollbar but keep functionality */
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+ .no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+}
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
new file mode 100644
index 0000000..5829222
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/src/types/index.ts
@@ -0,0 +1,115 @@
+// =============================================================================
+// POS MICRO - TypeScript Types
+// =============================================================================
+
+export interface Product {
+ id: string;
+ name: string;
+ barcode?: string;
+ price: number;
+ cost?: number;
+ categoryId?: string;
+ category?: Category;
+ imageUrl?: string;
+ trackStock: boolean;
+ currentStock: number;
+ minStock: number;
+ isFavorite: boolean;
+ isActive: boolean;
+}
+
+export interface Category {
+ id: string;
+ name: string;
+ color?: string;
+ sortOrder: number;
+ productCount?: number;
+}
+
+export interface CartItem {
+ product: Product;
+ quantity: number;
+ unitPrice: number;
+ discount: number;
+ subtotal: number;
+}
+
+export interface Sale {
+ id: string;
+ ticketNumber: string;
+ items: SaleItem[];
+ subtotal: number;
+ discount: number;
+ tax: number;
+ total: number;
+ paymentMethod: PaymentMethod;
+ amountReceived?: number;
+ change?: number;
+ status: 'completed' | 'cancelled';
+ notes?: string;
+ createdAt: string;
+}
+
+export interface SaleItem {
+ id: string;
+ productId: string;
+ productName: string;
+ quantity: number;
+ unitPrice: number;
+ discount: number;
+ subtotal: number;
+}
+
+export interface PaymentMethod {
+ id: string;
+ name: string;
+ type: 'cash' | 'card' | 'transfer' | 'other';
+ isActive: boolean;
+ requiresReference: boolean;
+}
+
+export interface DailySummary {
+ date: string;
+ totalSales: number;
+ salesCount: number;
+ cancelledCount: number;
+ cashSales: number;
+ cardSales: number;
+ transferSales: number;
+}
+
+export interface User {
+ id: string;
+ name: string;
+ role: 'owner' | 'cashier';
+}
+
+export interface Tenant {
+ id: string;
+ businessName: string;
+ planType: 'pos_micro';
+ maxProducts: number;
+ maxSalesPerMonth: number;
+ currentMonthSales: number;
+}
+
+export interface AuthResponse {
+ accessToken: string;
+ refreshToken: string;
+ user: User;
+ tenant: Tenant;
+}
+
+// API Response types
+export interface ApiResponse {
+ data: T;
+ message?: string;
+}
+
+export interface PaginatedResponse {
+ data: T[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/tailwind.config.js b/projects/erp-suite/apps/products/pos-micro/frontend/tailwind.config.js
new file mode 100644
index 0000000..3d46f8b
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/tailwind.config.js
@@ -0,0 +1,26 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ 50: '#ecfdf5',
+ 100: '#d1fae5',
+ 200: '#a7f3d0',
+ 300: '#6ee7b7',
+ 400: '#34d399',
+ 500: '#10b981',
+ 600: '#059669',
+ 700: '#047857',
+ 800: '#065f46',
+ 900: '#064e3b',
+ },
+ },
+ fontFamily: {
+ sans: ['Inter', 'system-ui', 'sans-serif'],
+ },
+ },
+ },
+ plugins: [],
+};
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.json b/projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.json
new file mode 100644
index 0000000..5413626
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.node.json b/projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/projects/erp-suite/apps/products/pos-micro/frontend/vite.config.ts b/projects/erp-suite/apps/products/pos-micro/frontend/vite.config.ts
new file mode 100644
index 0000000..e602774
--- /dev/null
+++ b/projects/erp-suite/apps/products/pos-micro/frontend/vite.config.ts
@@ -0,0 +1,70 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { VitePWA } from 'vite-plugin-pwa';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [
+ react(),
+ VitePWA({
+ registerType: 'autoUpdate',
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
+ manifest: {
+ name: 'POS Micro',
+ short_name: 'POS',
+ description: 'Punto de venta ultra-simple para negocios pequeños',
+ theme_color: '#10b981',
+ background_color: '#ffffff',
+ display: 'standalone',
+ orientation: 'portrait',
+ start_url: '/',
+ icons: [
+ {
+ src: 'pwa-192x192.png',
+ sizes: '192x192',
+ type: 'image/png',
+ },
+ {
+ src: 'pwa-512x512.png',
+ sizes: '512x512',
+ type: 'image/png',
+ },
+ {
+ src: 'pwa-512x512.png',
+ sizes: '512x512',
+ type: 'image/png',
+ purpose: 'any maskable',
+ },
+ ],
+ },
+ workbox: {
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
+ runtimeCaching: [
+ {
+ urlPattern: /^https:\/\/api\./i,
+ handler: 'NetworkFirst',
+ options: {
+ cacheName: 'api-cache',
+ expiration: {
+ maxEntries: 100,
+ maxAgeSeconds: 60 * 60 * 24, // 24 hours
+ },
+ cacheableResponse: {
+ statuses: [0, 200],
+ },
+ },
+ },
+ ],
+ },
+ }),
+ ],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ port: 5173,
+ host: true,
+ },
+});