# Tarea: Correos multi-idioma según idioma preferido del usuario

> **Estado:** ✅ **CERRADA** (2026-09-21). T1–T4 + mejora de dashboards por rol en producción desde
> finales de julio 2026 (`ca09fdcf`, `8af34ab4`, `6ed03b1e`, `f4002dfe`, ~2026-07-28), funcionando en
> uso real (confirmado por el usuario) — no quedó registro de una corrida formal en Mailpit/Sail, pero
> el módulo lleva más de un mes operando en prod. `machinery_manager` y `preferred_language` verificados
> presentes en seeders/modelo. Sin pendientes.
> **Relación con la tarea anterior:** la migración i18n de UI (`docs/I18N-TICKETS-ARCHIVOS.md`) está
> **CERRADA/COMPLETA**. Esta fue una tarea independiente, con su propia lógica, separada del scope
> `__()` de UI.

---

## 🧭 RESUMEN DE ESTADO

### ✅ Tareas REALIZADAS (código completo)
- [x] **T1 — Traducir cuerpos de correo:** `TicketMailable` + `emails/tickets/layout.blade.php` + 13 blades de cuerpo +
      `partials/button` + 66 keys (6 JSON). 11 servicios `EmailTicket/**` migrados. `ContactMailable`/3 forms de contacto intactos.
- [x] **T2 — Idioma preferido:** migración `preferred_language` en `users` (idempotente) + `HasLocalePreference` +
      `User::localeForEmail()` + selector en perfil (staff y customer, vía `Language`) + 8 correos de cliente con `->locale(...)`.
- [x] **T3 — Rol `machinery_manager` + correos internos + jerarquía:** rol + permiso `receive_ticket_admin_emails` (solo
      machinery_manager); 5 correos internos vía trait `NotifiesTicketAdmins` (localizados a holders + copia solo-BCC inglés,
      `machinery@` solo `From`); jerarquía `config/roles.php` + helpers `User` + gating en CreateUser/EditUser/UserTable.
- [x] **T4 — Redirección a dashboards:** `dashboardRoute()` permission-driven + ruta canónica `/dashboard` + bug `route('dashboard')` arreglado.
- [x] **Mejora dashboards por rol:** dashboard **customer** (sus tickets) + dashboard **staff** (resumen tipo admin + accesos) +
      **default** sin permiso (fallback). Rename welcome→staff. Navbars → `route('dashboard')`. `TicketKanbanStatsService` (DRY).

### ⏳ Tareas PENDIENTES — **DEJADAS PARA DESPUÉS** (correr en Sail + pruebas)
> Nada de esto se ejecutó en el entorno del asistente (no hay PHP/Sail). Todo el código está listo.

**Comandos a correr (en orden):**
```bash
./vendor/bin/sail artisan migrate                            # T2: columna preferred_language en users
./vendor/bin/sail artisan db:seed --class=RoleSeeder         # T3: rol machinery_manager
./vendor/bin/sail artisan db:seed --class=PermissionSeeder   # T3 + dashboards: permisos, rename view_welcome→view_staff, view_customer
./vendor/bin/sail artisan route:clear                        # T4/dashboards: rutas nuevas/renombradas
./vendor/bin/sail artisan view:cache                         # compila blades nuevos (correos + dashboards)
./vendor/bin/sail artisan queue:work --tries=3               # worker (los correos se encolan)
```

**Acción manual pendiente:**
- [x] Crear la cuenta **`machinery@group-itg.com`** como usuario con rol `machinery_manager` (hecho vía `UserSeeder`, password temporal `password`).

---

## 🔜 PENDIENTES PRÓXIMA SESIÓN (agregadas 2026-07-21, ejecutar mañana)

1. **Crear rol `superadmin`** — máximo nivel de la jerarquía (rank 100 en `config/roles.php`, ya previsto). Agregarlo en `RoleSeeder` + `PermissionSeeder` (todos los permisos, incl. `access_sandbox`) y validar que la jerarquía `canManageUser()/assignableRoles()` lo trate como tope (solo un superadmin gestiona admins).
2. **Cambiar la contraseña de `machinery@group-itg.com`** — el `UserSeeder` la dejó como `password` (temporal). Poner una contraseña real/segura (no dejar la default en prod).
3. **Verificar en PRODUCCIÓN qué usuarios tienen rol `admin`** — revisar si deben migrarse a `machinery_manager` (que es admin sin Filament ni sandbox) o quedarse como `admin`. Confirmar que la migración de roles se aplicó correctamente y ningún usuario quedó sin el rol esperado.

> ⚠️ Recordar: la DB local NO refleja prod. La verificación del punto 3 debe hacerse contra los datos reales de producción.

**Pruebas pendientes (Mailpit `http://localhost:8025`):**
- [ ] Correos de **cliente** llegan en el idioma del perfil (probar es/it/pt/fr/zh_CN).
- [ ] Correos **internos** llegan a los usuarios con `receive_ticket_admin_emails` en su idioma + copia solo-BCC en inglés; **admin NO** recibe.
- [ ] **Jerarquía** en la tabla de usuarios (machinery_manager no toca admin; admin no toca admin; ID 1 protegido).
- [ ] **Login por rol** cae en su dashboard (admin/machinery_manager→admin; staff→staff; customer→customer; rol sin permiso→default).
- [ ] Navbars: header del dropdown y botón del navbar de tickets llevan al dashboard; "My profile" sigue yendo al perfil.
- [ ] Link de verificación de email ya no rompe (`route('dashboard')` existe).

---

## 🎯 Objetivo

Que **los correos de la plataforma lleguen al usuario en su idioma preferido** (en/es/fr/it/pt/zh_CN), tanto para
**staff** como para **customer**. El idioma se decide por un **campo nuevo en `users`** que el usuario puede
**editar en su perfil**. Antes de enviar cada correo, su contenido (asunto + título + cuerpo) se **renderiza en ese idioma**.

> ⚠️ **Cambio de decisión de negocio:** en la tarea anterior los correos (`app/Services/EmailTicket/**` + Mailables)
> se dejaron **en inglés a propósito**. **Esta tarea revierte esa decisión**: ahora los correos SÍ se traducen.

---

## ✅ DECISIONES CERRADAS (no volver a preguntar)

| # | Tema | Decisión |
|---|------|----------|
| 1 | **Nombre del campo idioma** | `preferred_language` en `users` — `string(10)` **nullable**, valores `en\|es\|fr\|it\|pt\|zh_CN`. Default `null` → fallback al locale de la app. **Guarda SOLO el código de idioma**, nada más. |
| 2 | **Destinatarios de correos internos** | Por **PERMISO nuevo** `receive_ticket_admin_emails`. Se envía a `User::permission('receive_ticket_admin_emails')->get()`, **cada uno en su idioma**. NO por rol directo (más flexible, idiomático Spatie). |
| 3 | **Buzón `machinery@group-itg.com`** | **DEJA de recibir** correos (reemplazado por los usuarios con el permiso). ⚠️ Sigue siendo el **`from`/remitente** (`fromAddress`) — eso NO cambia. |
| 4 | **Notificaciones campanita** (`app/Notifications/Tickets/**`) | **FUERA DE SCOPE**. Ya están traducidas con `__()` (verificado: las 10 clases). Se renderizan con el locale de sesión de quien las ve. NO se tocan. |
| 5 | **Correos al cliente** (8 envíos `->to($userEmail)`) | Resolver el `User` por email, aplicar su `preferredLocale()`, fallback al locale app si `null`. |
| 6 | **Rol nuevo** | `machinery_manager`. Alcance: **todas las funciones de tickets + gestión de usuarios**, pero **SIN Filament** (editor web/landing) y **SIN `access_sandbox`**. |
| 7 | **Editor web / landing** | Es el **panel de Filament**, gateado por `hasRole('admin')` en `User::canAccessPanel()`. Como `machinery_manager` NO es `admin`, queda **excluido automáticamente** (no se toca Filament). |
| 8 | **Convención i18n** | La misma de la UI: key en **INGLÉS Sentence case** + valor en los **6 JSON** (`en/es/fr/it/pt/zh_CN`), reusar keys existentes, no duplicar. |

### 📝 Registro de preguntas resueltas (Q&A con el usuario, 2026-07-21)
- **¿Cómo se modela quién recibe los correos internos?** → **Permiso nuevo** `receive_ticket_admin_emails` (no rol).
- **¿Qué pasa con el buzón `machinery@`?** → **Dejar de enviarle** (reemplazo total por usuarios con el permiso).
- **¿Alcance del rol `machinery_manager`?** → **Tickets + gestión de usuarios** (`view_users`, `create_user`, `edit_user`,
  `view_user_info`), pero **sin Filament ni sandbox**.
- **¿Nombre del rol?** → **`machinery_manager`**.
- **¿El campo guarda solo el código?** → Sí, solo el código de idioma.

---

## 🔑 Contexto técnico ya verificado (para no re-investigar)

**Correos:**
- Los 11 servicios `app/Services/EmailTicket/**` arman el HTML del cuerpo **inline con heredoc** (`<<<HTML … HTML;`),
  con `title`/`subject`/`emailContent` **hardcodeados en inglés**, y los pasan a `App\Mail\ContactMailable`
  → blade `resources/views/emails/contact-email.blade.php`.
- Se envían **encolados**: `Mail::mailer('smtp-machinery')->to(...)->queue(new ContactMailable(...))`. `QUEUE_CONNECTION=database`
  → **requieren worker** (`sail artisan queue:work`). Probar con **Mailpit** (`http://localhost:8025`).
- Hoy se envía a **email string** (`->to($userEmail)` / `->to('machinery@...')`), NO a modelo `User` →
  `HasLocalePreference` no aplica solo; hay que **enviar a `->to($user)` (modelo)** o pasar **`->locale($lang)`** explícito.

**Clasificación de los 13 envíos (verificado por grep):**
- **8 al CLIENTE** (`->to($userEmail)`): `SaleMachinePublishedNotificationService`, `SaleMachineSoldNotificationService`,
  `SaleTicketReturnNotificationService`, `SaleNotificationService` (rama cliente), `PurchaseTicketApprovedNotificationService`,
  `PurchaseTicketSoldNotificationService`, `PurchaseTicketReturnNotificationService`, `PurchaseNotificationService` (rama cliente).
- **5 INTERNOS** (`->to('machinery@group-itg.com')`): `SaleNotificationService` (rama admin),
  `SaleObservationFinishNotificationService`, `UsedMachineRequestMailService`, `PurchaseNotificationService` (rama admin),
  `PurchaseObservationFinishNotificationService`.

**Roles y permisos (Spatie `spatie/laravel-permission`):**
- Roles actuales: `admin`, `customer`, `commercial`, `marketing`, `validator` (`database/seeders/RoleSeeder.php`).
- Permisos y su asignación a roles: `database/seeders/PermissionSeeder.php`. El array `$permissions` es **single source of truth**
  (poda con `whereNotIn(...)->delete()`, idempotente). Cada rol se arma con `->syncPermissions([...])`.
- El editor web/landing = **panel Filament**, gateado SOLO por `hasRole('admin')` en `User::canAccessPanel()`.
- **Todas las funciones de tickets se gatean por PERMISO** (`can:...` en rutas, `@can` en vistas), NO por rol → un rol nuevo
  con los permisos correctos tiene acceso completo a tickets. `isAdmin()` (`User.php`) **no se usa en ningún lado**.
- La campanita interna ya usa el patrón `User::role(['validator','admin'])->get()` (ej. `SaleTicketService::notifyRoles()`).
- Validación de rol al crear/editar usuario: `app/Livewire/User/CreateUser.php:47` y `EditUser.php:66`
  → `'in:admin,customer,commercial,marketing,validator'` (hay que **agregar `machinery_manager`**).
- `User::dashboardRoute()` decide el dashboard con `match($this->roles->first()->name)`: solo `'admin' => admin.dashboard`,
  resto → `welcome.dashboard`. ⚠️ Solo mira el **primer rol** (ver Tarea 4).

---

## 📋 TAREAS (orden de ejecución)

### ✅ Tarea 1 — Traducir los cuerpos de los correos (i18n) — **HECHA (falta test de render en Sail)**
> Deja los correos **traducibles**. Es prerequisito de las Tareas 2 y 3 (sin esto, cambiar el locale no cambia nada).

**Decisión de implementación (esqueleto NUEVO y aislado):** NO se tocó `ContactMailable` ni su blade
`emails/contact-email.blade.php` (los usan 3 forms de contacto públicos fuera de scope: `ContactProductsPost`,
`ContactRequestPost`, `CatalogueContactForm`). En su lugar se creó un Mailable + layout propios de tickets.

**Qué se CREÓ:**
- `app/Mail/TicketMailable.php` — Mailable nuevo. **Difiere el render**: recibe `bodyView` + `bodyData` +
  `titleKey`/`titleParams` + `subjectKey`/`subjectParams` (resuelve `__()` dentro de `envelope()`/`content()`).
  Esto deja T2 trivial: basta `Mail::...->locale($lang)` y **todo** (asunto+título+cuerpo+footer) sale en ese idioma.
- `resources/views/emails/tickets/layout.blade.php` — esqueleto propio (logo, título, footer traducido, `<html lang>` dinámico, `@include($bodyView,$bodyData)`).
- `resources/views/emails/tickets/partials/button.blade.php` — botón CTA reutilizable (`$url`,`$label`,`$align`).
- **13 blades de cuerpo** en `resources/views/emails/tickets/**` (uno por envío) con `__()`; observación del cliente ahora **escapada** (`{{ }}`).
- **66 keys nuevas** en los 6 JSON (`en/es/fr/it/pt/zh_CN`), Sentence case. Reusadas: `Brand, Model, Dashboard, Email, Country, Name, Phone, Message, Division`.

**Qué se CAMBIÓ:** los 11 servicios `EmailTicket/**` → usan `TicketMailable` (view+datos+keys) en vez de heredocs.
`machinery@` sigue como `fromAddress` (remitente), sin cambios de destinatarios en esta tarea.

**Pendiente de verificación (requiere Sail + worker + Mailpit):** `sail artisan view:cache` (compila blades),
disparar un correo de cada tipo y verlo en `http://localhost:8025`. No se pudo compilar en el entorno de desarrollo del asistente.

**Nota para T2:** como el render está diferido en `TicketMailable`, T2 solo agrega `->locale($lang)` al envío
(no hace falta `App::setLocale` manual ni `view()->render()` en el service).

---

### ✅ Tarea 2 — Lógica del idioma preferido — **HECHA (falta correr migración + test en Sail)**
> Depende de Tarea 1.

**Qué se AGREGÓ:**
- **Migración** `database/migrations/module_core/2026_07_21_000000_add_preferred_language_to_users_table.php`:
  columna `preferred_language` (`string(10)` nullable) en `users`, idempotente (`Schema::hasTable` + `Schema::hasColumn`),
  robusta para prod. (module_core se auto-carga vía `AppServiceProvider::loadMigrationsFrom`.)
- En `User`: `implements FilamentUser, HasLocalePreference`; `preferredLocale(): ?string` (devuelve `preferred_language ?: null`);
  `preferred_language` en `$fillable`; helper estático **`User::localeForEmail(?string $email): string`** (resuelve el
  `preferred_language` por email, fallback a `app()->getLocale()` — sirve para los correos que van a un email string).
- En el **perfil** (Volt `update-profile-information-form.blade.php`): `<select>` de idioma para **staff y customer**
  (fuera del bloque `@if customer`), opciones desde el modelo `Language` (endónimos: English/Español/Italiano/Português/
  Français/中文) + opción "Use site language" (guarda `null`); validación `Rule::in(Language::pluck('language_code'))`;
  guardado en `update([... 'preferred_language' => ... ?: null])`. Datos a la vista vía `with()`.
- **2 keys nuevas** en los 6 JSON: `Preferred language for emails`, `Use site language`.

**Qué se CAMBIÓ:**
- **8 correos al cliente** ahora aplican `->locale(User::localeForEmail($userEmail))` antes de `->queue(...)`
  (render diferido de `TicketMailable` → sale todo en ese idioma). Los internos siguen sin locale (van en T3).

**Pendiente de verificación (Sail):** `sail artisan migrate` (crea la columna), editar el idioma en el perfil y comprobar
en Mailpit que un correo de cliente llega en ese idioma. No se pudo correr en el entorno del asistente.

**(Opcional, NO hecho)** setear idioma también en alta de usuario (`CreateUser`) y registro guest (`RegisterStepForm`).

---

### ✅ Tarea 3 — Rol `machinery_manager` + reasignar correos internos — **HECHA (falta seed + test en Sail)**
> Depende de Tareas 1 y 2. Cierra la lógica de correos internos.

**Implementado:**
- **Rol** `machinery_manager` en `RoleSeeder`; **permiso** `receive_ticket_admin_emails` en `PermissionSeeder` (array
  single-source-of-truth). `machinery_manager` = permisos de tickets/usuarios de `admin` **menos `access_sandbox`** + `receive_ticket_admin_emails`.
  Permiso asignado **solo a `machinery_manager`** (decisión del usuario).
- **Correos internos (5)**: trait `App\Services\EmailTicket\Concerns\NotifiesTicketAdmins::queueToTicketAdmins()`:
  1 copia **localizada** por cada `User::ticketAdminRecipients()` (`->to($user)`, auto-idioma vía HasLocalePreference) **+**
  **1 copia solo-BCC** (sin `To`) a la lista fija de devs/stakeholders en **inglés** (`->locale('en')`).
  ⚠️ **Decisión final:** `machinery@` **NO se usa como `To`** (evita duplicado): será **una cuenta de usuario con el permiso**
  `receive_ticket_admin_emails`, así que recibe por el loop como cualquier otro. `machinery@` queda **solo como remitente `From`**
  (const `ADMIN_EMAIL`, comentada su función en cada service). Los BCC de devs (`ismaharo18@`, `luiscastellanos.dv@`) + `marketing@`/`j.spano@` se conservan.
- 📌 **PENDIENTE (acción del usuario):** **crear la cuenta `machinery@group-itg.com` como usuario con rol `machinery_manager`**
  (o al menos con el permiso `receive_ticket_admin_emails`) para que reciba las notificaciones internas.
- **Jerarquía de roles** (`config/roles.php`: superadmin>admin>machinery_manager>staff>customer). Helpers en `User`:
  `roleRank()`, `canManageUser()`, `assignableRoles()`. `CreateUser` limita las opciones de rol a `assignableRoles()`
  (validación server-side con `Rule::in`); `EditUser::mount()` gatea con `canManageUser()`; `UserTable::actionRules()`
  oculta editar cuando el actor no supera al target (y para ID 1). Badge + labels de `machinery_manager` agregados.
- `dashboardRoute()` incluye `machinery_manager` → `admin.dashboard`. Validación `type_user` ya no hardcodeada.
- Keys nuevas en 6 JSON: `Machinery manager`, `Unauthorized to edit this user.`

**⚠️ Cambios de comportamiento a tener en cuenta:**
- La jerarquía es **estricta**: un `admin` ya **no** puede editar a otro `admin` (solo un `superadmin` futuro, rank 100).
- `UserTable::actionRules()` estaba **comentado**; ahora está **activo** (antes NADIE se ocultaba, ni ID 1). ID 1 sigue protegido.
- `EditUser` sigue con el `<select>` de rol **disabled** y `update()` **no reasigna** el rol (comportamiento previo, no tocado).

**Pendiente de verificación (Sail):** `sail artisan db:seed --class=RoleSeeder && sail artisan db:seed --class=PermissionSeeder`,
asignar un usuario a `machinery_manager`, y probar (a) que ese usuario recibe los internos en su idioma + copia archivo a machinery@,
(b) jerarquía en la tabla de usuarios.

**Qué se AGREGA:**
- Rol `machinery_manager` en `database/seeders/RoleSeeder.php`.
- Permiso nuevo `receive_ticket_admin_emails` en el array `$permissions` de `PermissionSeeder.php`.
- `->syncPermissions([...])` para `machinery_manager`: **todos los permisos de tickets/máquinas/dashboard que tiene `admin`**
  + **gestión de usuarios** (`view_users`, `create_user`, `edit_user`, `view_user_info`) + `receive_ticket_admin_emails`.
  **NO** incluir `access_sandbox`. (Filament ya queda excluido solo, no es permiso.)
- `machinery_manager` en la validación `type_user` de `CreateUser.php:47` y `EditUser.php:66`
  (`in:admin,customer,commercial,marketing,validator,machinery_manager`).

**Qué se CAMBIA:**
- **Refactor de los 5 correos internos**: de `->to('machinery@group-itg.com')` a un loop sobre
  `User::permission('receive_ticket_admin_emails')->get()`, enviando a cada `$user` **en su idioma**.
- **Ajustar `User::dashboardRoute()`** para incluir `machinery_manager` (que aterrice en el dashboard de tickets, no en welcome).

**Qué se VERIFICA:**
- Que **ningún correo** quede apuntando a `admin` ni a `machinery@` como **destinatario principal**; todos los internos
  pasan al permiso nuevo. (`machinery@` solo permanece como `from`.)

**🔐 Jerarquía de roles en la gestión de usuarios (parte de esta tarea):**
Como `machinery_manager` recibe permisos de gestión de usuarios, **NO debe poder crear/editar/borrar usuarios de rol
igual o superior al suyo** (p. ej. crear un `admin` o `superadmin`). Hay que agregar gating por **jerarquía**:
- **Archivos:** `app/Livewire/User/CreateUser.php`, `app/Livewire/User/EditUser.php`, vista/modal
  `resources/views/livewire/user/create-user.blade.php` + `edit-user.blade.php`, y la tabla `resources/views/users/user-table.blade.php`.
- **Qué se hace:** filtrar las opciones de `type_user` disponibles según la jerarquía del **actor** (solo roles estrictamente
  por debajo del suyo), y gatear los botones editar/borrar por fila en la tabla. Validar en servidor (no solo ocultar en la vista).
- **Borrado:** hoy **NO existe** acción de borrar usuarios (solo `CreateUser`/`EditUser`). Si se agrega, aplicar la misma jerarquía.
- **Jerarquía prevista:** `superadmin` (🔜 futuro, se agrega después) > `admin` > `machinery_manager` > resto. `superadmin`
  y su nivel se implementarán en una iteración posterior; dejar la lógica de jerarquía preparada para incluirlo.

---

### ✅ Tarea 4 — Mejorar el redireccionamiento a dashboards — **HECHA (falta test en Sail)**
> Independiente del resto.

**Problema resuelto:** `dashboardRoute()` decidía con `match($this->roles->first()->name)` — solo miraba el primer rol,
duplicaba la lógica de permisos que ya protege las rutas, y fallaba con múltiples roles.

**Implementado (patrón permission-driven + punto de entrada único):**
- `config/roles.php` → **`dashboards`** (mapa ordenado permiso→ruta) + `dashboard_fallback`.
- `User::dashboardRoute(): string` ahora recorre ese mapa y devuelve la **primera ruta cuyo permiso** tiene el usuario
  (`$this->can($permission)`). Alineado con el guard real de cada dashboard (fuente única de verdad), robusto con múltiples
  roles, y **no requiere tocar código** al agregar roles (solo el permiso de dashboard correcto).
- **Ruta canónica `/dashboard`** (`routes/web.php`, name `dashboard`, `auth`+`verified`) que redirige a `dashboardRoute()`.
- ✅ Arregla el **bug latente**: `route('dashboard')` (usado en `VerifyEmailController` y el `AuthenticatedSessionController`
  de Breeze) ya **existe**. El login Volt sigue usando `dashboardRoute()` + intended (sin cambios).

**Pendiente de verificación (Sail):** `sail artisan route:clear`, login con cada rol (admin/machinery_manager → dashboard-admin;
staff/customer → dashboard-welcome) y probar el link de verificación de email.

---

### ✅ Mejora extra — Dashboards por rol (apalanca T4) — **HECHA (falta seed + test en Sail)**
> Solicitada tras T4. Aprovecha el enrutado permission-driven de dashboards.

- **Customer** → dashboard propio nuevo `customer.dashboard` (`/dashboard-customer`, `CustomerDashboardController`,
  vista `dashboard/customer-dashboard.blade.php`, permiso **`view_customer_dashboard`**): totales de sus tickets de venta/compra,
  contador "Returned for correction", desglose por estado (enum `label()`), últimos 5 de cada tipo con link a `*-tickets.edit-own`,
  y accesos para crear/ver sus tickets. En el seeder, customer pasó de `view_welcome_dashboard` → **`view_customer_dashboard`**.
- **Staff** (commercial/marketing/validator) → `welcome.dashboard` **rediseñado** a dashboard general: resumen de tickets
  (totales venta/compra/total + máquinas publicadas) + accesos rápidos gateados por `@can` (kanban venta/compra, archivados,
  solicitudes de máquina usada, perfil). Mantienen `view_welcome_dashboard`.
- **Admin/machinery_manager** → sin cambios (dashboard admin).
- **Refactor DRY:** `App\Services\Dashboards\TicketKanbanStatsService::overview()` centraliza los conteos de tickets kanban;
  lo usan `DashboardAdminController` y `StaffDashboardController`.
- `config/roles.php → dashboards`: `view_dashboard_admin` → `view_customer_dashboard` → `view_staff_dashboard` (prioridad).

**Ajustes posteriores (mismo bloque):**
- **Rename welcome → staff** (claridad): vista `staff-dashboard.blade.php`, `StaffDashboardController`, ruta `staff.dashboard`
  (`/dashboard-staff`), permiso `view_staff_dashboard`. Actualizado en config, `SidebarService`, seeder y `User`.
- **Dashboard default (fallback seguro):** `DefaultDashboardController` + `dashboard/default-dashboard.blade.php` + ruta
  `default.dashboard` (`/dashboard-default`, **sin permiso**, solo auth+verified). `config/roles.php → dashboard_fallback = default.dashboard`.
  Un rol nuevo sin permiso de dashboard aterriza aquí (no 403).
- **Navbars → dashboard:** el encabezado del dropdown de usuario (`nav-bar-app`) y el botón del navbar de tickets/guest
  (`form-navbar`) ahora apuntan a `route('dashboard')` (canónica, resuelve por rol). El item "My profile" del dropdown se
  conserva para el perfil. `SidebarService` ahora lista también el dashboard de customer.
- **Keys** de dashboards en los 6 JSON (+ `Staff dashboard`, `Tickets overview`).

**Pendiente (Sail):** `db:seed --class=PermissionSeeder` (crea `view_customer_dashboard` + swap customer), `route:clear`,
`view:cache`, y probar login como customer (→ dashboard-customer) y como staff (→ welcome rediseñado).

## ✅ Checklist de aceptación global
- [x] **T1:** 11 servicios `EmailTicket/**` migrados a `TicketMailable` + layout + 13 blades de cuerpo + 66 keys en 6 JSON (Sentence case). `ContactMailable`/3 forms intactos.
- [x] **T2:** campo `preferred_language` (migración idempotente) + `HasLocalePreference` en `User` + `$fillable` + `localeForEmail()`.
- [x] **T2:** selector de idioma editable en el **perfil** (staff y customer, vía modelo `Language`) + validación + guardado.
- [x] **T2:** 8 correos de cliente con `->locale(User::localeForEmail($userEmail))`, fallback a locale de app.
- [x] **T3:** rol `machinery_manager` + permiso `receive_ticket_admin_emails` (solo machinery_manager) + permisos (tickets + usuarios, sin sandbox/Filament).
- [x] **T3:** 5 correos internos vía trait `NotifiesTicketAdmins` → localizados a holders del permiso + 1 copia archivo (inglés) a `machinery@`+BCC.
- [x] **T3:** `machinery_manager` en validación `type_user` (dinámica) y en `dashboardRoute()`.
- [x] **T3:** jerarquía `config/roles.php` + `User::assignableRoles()/canManageUser()`; gateado en CreateUser/EditUser/UserTable. Estricta (admin no edita admin). Preparado para `superadmin`.
- [x] **T4:** `dashboardRoute()` permission-driven (config `dashboards`), ruta canónica `/dashboard`, bug `route('dashboard')` arreglado.
- [x] **Global:** módulo en producción hace más de un mes, en uso real (confirmado por el usuario 2026-09-21). No quedó registro de una prueba formal en Mailpit, pero se considera cerrado.

---

## 🚫 Fuera de scope
- La UI ya está migrada (`docs/I18N-TICKETS-ARCHIVOS.md`, CERRADA). No re-tocar `__()` de vistas/Livewire/controllers.
- Notificaciones campanita: ya traducidas, NO se tocan.
- No cambiar Filament ni la lógica de negocio de tickets/colas (salvo lo listado arriba).

---

## Copia de archivo en inglés desactivada (2026-09-30) — reactivada con otros destinatarios el 2026-10-02 (ver sección siguiente)

`NotifiesTicketAdmins::queueToTicketAdmins()` mandaba, además de una copia por usuario con el permiso
`receive_ticket_admin_emails`, **una copia de archivo en inglés** con un dev como `To` directo
(`ARCHIVE_TO`) y el resto de `ADMIN_BCC` en BCC. Esa copia **ya no sale**: el bloque está comentado
(no borrado) para poder reactivarlo. Afecta a los 5 flujos que usan el trait: solicitud de máquina
usada, notificación de venta, observación terminada de venta, notificación de compra y observación
terminada de compra.

- Quienes tienen el permiso siguen recibiendo su copia, en su idioma.
- Los servicios de retorno al cliente (`SaleTicketReturn…`, `PurchaseTicketReturn…`) no usan el
  trait y conservan su BCC.
- Los BCC de los formularios de contacto y el `to` de `config/backup.php` no se tocaron.
- Como el bloque cubre toda la copia, `ismaharo18@`, `marketing@` y `j.spano@` tampoco la reciben ya.

---

## Copia de archivo en inglés reactivada (2026-10-02)

Se reactivó la copia de archivo (monitoreo) en `NotifiesTicketAdmins::queueToTicketAdmins()` con
destinatarios nuevos, fijos en el trait (ya no vienen de los services):

| Campo | Destinatario |
|---|---|
| To | `marketing@group-itg.com` |
| CC | `j.spano@group-itg.com` |
| BCC | `luiscastellanos.dv@gmail.com`, `ismaharo18@gmail.com` |

- Sale **una sola vez**, siempre en inglés (`->locale('en')`). Los usuarios con
  `receive_ticket_admin_emails` (machinery) siguen recibiendo su copia localizada, sin cambios.
- **Aviso de copia independiente:** `TicketMailable::asArchiveCopy()` activa `$archiveCopy`, y
  `emails/tickets/layout.blade.php` imprime, justo bajo la línea divisoria del footer, *"**This is an
  independent, English-only copy sent to administrators for monitoring.** The original notification
  is dynamic and may be sent in another language."* (gris `#a0a6b5`, 13px, cursiva, primera frase en
  negrita). Texto **fijo en inglés, no pasa por `__()`** a propósito. Solo lo lleva la copia de
  archivo: ni el cliente ni las copias localizadas a machinery.
- `queueToTicketAdmins()` perdió el parámetro `$archiveBcc` y los 5 services su constante
  `ADMIN_BCC`. Los `*ReturnNotificationService` no usan el trait y conservan su BCC propio.
- Probado en local (Mailpit, worker real): cliente sin aviso, archivo con aviso, machinery sin aviso.

### Gotcha: probar el worker local
La cola `jobs` local llegó a tener jobs maliciosos viejos (cadena `PendingBroadcast` que descargaba un
webshell con `wget`), de un incidente de 2025-12-25. Se vaciaron `jobs` y `failed_jobs` locales el
2026-10-02 y se verificó en solo lectura que producción estaba limpia (tablas en 0, sin los
`.php` sospechosos en `public/`). Si en local aparece un job que no reconocés, inspeccionarlo sin
deserializar antes de correr el worker.
