# Estructura de vistas — Sale Tickets (Used Machine) y Purchase Tickets

> Mapa jerárquico de los archivos Blade y componentes Livewire que intervienen en los
> flujos de **creación / edición de tickets**. Pensado para leerse (humano o IA) antes de
> tocar cualquier vista y saber qué se hereda, qué se incluye y qué componente Livewire
> queda anidado dónde.
>
> Generado el 2026-07-03. Stack: Laravel + Livewire 3 + Vuexy/Bootstrap (sin Tailwind/WireUI).

---

## Leyenda de símbolos

| Símbolo | Significado |
|---------|-------------|
| 📐 | **Layout** (`@extends`) del que hereda la página |
| 🟢 | **Página Blade** (la que devuelve el controller con `return view(...)`) |
| ⚡ | **Componente Livewire** (`<livewire:...>`, `@livewire(...)` o paso de wizard anidado) |
| 🧩 | **Componente Blade** (`<x-...>`) |
| 📄 | **Partial** incluido con `@include(...)` |
| 🪟 | Se abre como **modal/offcanvas** vía `$dispatch('openModal', ...)` |
| 🔀 | **Ramifica** en tiempo de render / dynamic-component |

---

## Conceptos clave (leer primero)

### 1. `SaleUsedMachine` es la única fuente de verdad para Sale Tickets
No existe un "sale-ticket edit" propio: **todo** (draft, customer, staff) pasa por el
componente Livewire `UsedMachineEditForm` o sus formularios de máquina. El modo staff vs.
customer se decide **por permiso**, no por ticket-vs-catálogo.

### 2. `UsedMachineEditForm` ramifica en `render()` según `isStaff`
```
⚡ App\Livewire\SaleUsedMachine\UsedMachineEditForm
   └─ render() 🔀
      ├─ isStaff = true  → livewire.sale-used-machine.used-machine-edit-form            (staff)
      └─ isStaff = false → livewire.sale-used-machine.used-machine-edit-form-customer   (customer)
```
- `canEditStaffFields()` decide, **dentro** de la vista staff, si los campos van en
  `mode="staff"` o `mode="customer"` (según stage/status del ticket).

### 3. El "form de tipo" es dinámico (`x-dynamic-component`)
`UsedMachineForm::getFormComponentName()` mapea según `type_of_machine`:
```
weaving  → x-used-machine.types.weaving-form
knitting → x-used-machine.types.knitting-form
default  → x-used-machine.types.other-form
```
Cada `types.*-form` compone a su vez un subconjunto de `x-used-machine.sections.*`.

### 4. Los wizards de creación usan Spatie `WizardComponent`
`CreateUsedMachineWizard` (sale) y `PurchaseTicketCreateForm` (purchase) NO tienen un
Blade propio de layout: cada **step** es un componente Livewire independiente anidado.
Los steps `select-action-step-form` y `register-step-form` viven en el namespace
**compartido** `Ticket\Create\Steps` (los usan ambos wizards).

---

## Bloques compartidos (referenciados por varios flujos)

### Layouts
```
📐 layouts.users.main                         → páginas autenticadas del dashboard (staff/customer logueado)
📐 layouts.purchase-and-sale.general-form-vuexy → wizards/forms públicos de creación y drafts
      📄 layouts.purchase-and-sale.form-navbar-vuexy
      🧩 x-livewire-alert::scripts
      📄 layouts.partials.captcha
📐 layouts.guest.general                       → catálogo público de máquinas usadas
```

### Campos de máquina (usados en creación Y edición, sale)
```
🧩 x-used-machine.types.weaving-form   ─┐
🧩 x-used-machine.types.knitting-form   ├─ (dynamic-component, según type_of_machine)
🧩 x-used-machine.types.other-form     ─┘
      └─ componen 🧩 x-used-machine.sections.*:
           general-fields · machine-specs-fields · cam-details-fields · dobby-details-fields
           jacquard-details-fields · loom-accessories-fields · beams-rollers-fields
           core-knitting · type-of-machine · price-info-fields · additional-information
           owner-field · custom-sections · decription · media-information · status-information
🧩 x-used-machine.image
🧩 x-form-wizard-vuexy                  → carcasa visual de pasos (todos los steps de wizard)
```

### Partials de wizard (edición sale)
```
📄 livewire.sale-used-machine.partials.used-machine-staff-wizard-steps
      🧩 x-used-machine.sections.*  (mode = canEditStaffFields() ? 'staff' : 'customer')
📄 livewire.sale-used-machine.partials.used-machine-customer-wizard-steps
      🧩 x-used-machine.sections.*  (mode = 'customer')
```

---

# FLUJO 1 — CREACIÓN DE TICKETS POR WIZARD (CUSTOMER)

## 1A · SALE (máquina usada)
**Ruta:** `sale-tickets.create` → `SaleTicketCustomerController@create` → vista `sale-ticket.customer.create`

```
🟢 sale-ticket/customer/create.blade.php
   📐 extends layouts.purchase-and-sale.general-form-vuexy
   🧩 x-tickets.introduction-create-vuexy
   ⚡ sale-used-machine.create.create-used-machine-wizard   (CreateUsedMachineWizard · WizardComponent)
      │  steps() → componentes Livewire anidados (uno por paso):
      ├─ ⚡ ticket.create.steps.select-action-step-form        (SelectActionStepForm)  [pivote/compartido]
      │     🧩 x-form-wizard-vuexy
      ├─ ⚡ ticket.create.steps.register-step-form             (RegisterStepForm)      [pivote · invitado]
      │     🧩 x-form-wizard-vuexy
      │     🧩 x-tel-input
      ├─ ⚡ sale-used-machine.create.steps.machine-type-step-form   (MachineTypeStepForm)
      │     🧩 x-form-wizard-vuexy
      ├─ ⚡ sale-used-machine.create.steps.used-machine-intake-step-form (UsedMachineIntakeStepForm)
      │     🧩 x-form-wizard-vuexy
      │     🔀 x-dynamic-component = $this->formComponent  (mode="customer")
      │         └─ 🧩 x-used-machine.types.{weaving|knitting|other}-form
      │              └─ 🧩 x-used-machine.sections.*  (según tipo)
      └─ ⚡ sale-used-machine.create.steps.media-step-form     (MediaStepForm)
            🧩 x-form-wizard-vuexy
            ⚡ livewire:dropzone (photos)
            ⚡ livewire:dropzone (video)
```
> **Retoma de borrador:** si hay `session('sale_ticket_draft')` del usuario, el wizard
> arranca directo en `media-step-form`.

## 1B · PURCHASE
**Ruta:** `purchase-tickets.create` → `PurchaseTicketCustomerController@create` → vista `purchase-ticket.customer.create`

```
🟢 purchase-ticket/customer/create.blade.php
   📐 extends layouts.purchase-and-sale.general-form-vuexy
   🧩 x-tickets.introduction-create-vuexy
   ⚡ purchase-ticket.create.purchase-ticket-create-form   (PurchaseTicketCreateForm · WizardComponent)
      │  steps():
      ├─ ⚡ ticket.create.steps.select-action-step-form   [pivote/compartido con sale]
      ├─ ⚡ ticket.create.steps.register-step-form        [pivote/compartido con sale]
      └─ ⚡ purchase-ticket.create.steps.purchase-ticket-step-form (PurchaseTicketStepForm)
            🧩 x-form-wizard-vuexy
```

---

# FLUJO 2 — EDICIÓN DE TICKET DRAFT (borrador)

## 2A · SALE
**Ruta:** `sale-tickets.drafts.edit` → `SaleTicketDraftController@edit` → vista `sale-ticket.drafts.edit`

```
🟢 sale-ticket/drafts/edit.blade.php
   📐 extends layouts.purchase-and-sale.general-form-vuexy
   ⚡ sale-used-machine.used-machine-edit-form  :ticket        (UsedMachineEditForm)
      └─ render() 🔀 según isStaff  → (ver "Bloque UsedMachineEditForm" abajo)
```

## 2B · PURCHASE
**Ruta:** `purchase-forms.drafts.edit` → `PurchaseTicketDraftController@edit` → vista `purchase-ticket.drafts.edit`

```
🟢 purchase-ticket/drafts/edit.blade.php
   📐 extends layouts.purchase-and-sale.general-form-vuexy
   🔗 enlaces a purchase-tickets.create?view=select-action-step  (volver al wizard)
   ⚡ purchase-ticket.edit.purchase-ticket-edit-form  :purchaseTicket  edit-context="draft"
```

---

# FLUJO 3 — CREACIÓN EN DASHBOARD (STAFF)

## 3A · SALE (crear máquina directa desde el dashboard)
**Entrada:** `sale-used-machines.index` → `sale-used-machine.admin.index` → tabla → botón "Create" (openModal)

```
🟢 sale-used-machine/admin/index.blade.php
   📐 extends layouts.users.main
   ⚡ tables.sale-used-machine-table              (SaleUsedMachineTable)
        └─ botón dispatch('openModal', 'sale-used-machine.used-machine-create-form')
             🪟 ⚡ sale-used-machine.used-machine-create-form   (UsedMachineCreateForm)
                  vista: livewire.sale-used-machine.used-machine-create-form
                  🧩 x-used-machine.sections.general-fields         (mode="staff")
                  🧩 x-used-machine.sections.owner-field            (mode="staff")
                  🧩 x-used-machine.sections.machine-specs-fields
                  🧩 x-used-machine.sections.cam/dobby/jacquard-details-fields
                  🧩 x-used-machine.sections.loom-accessories · beams-rollers · core-knitting · type-of-machine
                  🧩 x-used-machine.sections.price-info-fields · additional-information
                  🧩 x-used-machine.sections.custom-sections · decription
                  🧩 x-used-machine.sections.media-information       (mode="staff")
                       ⚡ livewire:dropzone (photos)
                       ⚡ livewire:dropzone (video)
```

## 3B · PURCHASE
> **No existe creación de purchase ticket desde el dashboard.** Los purchase tickets
> nacen únicamente del wizard customer (Flujo 1B). El staff solo edita (Flujo 4B).

---

# FLUJO 4 — EDICIÓN DE TICKETS PARA EL STAFF

## 4A · SALE
**Ruta:** `sale-used-machines.edit` → `SaleUsedMachineController@edit` → vista `sale-used-machine.admin.edit`

```
🟢 sale-used-machine/admin/edit.blade.php
   📐 extends layouts.users.main
   🧩 x-breadcrumb
   ⚡ sale-used-machine.used-machine-edit-form  :machine :ticket     (UsedMachineEditForm, isStaff=true)
        → render() 🔀 → livewire.sale-used-machine.used-machine-edit-form
             📄 @include partials.used-machine-staff-wizard-steps
                  🧩 x-used-machine.sections.*   (mode = canEditStaffFields() ? 'staff' : 'customer')
                  🧩 x-used-machine.sections.owner-field
                  🧩 x-used-machine.sections.custom-sections
                  🧩 x-used-machine.sections.decription
   ── Modales de media (botones openModal) ──
   🪟 ⚡ sale-used-machine.used-machine-edit-media-form   (UsedMachineEditMediaForm)
        🧩 x-used-machine.sections.media-information (mode="staff")
   🪟 ⚡ sale-used-machine.used-machine-edit-promo-flyer-form (UsedMachineEditPromoFlyerForm)
        ⚡ livewire:dropzone (promo_flyer)
   ── Ticket sidebar ──
   📄 @include sale-ticket.partials._observations_list
   📄 @include sale-ticket.observations.create
   📄 @include components.tickets.activity-history
   ⚡ sale-ticket.team-comment.sale-ticket-comment-list       :saleTicketId
   ⚡ sale-ticket.team-comment.sale-ticket-create-comment-form :saleTicketId
```

## 4B · PURCHASE
**Ruta:** `purchase-tickets.edit-any` → `PurchaseTicketController@edit` → vista `purchase-ticket.admin.edit`

```
🟢 purchase-ticket/admin/edit.blade.php
   📐 extends layouts.users.main
   🧩 x-breadcrumb
   ⚡ purchase-ticket.edit.purchase-ticket-edit-form  :purchaseTicket
   ⚡ purchase-ticket.team-comment.purchase-ticket-comment-list        :purchaseTicketId
   ⚡ purchase-ticket.team-comment.purchase-ticket-create-comment-form
   📄 @include components.tickets.activity-history
   📄 @include purchase-ticket.partials._observations-list
   📄 @include purchase-ticket.observations.create
```

> **Solo lectura (staff):** `purchase-tickets.show` → `purchase-ticket.admin.show`
> (`x-breadcrumb`, `x-print-button`; sin formularios Livewire de edición).

---

# FLUJO 5 — EDICIÓN DE TICKET PARA CUSTOMER

## 5A · SALE
**Ruta:** `sale-tickets.edit-own` → `SaleTicketCustomerController@edit` → vista `sale-ticket.customer.edit`

> **En la práctica esta vista solo se abre para tickets `RETURNED`**: el botón "Edit" de
> `SaleTicketCustomerTable` está oculto salvo status `RETURNED` (`actionRules()`), y los
> `DRAFT` se editan por la ruta aparte `sale-tickets.drafts.edit` (Flujo 2A). Ver ⚠️ abajo.

```
🟢 sale-ticket/customer/edit.blade.php
   📐 extends layouts.users.main
   ⚡ sale-used-machine.used-machine-edit-form  :ticket        (UsedMachineEditForm, isStaff=false)
        → render() 🔀 → livewire.sale-used-machine.used-machine-edit-form-customer
             🔀 $isDraft ?  (dos ramas MUTUAMENTE EXCLUYENTES, no es duplicación)
             ├─ DRAFT  → layout con pestañas:
             │    📄 @include partials.used-machine-customer-wizard-steps  (withMedia=FALSE, tab "Details")
             │    tab "Media & Assets":
             │       🧩 x-used-machine.sections.media-information (upload, mode="customer")
             │       ⚡ sale-ticket.edit.sections.sale-ticket-photo-list  (status="draft")
             │       ⚡ sale-ticket.edit.sections.sale-ticket-video-list  (status="draft")
             └─ NO-DRAFT (p.ej. RETURNED) → wizard 5 pasos:
                  📄 @include partials.used-machine-customer-wizard-steps  (withMedia=TRUE)
                     └─ paso 5 "Media": 🧩 x-used-machine.sections.media-information (solo UPLOAD)
   ── A nivel de página (card "Multimedia Assets") ──
   ⚡ sale-ticket.edit.sections.sale-ticket-photo-list  :ticket  (listas de media EXISTENTE)
   ⚡ sale-ticket.edit.sections.sale-ticket-video-list  :ticket
   📄 @include sale-ticket.partials._observations_list
```

> ⚠️ **Edge case latente (no ocurre en navegación normal):** `isEditableByCustomer()`
> (`SaleTicket.php`) **sí acepta `DRAFT`**, y `sale-tickets.edit-own` no filtra más allá
> de esa comprobación. Si se entra **por URL directa** a `/sale-ticket/{draftId}/customer`
> con un draft propio, el form entra en su rama DRAFT (que ya renderiza photo/video-list)
> **y además** la página renderiza las listas → **doble render** del mismo componente
> Livewire sin `:key` distinto. Desde la UI no pasa (botón oculto para drafts). Si se quiere
> blindar: gatear la ruta/vista a `RETURNED`, o solo mostrar las listas de página cuando
> `! $isDraft`.

## 5B · PURCHASE
**Ruta:** `purchase-tickets.edit-own` → `PurchaseTicketCustomerController@edit` → vista `purchase-ticket.customer.edit`

```
🟢 purchase-ticket/customer/edit.blade.php
   📐 extends layouts.users.main
   ⚡ purchase-ticket.edit.purchase-ticket-edit-form  :purchaseTicket  :editContext=$ticket->status->value
   📄 @include purchase-ticket.partials._observations-list
```

---

## Bloque compartido — `UsedMachineEditForm` (referenciado por Flujos 2A, 4A, 5A)

Es el corazón del sistema sale. Se usa con distintos parámetros pero SIEMPRE ramifica igual:

```
⚡ App\Livewire\SaleUsedMachine\UsedMachineEditForm
   render() 🔀
   ├─ STAFF  (isStaff=true)  → livewire.sale-used-machine.used-machine-edit-form
   │     📄 @include partials.used-machine-staff-wizard-steps
   │           mode por campo = canEditStaffFields() ? 'staff' : 'customer'
   │
   └─ CUSTOMER (isStaff=false) → livewire.sale-used-machine.used-machine-edit-form-customer
         📄 @include partials.used-machine-customer-wizard-steps  (withMedia false/true)
         🧩 x-used-machine.sections.media-information (mode="customer")
         ⚡ sale-ticket.edit.sections.sale-ticket-photo-list
         ⚡ sale-ticket.edit.sections.sale-ticket-video-list
```

Parámetros de entrada por flujo:
| Flujo | Invocación | isStaff | Vista resultante |
|-------|------------|---------|------------------|
| 2A Draft | `:ticket` | según usuario | staff **o** customer |
| 4A Staff | `:machine :ticket` | true | edit-form (staff) |
| 5A Customer | `:ticket` | false | edit-form-customer |

---

## Índice rápido de componentes Livewire (clase → vista)

| Componente (clase) | Vista Blade |
|--------------------|-------------|
| `SaleUsedMachine\Create\CreateUsedMachineWizard` | (Spatie WizardComponent, sin vista propia) |
| `SaleUsedMachine\Create\Steps\MachineTypeStepForm` | `livewire.sale-used-machine.create.steps.machine-type-step-form` |
| `SaleUsedMachine\Create\Steps\UsedMachineIntakeStepForm` | `...create.steps.used-machine-intake-step-form` |
| `SaleUsedMachine\Create\Steps\MediaStepForm` | `...create.steps.media-step-form` |
| `SaleUsedMachine\UsedMachineCreateForm` | `...used-machine-create-form` |
| `SaleUsedMachine\UsedMachineEditForm` | `...used-machine-edit-form` **/** `...used-machine-edit-form-customer` 🔀 |
| `SaleUsedMachine\UsedMachineEditMediaForm` | `...used-machine-edit-media-form` (modal) |
| `SaleUsedMachine\UsedMachineEditPromoFlyerForm` | `...used-machine-edit-promo-flyer-form` (modal) |
| `SaleUsedMachine\UsedMachineFilter` | `...used-machine-filter` (catálogo público) |
| `PurchaseTicket\Create\PurchaseTicketCreateForm` | (WizardComponent) |
| `PurchaseTicket\Create\Steps\PurchaseTicketStepForm` | `livewire.purchase-ticket.create.steps.purchase-ticket-step-form` |
| `PurchaseTicket\Edit\PurchaseTicketEditForm` | `livewire.purchase-ticket.edit.purchase-ticket-edit-form` |
| `Ticket\Create\Steps\SelectActionStepForm` | `livewire.ticket.create.steps.select-action-step-form` (compartido) |
| `Ticket\Create\Steps\RegisterStepForm` | `livewire.ticket.create.steps.register-step-form` (compartido) |

---

## Notas de mantenimiento

- **Regla `<form>` (ver CLAUDE.md):** cualquier bloque con `input-group-merge` + `is-invalid`
  debe ir envuelto en `<form>` para que apliquen los estilos de validación Vuexy.
  En steps de wizard: `<form wire:submit.prevent novalidate ...>`. **No** envolver
  `<livewire:dropzone>` (ya renderiza su propio `<form>`).
- Si cambias un `x-used-machine.sections.*` o un `x-used-machine.types.*-form`, impacta a
  **todos** los flujos sale (creación wizard, creación dashboard y las tres ediciones).
- Los pasos `select-action-step-form` y `register-step-form` son compartidos por los
  wizards de sale y purchase: un cambio ahí afecta ambos.

---
---

# 🎯 REGLAS DE NEGOCIO + PROPUESTA DE REFACTOR (Sale Used Machine)

> Esta sección describe **cómo DEBE comportarse** el módulo (contrato de negocio) y una
> propuesta para simplificar el gateo actual, que hoy es confuso. Objetivo: **una sola
> fuente de verdad** para decidir qué se muestra y qué se puede editar.

## 0. Principios inamovibles

1. **Todo se gatea por PERMISOS, nunca por roles.** Prohibido `->hasRole(...)`,
   `role:...`, etc. Usar siempre `->can('permiso')`. (Hoy ya se cumple:
   `edit_sale_used_machines`, `view_machine_owner`.)
2. **El TICKET es la única fuente de verdad del ciclo de vida.** Aprobado / bloqueado /
   devuelto se decide con `ticket->status` + `ticket->stage`. **Dejar de mezclar**
   `machine->sale_status` e `machine->is_published` en las decisiones de UI/edición.
3. **Separar dos preguntas que hoy están fundidas en el string `mode`:**
   - **¿QUÉ campos existen para este usuario?** → depende del **permiso** (+ aprobación para los extras).
   - **¿PUEDO editar ahora (lock)?** → depende del **estado del ticket**.

## 0.1 Modelo de permisos — PERMISO (quién) vs ESTADO (cuándo)

**No confundir capacidad con estado.** Un permiso responde "¿tienes la capacidad?" (estable
por usuario); el estado del ticket responde "¿puedes hacerlo ahora?" (cambia por ticket).

Reparto real (`database/seeders/PermissionSeeder.php`):

| Permiso | admin | validator | marketing | commercial | customer |
|---------|:-:|:-:|:-:|:-:|:-:|
| `edit_sale_used_machines` (= `isStaff`) | ✅ | ❌ | ✅ | ✅ | **❌** |
| `edit_sales_ticket_customer` | ❌ | ❌ | ❌ | ❌ | **✅** |
| `view_machine_owner` (= `canManageOwner`) | ✅ | ✅ | ❌ | ✅ | ❌ |

Conclusiones:
- **`edit_sale_used_machines` es exclusivo del staff** (el customer NO lo tiene) → basta
  para `isStaff`. No hace falta un permiso nuevo para distinguir staff de customer.
- **"El customer solo edita cuando `DRAFT`/`RETURNED`" NO es un permiso — es ESTADO.** Ya
  está resuelto por `SaleTicketPolicy::update()` + `SaleTicket::isEditableByCustomer()`
  (DRAFT/RETURNED, stage no aprobado). No crear permiso para esto.
- **Crear un permiso nuevo SOLO si una acción debe ser de algunos staff y no todos** — como
  ya ocurre con `view_machine_owner` (lo tienen admin/validator/commercial, no marketing).
  Los extras (`is_published`, `sale_status`, custom-sections) hoy los pueden tocar todos los
  que tienen `edit_sale_used_machines` → no requieren permiso propio (se gatean con
  `showStaffExtras() = isStaff() && isApproved()`). Si algún día se quiere restringir p.ej.
  publicar a un subconjunto, ahí sí `publish_sale_used_machine`.

## 1. Ciclo de vida esperado (una sola fuente = el ticket)

| Momento | Ticket (status / stage) | Actor | Qué edita | Extras staff | Puede "devolver" | Acción de avance |
|---------|-------------------------|-------|-----------|--------------|------------------|------------------|
| **Wizard create** | se crea `DRAFT` / `INFO` | cliente | datos máquina + media | — | — | guardar borrador |
| **Draft edit** (2A) | `DRAFT` / `INFO` | cliente (dueño) | datos máquina + media | — | — | enviar 1 o enviar todas → a revisión |
| **Enviado, NO aprobado** (4A) | stage ∈ `caseNotApproved` (`INFO`,`ANALYZE_INFO`) | staff | datos máquina base (**menos datos**) | **oculto** | ✅ (sí, porque no aprobado) → `RETURNED` | aprobar |
| **Aprobado** (4A) | stage ∈ `postApprovalStages` (`MKT`,`DISCLOSURE`,`FINAL_CHECKS`) | staff | datos base **+ extras** | **visible** (`sale_status`, `is_published`, owner, custom-sections) | ❌ (ya no) | flujo post-aprobación |
| **Devuelto** (5A) | `RETURNED` (+ stage no aprobado) | cliente (dueño) | **solo** info máquina + imágenes/video | — | — | reenviar corregido → `IN_PROGRESS` |
| **Cerrado** | `REJECTED` / `ARCHIVED` | — | **solo lectura** | lectura | — | — |
| **Modal create staff** (3A) | se crea **directo en stage aprobado** + máquina | staff | crea todo (base + extras) | visible | — | queda como ticket normal ya aprobado |

> **Modal create = "saltarse la aprobación".** No es un caso especial de UI: simplemente
> crea el ticket ya en un `stage` post-aprobación (p.ej. `MKT`) con su máquina. Así el
> mismo predicado `isApproved()` hace que aparezcan los extras. **Sin flags truco.**

## 2. Predicados canónicos (centralizar en `UsedMachineEditForm` / un trait)

> **INVARIANTE:** toda máquina de venta SIEMPRE tiene ticket. La FK `sale_used_machines.sale_ticket_id`
> es `NOT NULL` (`->constrained()` sin `nullable()`) y hasta el modal create genera su ticket
> (`UsedMachineCreateForm::save()`). Por eso **`UsedMachineEditForm::mount()` hace
> `abort_if(! $this->ticket, 404)`** y ningún predicado necesita ramas "sin ticket".
> Prohibido reintroducir chequeos tipo `if (! $ticket)` / `$ticket ?? null` / `@if ($ticket)`.

Reemplazar el string `mode` y las validaciones dispersas por estos predicados, todos
derivados de permiso o del **stage/status del ticket** (NUNCA de `machine->sale_status`):

```php
// PERMISO (quién eres) — ya existe, conservar
public function isStaff(): bool         => auth()->user()?->can('edit_sale_used_machines');
public function canManageOwner(): bool  => auth()->user()?->can('view_machine_owner');

// TICKET (estado del ciclo) — ÚNICA fuente de verdad. El ticket SIEMPRE existe (ver invariante).
public function isApproved(): bool =>
    in_array($this->ticket->stage, SaleTicketStageEnum::postApprovalStages(), true);

public function isLocked(): bool                 // solo lectura
{
    if ($this->isStaff) {
        return in_array($this->ticket->status, [SaleTicketStatusEnum::REJECTED, SaleTicketStatusEnum::ARCHIVED], true);
    }
    // customer: solo puede editar en DRAFT/RETURNED (regla de estado ya existente)
    return ! $this->ticket->isEditableByCustomer();
}

// DERIVADO (no nueva señal)
public function showStaffExtras(): bool => $this->isStaff && $this->isApproved();  // sale_status, is_published, custom-sections
```

Cambios concretos respecto a hoy:
- `isApproved()`: pasa de `machine->sale_status !== INTAKE` → **`ticket->stage ∈ postApprovalStages`**.
- `isLocked()`: nuevo. Staff = rejected/archived; customer = `! isEditableByCustomer()`.
- `canEditStaffFields()`: **se elimina**. Su papel se reparte entre `showStaffExtras()`
  (visibilidad de extras) e `isLocked()` (editabilidad). Ya no existe el ternario
  `staff/customer` que causaba el `mode` raro.
- **Media (`save()`): se queda como está**, gateada por `machine->is_published`. NO es una
  inconsistencia: la ubicación de la media (ticket vs máquina) depende de la **publicación
  en catálogo**, un eje distinto de la aprobación del ticket. Se documenta como excepción.

## 2.1 Separación de responsabilidades — 4 funciones, NUNCA fusionar

`isLocked()` NO alcanza sola. Son **3 preguntas distintas** (+1 derivada). Cada campo puede
estar en cualquier combinación de **visible/oculto** × **editable/bloqueado**, así que se
necesitan predicados independientes:

| Pregunta | Función | Fuente | Qué controla |
|----------|---------|--------|--------------|
| ¿Puedo editar / guardar? | **`isLocked()`** | **status** del ticket | pone la pantalla en solo-lectura (`<fieldset disabled>`) |
| ¿Quién eres? | **`isStaff()`** | **permiso** `edit_sale_used_machines` | si los campos de staff **existen** para ti |
| ¿Ya está aprobado? | **`isApproved()`** | **stage** del ticket (`postApprovalStages`) | si aparecen los **extras** post-aprobación |
| *(derivado)* mostrar extras staff | **`showStaffExtras()`** = `isStaff && isApproved` | — | `sale_status`, `is_published`, custom-sections |

**Por qué no se fusionan** — ejemplo que lo rompe: staff editando un ticket **aprobado pero
`ARCHIVED`** → `showStaffExtras()` = true (VE los extras) **pero** `isLocked()` = true (NO
puede guardar). Un solo booleano no puede expresar "se ve **pero** está bloqueado".

**Decisiones fijadas:**
- `isLocked()` STAFF = `status ∈ [REJECTED, ARCHIVED]` (el staff edita desde el kanban en
  cualquier otro estado). CUSTOMER = `! ticket->isEditableByCustomer()` (solo DRAFT/RETURNED).
- El **cliente NUNCA tiene extras**: solo ve datos base de máquina + media. `showStaffExtras`
  es exclusivo del staff.
- Cada función una sola responsabilidad, sin solaparse → eso es lo que elimina el `mode` raro.

## 3. Matar el prop `mode` (string) → prop booleano `showStaffExtras` ✅ HECHO

- El string `mode` (`customer`/`staff`) **eliminado** de todo el scope (cero referencias).
- Las **3 secciones** que dependían de `mode` reciben ahora el booleano `showStaffExtras`
  (kebab en Blade: `:show-staff-extras`), mismo nombre que el método `showStaffExtras()`:
  - `x-used-machine.sections.status-information` → `@props(['showStaffExtras' => false])`, `@if ($showStaffExtras)`.
  - `x-used-machine.sections.custom-sections` → `@if ($showStaffExtras)` / `@disabled(! $showStaffExtras)`.
  - `x-used-machine.sections.general-fields` → recibe y reenvía `:show-staff-extras` a status-information.
  - `x-used-machine.sections.owner-field` → se gatea aparte con `@if ($canManageOwner)` (sin prop de modo).
- **Valores por call site:** create-form `:show-staff-extras="true"`; staff-wizard-steps
  `:show-staff-extras="$this->showStaffExtras()"` (+ custom-sections dentro de
  `@if ($this->showStaffExtras())`); customer-wizard-steps y wizard de creación → default `false`.
- **Editabilidad (lock):** los campos van envueltos en `<fieldset @disabled($this->isLocked())>`
  (un solo punto de read-only) + botón Save `@disabled($this->isLocked())` + guard
  `abort_if($this->isLocked(), 403)` en `save()`. Se acabó el `mode="customer"` que significaba "bloqueado".
- Borrados los `@props(['mode' => ...])` muertos de las 12 secciones que no lo usaban.

## 4. Unificar el layout de secciones (hoy duplicado 4 veces)

El mismo listado de `x-used-machine.sections.*` está copiado en:
1. `used-machine-create-form.blade.php` (staff, `mode="staff"` hardcoded)
2. `partials/used-machine-staff-wizard-steps.blade.php` (ternario)
3. `partials/used-machine-customer-wizard-steps.blade.php` (`mode="customer"`)
4. `components/used-machine/types/{weaving,knitting,other}-form.blade.php`

**Propuesta:** un único componente/partial "cuerpo de máquina" que reciba los predicados
(`$isStaff`, `$isApproved`, `$showStaffExtras`, `$canManageOwner`, `$isLocked`) y arme
las secciones una sola vez. Create, edit (staff y customer) y los pasos del wizard lo
consumen. El componente dinámico por tipo (`weaving/knitting/other`) sigue resolviéndose
igual, pero la lógica staff/customer/extras vive en **un solo lugar**.

## 5. Resultado esperado

- `mode` desaparece del vocabulario; se razona con `isStaff / isApproved / isLocked`.
- Aprobación y bloqueo por **ticket** (stage/status); la media sigue por `is_published`
  (publicación en catálogo), documentado como eje aparte.
- El modal create no necesita rutas ni flags especiales: crea ticket aprobado + máquina.
- Un cambio de reglas de aprobación se toca en `postApprovalStages()` y punto.

## 6. Checklist de implementación (orden sugerido)

- [x] **Invariante ticket:** `abort_if(! $this->ticket, 404)` en `mount()` (+ controller) y eliminadas ramas "sin ticket" en PHP/blades.
- [x] Predicados `isApproved()` (stage), `isLocked()` (status), `showStaffExtras()` en `UsedMachineEditForm`.
- [x] `isApproved()` por stage del ticket, también en `SaleUsedMachineController@edit`. Media en `save()` NO cambió (sigue por `is_published`, documentado).
- [x] Eliminado `canEditStaffFields()` y todos sus usos.
- [x] `status-information` / `custom-sections` / `general-fields`: `mode` → `:show-staff-extras`. `owner-field` con `@if($canManageOwner)`.
- [x] Quitado `mode` de las secciones restantes y de sus `@props` (cero referencias a `mode`).
- [x] Campos envueltos en `<fieldset @disabled($this->isLocked())>` + botón Save disabled + guard `abort_if` en `save()`.
- [ ] *(Opcional/pendiente)* Consolidar el listado de secciones en un solo partial (hoy duplicado en create-form / staff-wizard / customer-wizard / types).
- [ ] **Verificar en runtime los 5 flujos** (wizard create, draft edit, staff no-aprobado, staff aprobado, returned) + modal create. *(No se pudo correr `php`/`view:cache`: no hay contenedor Sail levantado.)*
