# Sr. Pan — Contexto, instrucciones y pendientes

Documento de referencia para continuidad del proyecto **panaderia** (CodeIgniter 4).  
**Última actualización: 8 julio 2026** (POS desktop skin, validación de efectivo, Documentación in-app).  
**Branch de trabajo actual:** `development` (integra landing, POS móvil, docs y cobros).

---

## 1. Resumen del proyecto

Aplicación web para **Sr. Pan** (panadería gourmet en Chiapas) con dos frentes:

| Frente | Ruta | Usuario | Propósito |
|---|---|---|---|
| **Landing pública** | `GET /` | Cliente | Catálogo, bolsa de pedido, checkout por WhatsApp |
| **POS / Admin** | `/entrar`, `/inicio`, `/articulos`, `/tiendas`, `/documentacion`, etc. | Staff | Ventas, artículos, sucursales, reportes, guía de uso |

**Stack:** PHP 8.2, CodeIgniter 4, MySQL, jQuery/Bootstrap en admin, vanilla JS en landing.  
**Entorno local:** DAMP (`panaderia.test`).  
**Repo:** `gitlab.com/sistematlan/panaderia`

---

## 2. Modelo de datos relevante

### Artículos (`articles`)
- `name`, `description` (TEXT), `category`, `cost`, `price`, `paused`
- `articles.price` = **precio mínimo** entre sucursales (sincronizado automáticamente)

### Sucursales (`stores`)
- `name` — nombre de la tienda (ej. identificador en POS y landing)
- `address` — en landing se usa como **texto de referencia** bajo el nombre; en mapas/FAQ como ciudad
- `phone`

### Precios por sucursal (`prices`)
- `store`, `article`, `name` (fijo: `regular`), `amount`
- Índice: `(article, store)`
- **Fuente de verdad** para cobro en POS y bolsa de landing

### Ventas (`sales`)
- `store` — sucursal donde se registró la venta (POS)

---

## 3. Lo implementado (estado actual)

### Admin — Artículos (`/articulos`)
- [x] Campo **descripción** (create + modal edit)
- [x] **Precios por sucursal** (un input por tienda en BD)
- [x] API enriquecida con `storePrices` y `description`
- [x] Columna **Precio removida** del DataTable (solo costo + estatus)

### Admin — Tiendas (`/tiendas`)
- [x] Modal de edición (nombre + dirección)
- [x] Auto-guardado al cambiar campos
- [x] Backend `PUT /store/{id}` con lectura desde POST

### Landing (`/`)
- [x] Descripción desde BD (fallback a copy genérico por nombre/categoría)
- [x] **Sin precio visible** en tarjetas del catálogo
- [x] Sucursales desde `stores` (no hardcodeadas)
- [x] Bolsa: selector muestra **nombre de tienda** + dirección pequeña muted
- [x] Precio calculado al elegir sucursal (`srpan_bag_v2` en localStorage)
- [x] Hero mantiene “Precio de entrada” = mínimo global entre todas las sucursales
- [x] WhatsApp incluye total por sucursal seleccionada

### POS desktop / tablet (`/inicio`, `.pos-desktop`, ≥ `lg`)
- [x] Selector de **tienda en navbar** (derecha, antes del avatar); etiqueta “Tienda” oculta en pantallas &lt; `md`
- [x] Precios de tarjetas y carrito según sucursal activa; `localStorage` (`pos_store_id`)
- [x] Venta envía `store` al backend; `SaleItemService` usa precio de esa sucursal
- [x] **Skin Sr. Pan** alineado al móvil: tokens, chips de categoría, tarjetas con stepper (− / qty / +), panel de cobro, CTA “Cobrar e imprimir” (`public/assets/css/pos-desktop.css`)
- [x] Scroll del catálogo con PerfectScrollbar solo en la lista (`#vertical-example`), panel flex para recorrer todos los artículos
- [x] **Efectivo obligatorio**: sin monto en `#cashreceived` (o menor al total) no se cobra; validación en cliente + servidor

### POS móvil (`/inicio` en smartphone, `#pos-mobile`)
Detalle completo en [`docs/POS-MOVIL.md`](POS-MOVIL.md).
- [x] 4 pantallas virtuales en la misma ruta (Home → Catálogo → Cobrar → Recibo), solo bajo `lg`
- [x] Tokens de marca Sr. Pan (Fraunces/DM Sans, scoped a `#pos-mobile`), tab bar, bag bar, targets ≥44px, safe areas iOS
- [x] "Más vendidos" por tienda (`ReportService::topArticleIdsByStore`, 30 días, fallback global)
- [x] Carrito compartido con el core del desktop vía `pos:cart-changed` + `submitSale()`
- [x] `POST /sale` devuelve `line_items` + `created_at_formatted` (`uuid` top-level)
- [x] WhatsApp: botón principal `wa.me` (tel. 10 dígitos, **no se persiste**); secundario “Compartir imagen del recibo” (html2canvas)
- [x] Ticket público `/ticket/(:any)` (uuid v6 = token)
- [x] **Efectivo**: campo `#pos-m-received` (“Monto recibido”); `getCashReceivedAmount()` en viewport móvil **solo** lee ese input (no el desktop). Cliente + servidor exigen recibido ≥ total

### Documentación in-app (`/documentacion`)
- [x] Guía de uso para personal y admin (índice + acordeones), en el sidebar bajo **Ayuda** (encima de Soporte)
- [x] Controlador `Docs`, vista `app/Views/docs/index.php`, estilos `public/assets/css/docs-help.css`
- [x] Acceso: cualquier usuario `loggedIn`

### Backend / herramientas
- [x] `PriceService` — sync, consulta y precio mínimo
- [x] Migración `AddDescriptionToArticles`
- [x] Migración `SeedStorePricesFromArticles` (una vez, al migrar)
- [x] Comando `php spark prices:seed-stores` (pruebas / re-sync)
- [x] `Sale::create` valida efectivo: `payment.received` &gt; 0 y ≥ `amount`; recalcula `cashback` en servidor

---

## 4. Archivos clave

| Área | Archivos |
|---|---|
| Precios | `app/Services/PriceService.php`, `app/Models/PriceModel.php` |
| Artículos | `app/Controllers/Article.php`, `app/Views/article/*` |
| Tiendas | `app/Controllers/Store.php`, `app/Views/store/*` |
| Landing | `app/Controllers/Home.php`, `app/Views/landing.php`, `public/assets/js/srpan-cart.js`, `public/assets/css/srpan-landing.css` |
| POS core | `app/Views/start.php`, `app/Controllers/Sale.php`, `app/Services/SaleItemService.php` |
| POS desktop | `public/assets/css/pos-desktop.css` (skin `.pos-desktop`) |
| POS móvil | `app/Views/pos/mobile.php`, `public/assets/js/pos-mobile.js`, `public/assets/css/pos-mobile.css`, `public/assets/vendor/html2canvas/`, `docs/POS-MOVIL.md` |
| Documentación | `app/Controllers/Docs.php`, `app/Views/docs/index.php`, `public/assets/css/docs-help.css`, sidebar en `app/Views/components/sidebar.php` |
| Navbar compartido | `app/Views/components/navbar.php` (slot `navbarExtras`) |
| Seed precios | `app/Commands/SeedStorePrices.php` |
| Migraciones | `app/Database/Migrations/2026-07-01-*` |
| Preview estática | `scripts/build-cloudflare-preview.sh`, `wrangler.toml`, `dist/` |
| Landing (detalle UX) | `docs/LANDING-SRPAN.md` |

---

## 5. Instrucciones de operación

### Desarrollo local (DAMP)

```bash
# Migraciones
docker exec -i panaderia php /app/spark migrate

# Poblar precios de prueba (copia articles.price → prices por sucursal)
docker exec -i panaderia php /app/spark prices:seed-stores --force

# URLs
# Landing:        https://panaderia.test/
# POS:            https://panaderia.test/inicio
# Artículos:      https://panaderia.test/articulos
# Tiendas:        https://panaderia.test/tiendas
# Documentación:  https://panaderia.test/documentacion
```

### Comando `prices:seed-stores`

```bash
php spark prices:seed-stores              # solo inserta faltantes
php spark prices:seed-stores --force      # sobrescribe con articles.price
php spark prices:seed-stores --store=2    # una sucursal específica
```

### Deploy preview landing (Cloudflare Pages)

La preview es un **snapshot estático** generado desde la app PHP local.

```bash
# 1. Asegurar BD actualizada + precios poblados en panaderia.test
./scripts/build-cloudflare-preview.sh

# 2. Deploy
wrangler pages deploy dist \
  --project-name=srpan-landing-preview \
  --branch=feat/landing-srpan \
  --commit-dirty=true
```

**URLs preview:**
- https://feat-landing-srpan.srpan-landing-preview.pages.dev

### Producción PHP (`srpan.sistematlan.icu`)

- Deploy histórico por **FTPS** a `/public_html/srpan.sistematlan.com`
- Tras subir código, en servidor:
  ```bash
  php spark migrate
  php spark prices:seed-stores --force
  ```
- Nota: la raíz en producción puede redirigir a `/entrar` (POS); la landing vive en preview Cloudflare hasta merge/deploy final.

---

## 6. Convenciones y decisiones

1. **`stores.name`** = identificador principal de tienda (POS navbar, bolsa landing).
2. **`stores.address`** = referencia secundaria (texto pequeño en bolsa; ciudad en secciones de ubicación).
3. **`articles.price`** = mínimo entre sucursales; no reemplaza `prices`.
4. **Landing bolsa** no registra venta en POS; solo WhatsApp.
5. **POS** siempre recalcula precios en servidor (`SaleItemService`); no confía en el cliente.
6. **localStorage:**
   - Landing: `srpan_bag_v2` (migra desde `v1`)
   - POS: `pos_store_id`
7. **Ticket público**: `/ticket/{uuid}` no requiere login (uuid v6 = token); el ticket muestra el nombre del cajero, igual que el impreso.
8. **Teléfono del cliente**: solo se usa para abrir WhatsApp; **no se guarda** en BD (decisión de diseño; si se quiere historial, agregar `customer_phone` a `sales`).
9. **POS móvil**: ids con prefijo `pos-m-`; no duplica lógica de carrito (llama a `setCartItem` / `submitSale` de `start.php`).
10. **Efectivo**:
    - Desktop: `#cashreceived` / `#cashback`
    - Móvil: `#pos-m-received` / `#pos-m-change`
    - `getCashReceivedAmount()` en `start.php`: en viewport ≤991.98px **solo** lee `#pos-m-received` (vacío = 0; no cae al input desktop)
    - `Sale::create` exige `payment.received` &gt; 0 y ≥ total cuando `payment_type === cash`; recalcula cambio
11. **Documentación in-app** es estática (sin CMS); se edita en `app/Views/docs/index.php`.

---

## 7. Formato API / POST relevante

### Crear artículo
```
description: "..."
store_prices[1]: 45.00
store_prices[2]: 48.00
```

### Respuesta artículo (index/show)
```json
{
  "id": 12,
  "name": "...",
  "description": "...",
  "storePrices": { "2": 45.0, "4": 48.0 },
  "price": { "name": "regular", "amount": 45.0 }
}
```

### catalogJson (landing)
```json
{
  "stores": [{ "id": 2, "name": "Huixtla", "address": "Iturbide Ote 13" }],
  "articles": [{ "id": 12, "prices": { "2": 45.0, "4": 48.0 } }]
}
```

### Venta POS
```json
{
  "store": 2,
  "cart": { "12": { "id": 12, "qty": 2, ... } },
  "payment_type": "cash",
  "amount": 90,
  "payment": { "received": 100, "cashback": 10 },
  "discount": 0,
  "aut": null
}
```
- Si `payment_type` es `cash`, el servidor **rechaza** la venta sin `payment.received` o si es menor que el total recalculado.
- En `card` / `transfer` no se exige `received` (en tarjeta se puede enviar `aut`).

---

## 8. Pendientes

### Alta prioridad
- [ ] **Merge** `feat/landing-srpan` → `main` y deploy producción definitivo
- [ ] Ejecutar migraciones + `prices:seed-stores --force` en **producción**
- [ ] Verificar que `srpan.sistematlan.icu` sirva landing en `/` o definir dominio final
- [ ] Completar datos de tiendas: `name` + `address` correctos para cada sucursal

### Contenido / negocio
- [ ] Redactar **descripciones** de artículos en `/articulos` (hoy muchas vacías → fallback genérico)
- [ ] Ajustar **precios distintos** por sucursal donde aplique (seed copia el mismo monto)
- [ ] Direcciones físicas completas en footer/FAQ (calle, horarios) cuando el cliente las proporcione

### Landing (ver también `docs/LANDING-SRPAN.md`)
- [ ] Campo de notas en checkout WhatsApp (fecha, entrega, comentarios)
- [ ] Imágenes por `article.id` en BD (hoy match por nombre)
- [ ] Subset Remix Icon / critical CSS / self-host fonts
- [ ] Analytics: `add_to_bag`, `open_bag`, `whatsapp_checkout`

### POS / Admin
- [ ] Mostrar líneas del carrito en el resumen desktop con precio unitario (hoy el listado `#cart-items` no se rellena)
- [ ] Validar en servidor que `store` sea obligatorio en ventas
- [ ] Reportes filtrados por sucursal (`sales.store`)
- [ ] Mantener la guía `/documentacion` al día cuando cambie el flujo de venta

### POS móvil (ver también `docs/POS-MOVIL.md`)
- [ ] Probar en teléfono físico: share nativo con imagen, deep link real de WhatsApp, safe areas con notch
- [ ] Corregir artículo `id=54` con nombre vacío en BD (aparece como "Artículo")
- [ ] Opcional: al redimensionar de móvil ↔ desktop a mitad de venta, sincronizar qty en la UI del layout que se muestra

### Técnico / deuda
- [ ] Resolver migración fallida `AddReceivedAmountSale` si `cash` ya existe en `sales` (entornos viejos)
- [ ] Tests automatizados para `PriceService` y `SaleItemService`
- [ ] Sincronizar `dist/` y docs tras cada release

---

## 9. Checklist rápido para retomar

1. `git checkout feat/landing-srpan && git pull`
2. `docker exec -i panaderia php /app/spark migrate`
3. `docker exec -i panaderia php /app/spark prices:seed-stores --force`
4. Probar `/articulos`, `/tiendas`, `/inicio`, `/`
5. Si hay cambios en landing: `./scripts/build-cloudflare-preview.sh` + `wrangler pages deploy ...`

---

## 10. Contacto / referencias

- Instagram: `@sr.pan_mx`
- WhatsApp landing: `5212281159021`
- Documentación landing detallada: [`docs/LANDING-SRPAN.md`](LANDING-SRPAN.md)