# CLAUDE.md

Guía para trabajar en este proyecto (Laravel + Livewire 3 + Vuexy/Bootstrap).

## Comunicación y protocolo de trabajo

- **Respuestas concisas.** Nada de bloques de texto densos: párrafos cortos o listas, directo a lo
  que importa.
- **Propuesta antes de codificar, salvo en tareas triviales.** Ante un fix, mejora o módulo no
  trivial, presentar primero un plan de acción y esperar aprobación explícita antes de tocar
  código. No aplica a typos, fixes de una línea, ni cuando el usuario pide explícitamente que se
  haga directo. Una vez aprobado el plan, ejecutar hasta terminar sin volver a preguntar por cada
  paso intermedio.
- **Ningún fix, feature o refactor no trivial se da por terminado sin actualizar su documentación.**
  Si el tema ya tiene un doc en `docs/`, actualizarlo con el detalle técnico del cambio. Si es un
  tema nuevo, crear `docs/nombre-del-tema.md` y agregarlo de inmediato a la tabla del "Índice de
  documentación" al final de este archivo. `CLAUDE.md` en sí **no** se toca para documentar un fix
  puntual — solo para una regla/convención nueva de trabajo.

## Flujo de Git

Ramas fijas, **prohibido eliminarlas**: `production` (código en vivo) y `development` (rama de
integración activa — no `develop`). Todo trabajo nuevo se hace en una **rama pivote** creada desde
`development` (p. ej. `pivot/i18n-ficha-tecnica`), nunca commiteando directo sobre `development`.

- **Cuando el usuario da la tarea por terminada** ("fix terminado", "listo", "dalo por cerrado",
  "tarea cerrada"...), cerrar la pivote solo, sin preguntar paso a paso — este es el único caso en
  que una rama se borra sin pedir permiso de nuevo, porque el propio flujo ya lo verifica. Esto es
  más que solo commitear: implica merge + push + borrado de la pivote, no un commit suelto:
  1. `git merge --no-ff <pivote>` sobre `development`.
  2. `git rev-list --count development..<pivote>` → tiene que dar **0** (confirma que todo quedó
     fusionado; no vale a ojo). Si no da 0, parar y avisar — no borrar.
  3. `git push origin development`.
  4. `git branch -d <pivote>` (con `-d` minúscula, nunca `-D`: si por algo no estuviera del todo
     fusionada, git se niega solo y avisa, así que no hay forma de perder trabajo).
  Solo aplica si, además, la tarea de esa pivote quedó realmente cerrada (sin pendientes propios
  abiertos en `docs/PENDIENTES.md` — p. ej. un paso manual todavía pendiente en el servidor real
  bloquea el borrado hasta que se resuelva). Si la pivote también se había pusheado a `origin`,
  borrarla ahí también (`git push origin --delete <pivote>`).
- Esta regla es **de acá en adelante**; no aplica retroactivo a pivotes viejas que ya se dejaron
  como respaldo bajo el criterio anterior. `pivot/editor-web` y `merge/production-fix-fusion` siguen
  el suyo propio ("semanas estables en producción", ver `docs/PENDIENTES.md`) — para esas dos (y
  cualquier rama fuera de este flujo) sigue rigiendo: ⛔ **no se borran sin preguntar antes,
  nombrándolas explícitamente.**
- Prohibido siempre: `git push --force` y borrar `development`/`production` directamente.
- **Antes de cada commit:** `git status` y `git diff` para revisar qué se está confirmando.
- ⛔ **Prohibido correr Pint sobre todo el proyecto** (`vendor/bin/pint` a secas, sin `--dirty`).
  Solo se corre sobre los archivos que la tarea modificó o creó, y una sola vez: cuando la tarea ya
  terminó por completo, no en pasos intermedios.
  ```bash
  vendor/bin/pint --dirty
  ```

## Estado del proyecto

ℹ️ **El deploy normal NO copia archivos: `deploy.sh` trae el código con `git pull`.** Como `git pull`
también borra lo eliminado, un archivo que se quita del repo desaparece solo en el servidor.

⚠️ **Si copiás archivos a mano** (`rsync`/`scp`: contenido de `storage`, un volcado, un logo suelto),
correr siempre después (el rsync corre como el usuario SSH y `www-data` no puede escribir en lo que
crea):

```bash
sudo chown -R www-data:www-data storage bootstrap/cache public/sitemaps public/storage
```

`URL_BACKUP` apunta a `/var/backups/group-itg.com`, **fuera del proyecto**, y esa carpeta tiene que
existir y ser de `www-data` o `backup:run` muere en `ZipArchive::close()`.

## Arquitectura y stack

- **Backend/admin:** Laravel + Livewire 3, layout **Vuexy/Bootstrap** (`build-users/assets`), **sin
  Tailwind ni WireUI**. Bundles propios `resources/css/admin.css` + `resources/js/admin.js`.
  `resources/css/app.css` es el bundle viejo con Tailwind — solo vistas legacy, no mezclar.
- **Filament PHP** se usa exclusivamente para el **Editor Web** (page builder: pages, homes,
  sectores, bloques).
- **Frontend/Guest:** layouts propios (`layouts/guest/*`) para vistas públicas y no logueadas.

## Validación, autorización y alertas

- **El proyecto es casi enteramente Livewire** (no hay `FormRequest` en uso hoy). La validación va
  con `$this->validate()`/`#[Validate(...)]` inline en el componente, o un **Form Object** de
  Livewire (`php artisan make:form`) para formularios grandes — ya hay 2 en el proyecto. Un
  `FormRequest` (`php artisan make:request`) solo aplica al puñado de Controllers tradicionales que
  reciban un formulario clásico (no Livewire); si se elige usar uno ahí, que centralice las reglas
  de ese controller — no es un estándar retroactivo para lo que ya existe.
- **Policies** (`php artisan make:policy`) solo cuando el caso de uso lo amerite, no por defecto —
  ya existen `SaleTicketPolicy`, `PurchaseTicketPolicy`, `SaleUsedMachinePolicy`. Igual que el resto
  del proyecto, deben resolver por **permiso** (`$user->can('...')`), nunca por rol — ver "Permisos
  y roles" abajo.
- **Alertas en Livewire:** paquete `jantinnerezo/livewire-alert` (`$this->alert($type, $title,
  ['text' => $mensaje])`), título corto + mensaje en `text`. **No existe** SweetAlert vía
  `dispatch('swal:...')` en este código — no usarlo. Admin sin Livewire: alertas nativas de la
  plantilla, no `$this->alert()`.

> **👉 Convención completa de alertas (título/text, excepciones, ejemplos): `docs/CLAUDE-VUEXY.md`
> §3.3.**

## Internacionalización (i18n) — TODO texto de usuario en `__()`

El proyecto usa **traducciones JSON de Laravel** (`lang/en.json`, `lang/es.json`, `lang/fr.json`,
`lang/it.json`, `lang/pt.json`, `lang/zh_CN.json`) con la **cadena en INGLÉS como key**.

**Regla general:** todo lo que **ve el usuario final** debe ir en formato de traducción `__('...')`:
- Texto plano, labels, placeholders, títulos, botones, tooltips y opciones de select en blades.
- Sweet alerts / `$this->alert(...)` / `notify(...)` (title y text/message).
- Mensajes flash (`session()->flash(...)`, `->with(...)`, redirect).
- Mensajes de validación (`messages()`, atributos) y de `app/Rules/*`.
- Notificaciones campanita (`app/Notifications/**` → `title()`/`message()`).
- Excepciones que terminan mostrándose al usuario (alert/flash/notify).

**Excepción — SIEMPRE en inglés (NO traducir):**
- Mensajes de `Log::info/warning/error/debug(...)` y excepciones solo logueadas/técnicas internas
  → deben quedar en inglés para poder buscarlas fácil en los logs.
- **La pantalla de roles y permisos (`/roles`, gestión de superadmin) NO se traduce.** Solo la ve el
  superadmin, es administración interna y su vocabulario es el del código (`Rank`, `Permissions`,
  `Sensitive`, `Superadmin only`, `Users & access`, `kanban lanes`…). Sus ~30 cadenas aparecen como
  "faltantes" al comparar `en.json` con los otros idiomas: **es a propósito, no hay que completarlas.**

**Cómo:**
- Blade: `{{ __('Texto') }}`; en atributos/directivas `placeholder="{{ __('...') }}"` o `__('...')`.
- PHP: `__('Texto')`. En `<script>` inline: `@js(__('...'))`.
- Interpolación con placeholders (nunca concatenar): `__('Ticket #:id ...', ['id' => $x])`.
- NO doble-envolver lo que ya tiene `__()`. NO tocar keys de arrays, enums, rutas, iconos,
  clases CSS, `wire:`, formatos de fecha ni IDs.
- **Correos (Mail/Mailable / `app/Services/EmailTicket/**`):** hoy quedan EN INGLÉS a propósito
  (tarea de correos multi-idioma en curso, ver índice de documentación).

> Convención i18n del proyecto: key en INGLÉS Sentence case + valor en los 6 JSON
> (`en/es/fr/it/pt/zh_CN`), reutilizar keys existentes, no duplicar.

## Permisos y roles — TODO se gatea por permiso, NUNCA por rol

**Regla:** no queda ni un `hasRole()` en `app/`, `routes/` ni `resources/`. Cualquier control de acceso nuevo
va por permiso (`can()`, `@can`, `middleware('permission:...')`, `Gate::authorize()`). La única excepción
legítima es `config/roles.php → hierarchy`, porque la jerarquía **es** un concepto de rol.

- **`PermissionSeeder` es la fuente de verdad.** Cambiar el reparto de permisos = editar el seeder y
  deployar, nunca desde la UI — **`/roles` es SOLO LECTURA** (se revertiría en silencio en el próximo
  deploy).
- **Spatie no puede DENEGAR:** `hasPermissionTo()` es `directos || vía-rol`. Se puede sumar un
  permiso a un usuario pero **NO restar** uno que da su rol.
- Tras cambiar permisos: `permission:cache-reset` y **reiniciar el worker** (mantiene su propia copia
  en memoria).

> **👉 Detalle completo (carriles del kanban, superadmin, diff en el deploy, permisos por usuario,
> correos internos, comandos pendientes, checklist): `docs/PERMISOS-ROLES-SEGURIDAD.md`.**

## Base de datos / entorno

- La DB conectada es **LOCAL de pruebas**, NO producción. Sus registros son de prueba y
  **NO representan datos reales de prod** — no basar decisiones (ni lógica de migraciones) en
  conteos/estado de los registros locales.
- **NUNCA** limpiar / vaciar / hacer wipe / `migrate:fresh` / `db:wipe` de la DB local. No borrar registros.
- **`php artisan db:seed` (el seeder completo) pide autorización explícita antes de correrlo**:
  puede reinsertar filas base sobre datos ya editados a mano en local. Los seeders puntuales
  pensados para volver a correrse (p. ej. `PermissionSeeder` en cada deploy) son la excepción ya
  documentada en "Permisos y roles".
- **`php artisan test` solo con filtro** (`--filter=...` o apuntando a un archivo/carpeta puntual).
  `Pest.php` aplica `RefreshDatabase` a toda la suite, así que correrla completa sin filtrar pisa la
  DB local real.
- Las **migraciones de datos** deben escribirse de forma **general y robusta** (idempotentes, con
  dedup y guards `Schema::hasTable`), pensadas para correr **en prod** y migrar correctamente los
  registros reales — no para el estado puntual de la DB local.

## Gotchas

### Errores de service: `guard()` en Livewire, nada en los controllers

Los **services lanzan** el error (`abort(403, 'mensaje user-facing')`) y **la UI lo muestra**, sin
try/catch repetido en cada componente. Lo hace `App\Livewire\Traits\HandlesServiceExceptions`
(13 componentes lo usan):

- **Livewire** → envolver **TODA la acción** en `$this->guard(fn () => ...)`: la llamada al service,
  el `alert()` de éxito y el `nextStep()`/`redirect()`. Así el éxito **solo corre si no hubo
  excepción**. Si la acción devuelve un redirect, `return $this->guard(...)`.
  ⚠️ **`validate()` va FUERA del `guard()`**: la `ValidationException` la maneja Livewire pintando
  los errores de campo, y atraparla la convertiría en un toast genérico.
- **Controllers** → **NO** llevan `guard()` (no tienen `$this->alert()`). El `abort()` sube al
  handler de Laravel y pinta la vista `errors/403` sola, que ya imprime `$exception->getMessage()`.
- **Qué mensaje se ve:** `AuthorizationException` y `HttpException` muestran el mensaje **del
  service** (por eso va en `__()`); cualquier otra excepción muestra un texto genérico y se
  **loguea** — los errores técnicos internos no se filtran al usuario, y por eso se dejan en inglés.

### Composer: NO correr `composer update` a secas

`composer update` completo **falla** (bloquea en advisories de `laravel/framework` 11.x/12.x, no es
un problema de este repo). Usar siempre **update dirigido**: `./vendor/bin/sail composer update
vendor/paquete`. **Subir a Laravel 12 está PROHIBIDO** por ahora.

> **👉 Por qué falla y detalle completo: `docs/CLAUDE-COMPOSER.md`.**

### Comandos artisan de un solo uso: cuándo se pueden borrar

Un comando **se queda** solo si sigue teniendo trabajo que hacer: lo invoca el deploy o una migración
(ej. `slugs:sync`, `media:brand-images-to-storage`), está agendado (`notifications:prune`,
`slugs:prune-history`), es diagnóstico/reparación de un problema que puede volver (`links:audit`,
`links:repair`, `media:audit-public-images`, `page-blocks:repair-wrappers`) o son datos que hay que
regenerar al cambiar el catálogo (`nav:sync-division-menus`). **Si ninguno aplica y su output ya vive
en la DB de producción** (no se re-siembra corriendo comandos, se copia la base), el comando es
código muerto y se borra — así se dieron de baja 9 backfills el 2026-09-08
(`docs/MIGRACION-VISTAS-DB-PENDIENTES.md` §17). No borrar el código que los explica: las secciones
que quedan documentan por qué los datos son como son.

## Índice de documentación (`docs/`)

Todo el conocimiento específico de cada subsistema vive acá, no en este archivo. Consultar **antes**
de tocar el tema correspondiente:

| Doc | Cubre |
|---|---|
| `PENDIENTES.md` | **Único lugar con el checklist de pendientes** del proyecto: limpiezas sueltas (ramas, comandos de un solo uso, tablas/archivos huérfanos) y el resumen de las tareas grandes en curso. Un pendiente nuevo se agrega ahí, no como tabla suelta en otro doc. |
| `CLAUDE-SLUGS-CATALOGO.md` | Catálogo de slugs/redirects (`slugs`, `slug_history`), enrutado de contenido, `publicPathPattern()`, `ContentController`. |
| `CLAUDE-SEO-RASTREO.md` | `robots.txt` por división, `noindex` vs `Disallow`, sitemaps, Search Console. |
| `CLAUDE-PANEL-RENDIMIENTO.md` | Rendimiento de los modales del Editor Web (Filament): `LazyTabs`, `LocaleSwapEditor`, `LocaleTabs`, la trampa de `dehydratedWhenHidden()`. |
| `MIGRACION-VISTAS-DB-PENDIENTES.md` | Contenido por división (homes/about/partners/contact), títulos del panel, logos/banderas/`BrandLogo`, `$blog->image`, limpieza de `public/images`, comandos de backfill dados de baja. |
| `CLAUDE-CSS-PAGE.md` | Reglas de edición de `resources/css/page.css` (especificidad, `!important`, alcance por bloque). |
| `CLAUDE-VUEXY.md` | Migración de vistas a Vuexy/Bootstrap: inputs, modales, notificaciones (`livewire-alert`, convención título/`text`), gotchas (incluye `input-group-merge` sin `<form>`). |
| `ESTRUCTURA-TICKETS-BLADE.md` | Mapa de las vistas de tickets (Sale/Purchase/máquina usada): qué hereda de qué, wizards de Spatie. |
| `I18N-TICKETS-ARCHIVOS.md` | Registro de la migración i18n de UI de tickets (checklist 185/185) + glosario ES/IT de maquinaria aprobado por el cliente. |
| `I18N-CORREOS-IDIOMA-USUARIO.md` | Tarea en curso: correos en el idioma preferido del usuario (`preferred_language`, T1-T4). |
| `PERMISOS-ROLES-SEGURIDAD.md` | Detalle de permisos: carriles del kanban, superadmin, `/roles`, diff en el deploy, permisos por usuario, correos internos. |
| `CLAUDE-DEPLOY-MANTENIMIENTO.md` | Modo mantenimiento del deploy (`down --render`), vista 503, `DEPLOY_BYPASS_SECRET`, familia de vistas de error. |
| `CLAUDE-DEPLOY-PRODUCCION-2026-09-17.md` | Bitácora del deploy en caliente del page builder: verificación, permisos +x, pendientes menores. |
| `IMPORTAR-CONTENIDO-EN-PROD-EN-CALIENTE.md` | Procedimiento para importar contenido a producción sin sustituirla. |
| `DESARROLLO-LOCAL.md` | Servicios locales (SOLO Docker/Sail): Mailpit, cola de correos/worker. |
| `CLAUDE-COMPOSER.md` | Por qué `composer update` completo falla y cómo hacer updates dirigidos. |
| `CLAUDE-COMANDOS-ARTISAN.md` | Mapa de todos los comandos artisan del proyecto: programados, invocados por el deploy o una migración, de la tarea de importar contenido, y de diagnóstico/reparación manual. |
| `CLAUDE-TIPTAP-COLOR-BUG.md` | Bug de `filament-tiptap-editor`: el color de texto pegado de Word se pierde al publicar (`str_contains` confundido con `-webkit-tap-highlight-color`); fix vía `App\Support\Tiptap\Extensions\FixedColor`. |
| `CLAUDE-CONTACT-REQUEST-ATTACHMENT.md` | Bug: adjunto de Contact Request daba 404 (mismatch disco `local` vs `public`); fix con preview `data:` URI + `hintAction` de descarga, sin rutas nuevas. |
| `CLAUDE-CONTACT-REQUEST-NOTIFICATION.md` | Aviso en la campanita del Editor Web por cada solicitud de contacto nueva (`access_web_editor`): observer, cola, convivencia con la campanita Vuexy en la tabla `notifications`. |
| `CLAUDE-CATALOGUE-FORM-BUG.md` | Bug: formulario de catálogo de máquinas fallaba siempre (`$machine->name` no existe, debía ser `$machine->translation->name`) — lead/correo perdido en cada solicitud. |

