# Migración Tailwind/WireUI → Vuexy (Bootstrap 5)

> Guía viva del proyecto de migración del panel de usuarios a la plantilla **Vuexy (Bootstrap 5)**,
> eliminando la mezcla con **Tailwind + WireUI**. Léela antes de tocar vistas/forms/modales del admin.

---

## 1. Objetivo

El panel admin mezclaba **Vuexy/Bootstrap** (plantilla) con **Tailwind + WireUI** (inputs/validación/modales).
Eso causaba conflictos de clases. Meta: dejar **todo el panel admin en Vuexy/Bootstrap puro**, sin Tailwind
ni WireUI, de forma **incremental y sin romper** lo que sigue en Tailwind.

Estrategia: un **layout limpio paralelo** (`layouts.users.main`) sin Tailwind. Las vistas migradas apuntan a
él; las no migradas siguen en `layouts.users.app` (Tailwind) hasta que les toque. Así se ve el avance vista a vista.

**Qué se queda (NO es Tailwind):**
- `jantinnerezo/livewire-alert` (SweetAlert2) → notificaciones.
- `wire-elements/modal` (`LivewireUI\Modal`) → mecánica de modales (se le cambia solo la vista a Bootstrap).
- PowerGrid (tablas) → ya usa tema Bootstrap (`App\PowerGridThemes\TableEditBootstrap5`).
- Cleave.js (formato de teléfono), flatpickr (datepicker), dropzone → vendors de Vuexy, no Tailwind.

**Qué se elimina del admin:** WireUI (`x-input`, `x-select`, `x-phone`, `x-password`, `x-toggle`,
`$this->notification()`, `x-dialog`, `x-notifications`) y todas las clases Tailwind.

---

## 2. Infraestructura (la BASE — ya creada)

| Archivo | Rol |
|---|---|
| `resources/views/layouts/users/main.blade.php` | **Layout limpio** Vuexy. SIN `app.css` (Tailwind) ni WireUI. Monta `@livewire('vuexy-modal')` y carga `cleave.js`. **Las vistas migradas extienden este.** |
| `resources/js/admin.js` | Bundle JS limpio (= bootstrap/axios + **PowerGrid** + **SweetAlert2** expuesto como `window.Swal`). NO Tailwind. (No tocar `app.js`.) |
| `resources/css/admin.css` | CSS limpio: imports de `flatpickr` + `livewire-dropzone-styles` + `sweetalert2`, tipografías, `.cursor-pointer` y el **CSS de scroll del modal admin**. (No tocar `app.css`.) |
| `vite.config.js` | Registra `admin.css` y `admin.js` como entries (además de los viejos). |
| `app/Livewire/VuexyModal.php` | Host de modales del admin. Extiende `LivewireUI\Modal\Modal` y solo cambia la vista a `modal-vuexy`. Alias Livewire: `vuexy-modal`. |
| `resources/views/vendor/wire-elements-modal/modal-vuexy.blade.php` | **Vista Bootstrap/Vuexy del modal** (centrado / top / offcanvas). Enlaza el estado Alpine de wire-elements con `bootstrap.Modal`/`bootstrap.Offcanvas`. |
| `resources/views/vendor/wire-elements-modal/modal.blade.php` | Vista **original Tailwind** del paquete. **NO TOCAR** → la usan los layouts guest. |
| `resources/views/components/tel-input.blade.php` | Componente reutilizable `<x-tel-input>` (Cleave.js + Alpine + `wire:model`). |

⚠️ **Dato clave de wire-elements:** `Modal::render()` tiene la vista **hardcodeada** (`view('wire-elements-modal::modal')`).
El atributo `view="..."` se **ignora**. Por eso el admin usa una **subclase** (`VuexyModal`) que sobrescribe `render()`.
El guest sigue usando `wire-elements-modal` + la vista Tailwind original (intacta).

---

## 3. Convenciones de migración (CÓMO migrar cada cosa)

### 3.1 Vista que extiende layout
```blade
@extends('layouts.users.main')   {{-- en vez de layouts.users.app --}}
```

### 3.2 Inputs (WireUI → Bootstrap/Vuexy)
- `<x-input/>` → `<input class="form-control">` dentro de `input-group input-group-merge` con ícono `ti tabler-*`.
- `<x-select/>` → `<select class="form-select">`.
- `<x-phone/>` → `<x-tel-input model="campo" :label="..." :placeholder="..."/>`.
- `<x-password/>` → input con toggle ver/ocultar (Alpine `x-data="{show:false}"`, ícono `tabler-eye`/`tabler-eye-off`;
  abierto = visible, tachado = oculto).
- Label: `<label class="form-label">Texto <span class="text-danger">*</span></label>`.
- **Validación:** `@error('campo') is-invalid @enderror` en el input + `@error('campo')<div class="text-danger small mt-1">{{ $message }}</div>@enderror`.
  (La validación real sigue siendo server-side en el componente Livewire — no se pierde.)

### 3.3 Notificaciones (WireUI → livewire-alert)
En el componente PHP:
```php
use Jantinnerezo\LivewireAlert\LivewireAlert;   // en vez de WireUi\Traits\WireUiActions
class Foo extends ModalComponent {
    use LivewireAlert;                            // en vez de use WireUiActions
    // ...
    $this->alert('success', __('Título'), [
        'position' => 'top-end', 'timer' => 3000, 'toast' => true,
        'text' => __('Descripción'),
    ]);
    // error -> $this->alert('error', ...)
}
```
Reemplaza todo `$this->notification()->send([...])`.

#### Formato de las alertas: **título CORTO + mensaje en `text`** (regla obligatoria)

El **título** (2º arg de `->alert()`) debe ser **una sola palabra según el tipo**; el texto
descriptivo va **siempre** en la opción `text`. Meter el mensaje completo en el título hace
que el toast crezca a **2 filas** (se ve enorme).

```php
// ✅ BIEN — título corto, detalle en text
$this->alert('success', __('Success'), [
    'position' => 'top-end', 'timer' => 3000, 'toast' => true,
    'text' => __('Your account has been created successfully.'),
]);

// ❌ MAL — todo el mensaje en el título (se parte en 2 filas)
$this->alert('success', __('Your account has been created successfully.'), [
    'position' => 'top-end', 'timer' => 3000, 'toast' => true,
]);
```

Mapa título por tipo: `success → __('Success')`, `error → __('Error')`,
`warning → __('Warning')`, `info → __('Info')`.

**Excepciones (NO se tocan):**
- **Diálogos de confirmación** (`toast => false`, `position => 'center'`, con botones
  Confirm/Cancel): ahí el título ES una pregunta corta (`'Are you sure?'`, `'Delete photo?'`)
  y el detalle ya va en `text`. Ya cumplen el patrón.
- **Helper `notify(string $type, string $title, string $message)`** (kanbans, tablas,
  comment forms): ya reparte título/`text` (mete `$message` en `text`). Sus call sites
  ya pasan `message` → no se modifican.

**Regla práctica:** solo se corrigen las llamadas **directas** `$this->alert(...)` que **NO
llevan `text`** (meten todo en el título). Para esas: título → palabra corta por tipo +
mover el mensaje original tal cual a `'text'`.

### 3.4 Alertas de sesión en páginas (flash)
Bootstrap dismissible (NO `showAlert()` JS que no existe):
```blade
@if (session('success'))
  <div class="alert alert-success alert-dismissible d-flex align-items-center" role="alert">
    <i class="ti tabler-circle-check me-2"></i><span>{{ session('success') }}</span>
    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  </div>
@endif
```

### 3.5 Modales (estructura que SÍ hace scroll)
El componente Livewire del modal debe renderizar **header / body / footer como hermanos** (no envolver con `<form>`):
```blade
<div>
  <div class="modal-header"> ...avatar+título+subtítulo... <button class="btn-close" wire:click="$dispatch('closeModal')"></button></div>
  <div class="modal-body">
    <form id="miForm" wire:submit.prevent="metodo" novalidate>
      <div class="divider divider-start"><div class="divider-text">Sección</div></div>
      <div class="row g-4"> ...campos input-group... </div>
    </form>
  </div>
  <div class="modal-footer">
    <button type="button" class="btn btn-label-secondary" wire:click="$dispatch('closeModal')">Cancel</button>
    <button type="submit" form="miForm" class="btn btn-primary" wire:loading.attr="disabled" wire:target="metodo">
      <span wire:loading.remove wire:target="metodo">Guardar</span>
      <span wire:loading wire:target="metodo"><span class="spinner-border spinner-border-sm me-1"></span>Saving...</span>
    </button>
  </div>
</div>
```
- El submit del footer se asocia al form con `form="miForm"` (footer queda fijo, body scrollea).
- El scroll del body lo resuelve el CSS `#livewire-ui-modal.modal ...` en `admin.css` (porque wire-elements anida el componente).

### 3.6 Tamaño y formato del modal (por componente)
En la clase del ModalComponent:
```php
public static function modalMaxWidth(): string { return '5xl'; }   // sm..7xl  -> mapea a modal-sm/lg/xl
public static function modalType(): string { return 'center'; }    // 'center' | 'top' | 'offcanvas-end' | 'offcanvas-start'
```
- Sin `modalType()` → default `center`.
- `offcanvas-end` = panel lateral derecho (bueno para forms largos), `offcanvas-start` = izquierdo.

### 3.7 Componentes propios ya Bootstrap (reutilizables)
`x-breadcrumb`, `x-print-button` (en `resources/views/components/`) ya son Bootstrap puro. `x-tel-input` (nuevo).

---

## 4. Estado del proyecto

### ✅ Hecho
- Infraestructura (sección 2 completa).
- **Vistas show** → `main`, sin Tailwind: `sale-ticket/admin/show`, `sale-used-machine/admin/show`,
  `sale-used-machine/request/show`, `purchase-ticket/admin/show`, `users/user-detail`.
- **Dashboards**: `dashboard/dashboard-admin` (limpiado + alerts dismissibles), `dashboard/welcome-dashboard` (reescrito).
- **Módulo `profile`**: `profile/profile` (→ main). Las dos forms Volt (`livewire/profile/update-profile-information-form`,
  `update-password-form`) ya emitían Bootstrap (los componentes Breeze `x-text-input`/`x-input-label`/`x-primary-button`
  fueron reescritos a Bootstrap en este repo). Se cambió la confirmación de guardado de `dispatch('profile-updated')`/
  `x-action-message` a **toast livewire-alert** (trait `LivewireAlert` en la clase Volt). `components/user-profile-layout`
  ya era Bootstrap puro. *Nota:* `livewire/profile/delete-user-form` quedó SIN uso (no se incluye en la página; Tailwind/Breeze, se deja).
- **Módulo `users` (PILOTO COMPLETO Y VERIFICADO)**: `users/user-table` (→ main), `livewire/user/create-user` + `livewire/user/edit-user`
  (modal Vuexy, input-groups con íconos, toggle password, requisitos como lista compacta en vivo, scroll OK),
  `CreateUser.php` + `EditUser.php` (livewire-alert). *Nota:* en edit se agregaron inputs País/Ciudad que faltaban.
  Vistas + Livewire de user 100% migrados. **Alertas livewire-alert verificadas funcionando** (SweetAlert2 vía npm en
  `admin.js` como `window.Swal` + CSS en `admin.css`; toast con `z-index:100000` por encima del navbar fixed).
- **Paleta de marca en el customizer**: `config.js` define `defaultPrimaryColor: '#014023'` (main-color) y `availableColors`
  con la paleta (main-color, main-green, jet-stream, m-yellow, timberwolf) — sobrescribe `TemplateCustomizer.COLORS` sin tocar
  el vendor. Avatares (profile + navbar) usan `--bs-primary`/`bg-primary`, así toman el verde de marca solos.
- **sale-used-machine → FORM DE CREAR MÁQUINA (hecho)**: `UsedMachineCreateForm.php` (`notification()`→`flash/alert` livewire-alert),
  trait `Traits/UsedMachine/WithDynamicSections.php` (de WireUI `dialog()/notification()` → **livewire-alert**; confirms de borrado
  vía SweetAlert con `pendingDeletion` + `#[On('executePendingDeletion')]`). Vista modal `livewire/sale-used-machine/used-machine-create-form`
  reescrita al patrón Vuexy (header/body-scroll/footer, `form="usedMachineForm"`). Type-forms `components/used-machine/types/{weaving,knitting,other}-form`
  y TODAS las secciones `components/used-machine/sections/*` (general, status-information, machine-specs, cam/dobby/jacquard, loom-accessories,
  beams-rollers, core-knitting, type-of-machine, price-info, additional-information, decription/Quill, media-information/dropzone, custom-sections)
  → cards Vuexy con **inputs `input-group input-group-merge` + ícono** (estilo user). Moneda: select async WireUI → `<select>` con `Currency::orderBy('code')` (BD).
  `sale-used-machine/admin/index` → `main` + alerts Bootstrap.
  *OJO:* las secciones son **compartidas** con los forms guest/cliente (modo `user`) que aún están en layouts Tailwind:
  quedarán rotas hasta migrarlas (autorizado por el usuario). `components/used-machine/sections/extra-fields` NO está en el path de create → sigue Tailwind.

- **sale-used-machine → FORM DE EDITAR MÁQUINA + MEDIA + FLYER (HECHO, MÓDULO COMPLETO)**:
  - **Vista `livewire/sale-used-machine/used-machine-edit-form`** reescrita: **sidebar de tabs `position-sticky` (estático)** + contenido que scrollea con la página. 3 tabs: *Technical Specs*, *Media & Assets*, *Sales Tracking*.
  - **WIZARD de specs (Alpine, sin vendor extra)**: `x-data="{ step, total }"` con stepper (`specs-stepper` en `admin.css`) y `x-show`/`x-cloak` por paso. En vez del `<x-dynamic-component>`, **se componen las secciones a mano** ramificando por `$form->type_of_machine` (weaving/knitting/other + condicionales `cam/dobby/jacquard` según `machineSpecs.movement_type`) → réplica EXACTA de los type-forms, mismo `wire:model`/validación. Mismo patrón replicado en **`used-machine-create-form`** (5 pasos: General, Specifications, Pricing, Details, **Media**). El botón Save/Create va **dentro de la sección/footer** (no en el navbar de tabs), `form="usedMachineForm"`.
  - **`UsedMachineEditMediaForm` / `UsedMachineEditPromoFlyerForm`**: ⚠️ **BUG corregido** → extendían `App\Livewire\VuexyModal` (el *host* de modales) en vez de `LivewireUI\Modal\ModalComponent` (provoca fatal `render() must be compatible...`). Ahora extienden `ModalComponent` como create/CreateUser/EditUser. Además el atributo estaba escrito `\#[Livewire\Attributes\On(...)]` (el `\` lo volvía comentario y rompía `deleteMedia`/`deleteFlyer`) → corregido a `#[...]`. Vistas modal reescritas a Vuexy puro (dividers `divider divider-start`, acento `primary`, sin azul `info` ni `alert-icon` roto).
  - **Sales Tracking**: card de estado con confirmación **SweetAlert** (`window.Swal`, en vez de `confirm()` nativo); *Customer Requests* = botón redirect; *Activity Timeline* y *Team Comments* abren **offcanvas-end (modal derecho)** de Bootstrap.
  - **Widgets compartidos migrados con flag `bootstrap`** (NO rompe las vistas Tailwind aún sin migrar): `components/tickets/activity-history` (rama Bootstrap timeline), `SaleTicketCommentList` + `SaleTicketCreateCommentForm` (`public bool $bootstrap`; en modo bootstrap el form usa **livewire-alert** en vez de WireUI `notification()`, que no existe en `main`). Las ramas Tailwind originales quedan intactas tras `@else`.

- **sale-ticket → MÓDULO COMPLETO (HECHO — 2026-07-02)**:
  - **5 vistas → `main` + salida de Tailwind/WireUI**: `sale-ticket/{admin/kanban, admin/index, archived/index, customer/index}` (layout `app`→`main`; `<x-alert>` WireUI → alertas Bootstrap dismissibles). `sale-ticket/customer/edit` **reescrita completa** Tailwind→Bootstrap (cards + `row/col-lg-8/col-lg-4`, badges de status/stage; el `<x-confirm-button>` de "Return corrected ticket" → botón Bootstrap + `Swal.fire` que hace submit del form).
  - **Componentes WireUI → livewire-alert**: `SaleTicketKanban` (14 `notification()` → helper `notify()`; los 3 `<x-confirm-button>` Reject/Confirm/Delete → botones Bootstrap con confirm SweetAlert vía `askReject/askConfirm/askArchive()` → `onConfirmed` → acción con `#[On('...')]` que lee `pendingTicketId`), `SaleTicketCreateCommentForm` (quitado trait WireUI + rama condicional; **siempre** `alert()`), `SaleTicketTable` + `SaleTicketArchivedTable` (`notification()->confirm()` → patrón `onConfirmed` + propiedad `pendingTicketId` / `pendingStatusValue`+`pendingTicketId`, con `#[On]` en la acción).
  - **Vistas de comentarios** (`livewire/sale-ticket/team-comment/{comment-list,create-comment-form}`): eliminada la rama Tailwind muerta (`@else`); ahora **solo** Bootstrap (se renderizan siempre con `:bootstrap="true"` desde la edición de máquina).
  - Todo compila (`Blade::compileString` + `php -l`). No requirió `npm run build` (clases Vuexy ya presentes; SweetAlert ya cargado en `main`).

- **purchase-ticket → MÓDULO COMPLETO (HECHO — 2026-07-03)**: espejo de sale-ticket.
  - Componentes: `PurchaseTicketKanban` (`notify()` + `askReject/askConfirm/askArchive()` → `#[On]` `rejectTicket`/`confirTicket`/`markAsArchived` con `pendingTicketId`), `PurchaseTicketTable` (`confirmMarkAsArchived`→`onConfirmed` + `$ticketIdToArchive`), `PurchaseTicketArchivedTable` (`confirmStatusChange`→`onConfirmed` + `pendingStatusValue`/`pendingTicketId`), `PurchaseTicketCreateCommentForm` (siempre `notify()`/alert). Traits `WireUiActions` sin uso removidos de `PurchaseTicketCreateForm`, `CreateUsedMachineWizard`, `SaleUsedMachineTable`.
  - Vistas (6) → `main` + `<x-alert>`→Bootstrap + `<x-confirm-button>`→botón/`Swal.fire`: `purchase-ticket/{admin/kanban, admin/index, admin/edit, archived/index, customer/index, customer/edit}`. `admin/edit` reescrita completa (status/channels/system-actions con Swal, activity-history). Partials Bootstrap: `partials/_observations-list`, `observations/create`, `team-comment/{comment-list,create-comment-form}`. El `PurchaseTicketEditForm` blade ya era Bootstrap.

- **✅ LIMPIEZA FINAL COMPLETA (2026-07-03) — Tailwind/WireUI ELIMINADOS del panel:**
  - **auth 100% Vuexy**: `layouts/auth/auth.blade.php` reescrito a Bootstrap (card centrada, `admin.css`/`admin.js`, sin `tw-wrapper`). Las 4 páginas (`livewire/pages/auth/{forgot-password,reset-password,confirm-password,verify-email}`) reescritas a Bootstrap (input-group + iconos). Componentes Breeze `input-error`/`auth-session-status` limpiados de clases Tailwind. (`login`/`register` viven en `layouts.guest.general` — guest, fuera de alcance.)
  - **`sale-used-machine/request/index`** → `main` + Bootstrap (última vista en `app`).
  - **`price-info-fields`**: `<x-select-search>` (último WireUI vivo) → `<select class="form-select">`.
  - **BORRADOS (código muerto/obsoleto)**: `layouts/users/app.blade.php`, `app/View/Components/AppLayout.php`, `resources/css/app.css`, `resources/js/app.js`, `config/wireui.php`, `components/confirm-button.blade.php` (usaba `$wireui`), `components/{modal? no existía,danger-button,secondary-button}`, `livewire/profile/delete-user-form.blade.php` (muerto y roto), `components/used-machine/sections/extra-fields.blade.php`, `components/tickets/introduction-create.blade.php`. `activity-history` simplificado (solo rama Bootstrap; se quitó el `@else` Tailwind muerto).
  - **vite.config.js**: quitadas las 2 entries `app.css`/`app.js`. **composer.json**: quitado `wireui/wireui` (⚠️ correr `composer update`/`sail composer install` para actualizar el lock — el `composer.lock` aún lo lista hasta entonces).
  - **Verificado por grep**: 0 vistas en `layouts.users.app`, 0 refs a `app.css`/`app.js`, 0 componentes/magic WireUI (`$wireui`), 0 referencias rotas a archivos borrados.

- **✅ CIERRE DE PENDIENTES MENORES (2026-07-06):**
  - **customer/guest revisado — sin nada que migrar:** las vistas **realmente guest** (`layouts/guest/general`, Bootstrap propio `bootstrap.css`/`style.css`, sin Tailwind ni `@vite`/Vuexy) son solo **show/index**: `used-machine-filter`, `sale-used-machine/customer/index`, `sale-used-machine/customer/show`. Ya Bootstrap puro (sin Tailwind real, sin WireUI, sin `mode 'user'`, sin secciones Vuexy). Rutas guest = solo `guest.sale-used-machines.index` + `.detail`. (Fix: typo `lgbt-spacing`→`letter-spacing` en el `<style>` del filter.) El **wizard/edit "customer"** (`used-machine-edit-form-customer`, `used-machine-customer-wizard-steps`) es el **rol customer editando su ticket DENTRO del panel Vuexy** (`UsedMachineEditForm::render()` elige vista por `isStaff`), embebido en `sale-ticket/customer/edit` (→ `main`), `sale-ticket/drafts/edit` (→ `general-form-tickets`, carga Vuexy) y `sale-used-machine/admin/edit` (→ `main`). Se ven bien; no hay degradación.
  - **Baja de WireUI consolidada en el lock:** `wireui/wireui` fuera de `composer.lock` (0 refs). ⚠️ Se hizo con **update DIRIGIDO** `composer update wireui/wireui` (NO update completo, que está bloqueado por advisories — ver §6 y CLAUDE.md). Laravel intacto en `v11.48.0`.
  - **Traducciones de títulos de alertas:** claves `Success`/`Error`/`Warning`/`Info` agregadas a `lang/{en,es,fr,it,pt,zh_CN}.json` (JSON válido). Los toasts (§3.3) ya salen traducidos.
  - **Renombrados los archivos con sufijo `-vuexy` (nombrar por función, no por plantilla):** `general-form-vuexy`→`general-form-tickets`, `x-form-wizard-vuexy`→`x-form-wizard`, `form-navbar-vuexy`→`form-navbar`, `x-tickets.introduction-create-vuexy`→`x-tickets.introduction-create`. **Los modales SÍ conservan `vuexy`** (`modal-vuexy`, `VuexyModal`, alias `vuexy-modal`) porque son propios de esa plantilla.

### ✅ Hecho (cont.)
- **🐛 Media unificada en `used_machine_media` (2026-07-06) — código COMPLETO:** eliminado el split ticket-vs-máquina; **toda** la media (create/edit, staff/customer, publicada o no) vive en `used_machine_media` (`UsedMachineSaleMedia`) por `sale_used_machine_id`. La máquina se crea junto al ticket (`SaleTicketService:createSaleTicketWithRequest`), así que la media siempre tiene a dónde ir. Cambios:
  - **Escritura:** `MediaStepForm` (create) y `UsedMachineEditForm::save` guardan en la máquina (con `is_main` en la 1ª imagen); customer también. `SaleUsedMachineService::saveMachine` ya escribía ahí. `CreateSaleUsedMachineryService` (publicar) ya no copia media (la máquina ya la tiene).
  - **Lectura/borrado:** `SaleTicketPhotoList` + `SaleTicketVideoList` leen/borran de `$ticket->saleUsedMachine->media` (`UsedMachineSaleMedia`); validación `MaxTotalImagesRule` contra `sale_used_machine_id`.
  - **Borrados:** `SaleTicketMedia` (modelo), `SaleTicketMediaPolicy`, `SaleTicketMediaFactory`, `TicketMediaService`. `SaleTicket::media()` relación eliminada. `SaleTicketSeeder` repuntado.
  - **Migración (para PROD): `2026_07_06_000000_migrate_and_drop_sale_ticket_media.php`** — **solo dropea** `sale_ticket_media` (`Schema::dropIfExists`). La migración de datos draft→máquina ya la hace el backfill `...000001` (crea la máquina y copia su media a `used_machine_media`); en prod no hay máquinas con media huérfana (esos casos eran de desarrollo). `down()` recrea la tabla vacía. ⚠️ NO correr en local (ver CLAUDE.md "Base de datos / entorno"); se aplica al desplegar a prod.
  - **`request` (sale_ticket_requests):** SIN residuo de código — 0 refs en app/resources/routes, ningún modelo la mapea (los `*Request.php` vivos son `ContactRequest`/`PurchaseTicketRequest`/`UsedMachineRequest`, features aparte). Solo aparece en migraciones históricas (intactas).

### 🚫 NO tocar (guest / fuera de alcance)
- `resources/views/vendor/wire-elements-modal/modal.blade.php` (vista Tailwind original del guest).
- Layouts guest (`layouts/guest/*`, `layouts/purchase-and-sale/general-form`) y sus modales; `page.css` (tipografías, lo usa un guest). `login`/`register` (guest).
- **Filament** es aparte y **usa Tailwind por diseño** (`resources/css/tiptap/filament-tiptap-editor.css` con `@tailwind`) — NO tocar; no depende de `app.css`/`app.js`.

---

## 5. Flujo de trabajo / pruebas

- App vía **Sail (Docker)**. PHP no está en el PATH directo → usar `./vendor/bin/sail php ...`.
- Assets con **Vite**: `./vendor/bin/sail npm run dev` (reiniciar si se agregan entries nuevos a `vite.config.js`).
- Cambios solo-blade → recompilan solos al recargar. Cambios en `admin.css`/`admin.js` → HMR.

## 6. Notas / gotchas aprendidos
- **PowerGrid en admin.js**: si las acciones de tabla salen "undefined", falta el import de PowerGrid en `admin.js`.
- **Datepicker roto** = falta el CSS de flatpickr (está en `admin.css`).
- **Modal sale como div suelto** = no se montó el Alpine `LivewireUIModal()` o falta `bootstrap` JS / la vista correcta (`modal-vuexy`).
- **Sin scroll en modal** = el `<form>` envolvía body+footer, o falta el CSS de flex en `admin.css`.
- `showAlert()` no existía en el proyecto (bug latente); se reemplazó por alerts Bootstrap.
- **livewire-alert no muestra nada** = falta `window.Swal`. El paquete (`jantinnerezo/livewire-alert` v3) llama `Swal.fire()` pero **NO incluye SweetAlert2**; antes se cargaba por CDN solo en layouts guest. En `main` se instaló SweetAlert2 vía npm y se expone en `admin.js` (`import Swal from 'sweetalert2'; window.Swal = Swal;`) + su CSS en `admin.css`. `<x-livewire-alert::scripts />` (en `main`) solo registra el listener del evento `alert`; necesita el global.
- **Modal de contenido = `ModalComponent`, NO `Modal`/`VuexyModal`**: los componentes que se ABREN como modal (`openModal`) extienden `LivewireUI\Modal\ModalComponent`. `VuexyModal extends Modal` es el **host único** (`@livewire('vuexy-modal')`). Extender el host por error da fatal `render() must be compatible with ...VuexyModal::render(): Illuminate\View\View`.
- **Atributo PHP roto** = `\#[Attr]` (con backslash) se parsea como `\` + comentario. Debe ser `#[Attr]`. Síntoma: el listener `#[On('...')]` "no existe".
- **Confirmación bonita** = en vez de `confirm()` nativo, `Swal.fire({... customClass:{confirmButton:'btn btn-primary me-3', cancelButton:'btn btn-label-secondary'}, buttonsStyling:false })` y `.then(r => r.isConfirmed && form.submit())`.
- **Wizard sin vendor** = `x-data="{ step:1, total:N }"` + stepper `specs-stepper` (CSS en `admin.css`) + `x-show`/`x-cloak` por paso (regla `[x-cloak]{display:none}` en `admin.css`). El estado Alpine sobrevive a los re-render de Livewire.
- **Dark mode: verde de marca ilegible** = el `primary` (#014023) y `success` como TEXTO/hover/iconos no se ven sobre fondo oscuro. En `admin.css`, bajo `html[data-bs-theme="dark"]`, se fuerza `#fff` en `.text-primary/.text-success`, `.bg-label-*` (e iconos hijos), `.nav-link:hover/.active`, y breadcrumbs (`.breadcrumb-item a` no activo a blanco 70%, activo/hover a blanco pleno). Si aparece verde nuevo, añadir el selector a ese bloque.
- **Widget compartido entre layouts Tailwind y Vuexy** = añadir flag `bool $bootstrap` (o `'bootstrap' => true` en `@include`) y ramificar la vista `@if($bootstrap) ...Bootstrap... @else ...Tailwind original... @endif`. Así no se rompe lo que aún no se migra.
- **Validación de `input-group-merge` sin `<form>` ancestro** = las reglas de Vuexy
  (`_bootstrap-extended/forms/_validation.scss`) están anidadas dentro de `form { … }`
  (`form .input-group:has(.is-invalid)`). Sin un `<form>` ancestro, solo el `.form-control.is-invalid`
  se pinta de rojo (el icono `.input-group-text` queda gris) y el `:hover` de Bootstrap le gana al
  color de inválido. **Regla:** cualquier bloque con `input-group-merge` + `@error(...) is-invalid`
  va envuelto en `<form>`. En wizards Livewire (`x-form-wizard`, sin botón submit) envolver con:
  ```blade
  <form wire:submit.prevent novalidate wire:loading.class="opacity-50 pointer-events-none" wire:target="submit">
      ...
  </form>
  ```
  `wire:submit.prevent` (sin acción) evita recargas por Enter — los botones siguen `type="button"`
  con `wire:click`. `novalidate` desactiva la validación nativa. **No envolver**
  `<livewire:dropzone>` (ya renderiza su propio `<form>`; form anidado = HTML inválido). Patrón de
  referencia: `resources/views/livewire/sale-used-machine/used-machine-create-form.blade.php`.

---

## 7. Próximo paso sugerido
**sale-used-machine y sale-ticket 100% migrados.** Sigue **purchase-ticket** (es casi un espejo de sale-ticket, mismo patrón):
1. Componentes: `PurchaseTicketKanban` (helper `notify()` + `ask*()`/`onConfirmed` para los 3 confirm-button), `PurchaseTicketCreateCommentForm` (siempre `alert()`), `PurchaseTicketTable` + `PurchaseTicketArchivedTable` (`confirm()` → `onConfirmed` + propiedad de id). Quitar el trait `WireUiActions` sin uso de `PurchaseTicketCreateForm`, `CreateUsedMachineWizard` y `SaleUsedMachineTable`.
2. Vistas (6) → `main` + `<x-alert>`→alertas Bootstrap + `<x-confirm-button>`→botón/Swal: `purchase-ticket/{admin/kanban, admin/index, admin/edit, archived/index, customer/index, customer/edit}`.
3. Ojo `purchase-ticket/admin/edit.blade.php` (usa `<x-confirm-button>` con `form-ref` como el `customer/edit` de sale-ticket → mismo patrón botón + `Swal.fire`).

Luego `sale-used-machine/request/index` + `UsedMachineFilter`, y por último la **§ Limpieza final** (borrar `app.blade.php`/`app.css`/`app.js` + `composer remove wireui/wireui`), cuyo bloqueante es migrar `layouts/auth/auth.blade.php`.

## 8. Pendiente de diseño — Purchase form a wizard + tabs

**Rediseñar el formulario de creación/edición de Purchase tickets al mismo patrón wizard + tabs que ya usan Sale ticket y Used Machine.** Hoy el form de purchase no comparte ese diseño; la meta es consistencia visual entre los tres módulos.
- Referencia de estructura: `resources/views/livewire/sale-used-machine/used-machine-create-form.blade.php` (wizard 5 pasos: General, Specifications, Pricing, Details, Media) y `resources/views/sale-used-machine/admin/edit.blade.php` (tabs).
- Patrón wizard: `x-data="{ step, total }"` con stepper (`specs-stepper` en `admin.css`) + `x-show`/`x-cloak` por paso, secciones compuestas a mano ramificando por tipo (ver § 4 "Hecho (cont.)", nota del wizard de specs). Botón Save/Create **dentro** de la sección/footer, no en el navbar de tabs.
- Respetar el gotcha del `<form>` ancestro para `input-group-merge` (ver `CLAUDE.md` → "Gotchas").
