# 🛠️ Service Desk Fycls — Sprint Inventario: Importación + Jerarquía

Las mismas 2 mejoras aplicadas a NEXUS Fedef, pero adaptadas a **multi-tenant**:

```
1. IMPORTACIÓN MASIVA desde CSV
   ✓ Botón "Importar" en /assets
   ✓ Plantilla descargable con BOM UTF-8
   ✓ Solo crea assets en la EMPRESA ACTUAL (sesión)
   ✓ Validación de FKs (categorías, users, departments) solo de esa empresa
   ✓ Reporte de errores línea por línea

2. JERARQUÍA "DEPENDE DE" (anidamiento ilimitado)
   ✓ Cada asset puede tener parent_asset_id
   ✓ INTRA-EMPRESA: un activo solo puede depender de otro de su misma empresa
   ✓ Vista de árbol en el detalle (ancestros + dependientes)
   ✓ Anidación ilimitada
   ✓ Prevención de ciclos
   ✓ Importable desde CSV (columna parent_code)
```

---

## 🛡️ Aislamiento multi-tenant garantizado

```
Capa 1: SQL UNIQUE compuesto (company_id, code)
        → Cada empresa tiene su propio ACT-0001, ACT-0002...

Capa 2: CompanyScope (global) filtra automáticamente
        → Asset::all() devuelve solo de la empresa actual

Capa 3: BelongsToCompany trait
        → Al crear un Asset, asigna company_id automáticamente

Capa 4: Validación parent_asset_id con Rule::exists + where company_id
        → Imposible asignar como padre un asset de otra empresa
        (aún si conoces el ID, la validación lo rechaza)

Capa 5: generateCode() con MAX dentro de la empresa actual
        → Cada empresa empieza en ACT-0001 independientemente
```

---

## 📦 Archivos del paquete

```
sprint_assets_fycls/
├── LEEME.md                                          ← este archivo
├── sql/
│   └── sprint_assets_fycls.sql                       ← UNIQUE compuesto + parent_asset_id
├── plantilla/
│   └── plantilla_inventario.csv                      ← BOM UTF-8 + ejemplos
└── archivos/
    ├── Http/Controllers/
    │   └── AssetController.php                       ← REEMPLAZAR
    ├── Models/
    │   └── Asset.php                                 ← REEMPLAZAR
    ├── views/assets/
    │   ├── index.blade.php                           ← REEMPLAZAR (botón Importar)
    │   ├── show.blade.php                            ← REEMPLAZAR (sección Relaciones)
    │   ├── create.blade.php                          ← REEMPLAZAR (campo Depende de)
    │   ├── edit.blade.php                            ← REEMPLAZAR (campo Depende de)
    │   └── import.blade.php                          ← NUEVO archivo
    └── routes_patch.txt                              ← Instrucciones para routes/web.php
```

---

## 🚀 Pasos de instalación

### Paso 0: Backup

```bash
cd ~/public_html/servicedesk
mkdir -p .sprint_backups/assets_$(date +%Y%m%d_%H%M%S)
cp app/Http/Controllers/AssetController.php  .sprint_backups/assets_$(date +%Y%m%d)/
cp app/Models/Asset.php                       .sprint_backups/assets_$(date +%Y%m%d)/
cp resources/views/assets/index.blade.php     .sprint_backups/assets_$(date +%Y%m%d)/
cp resources/views/assets/show.blade.php      .sprint_backups/assets_$(date +%Y%m%d)/
cp resources/views/assets/create.blade.php    .sprint_backups/assets_$(date +%Y%m%d)/
cp resources/views/assets/edit.blade.php      .sprint_backups/assets_$(date +%Y%m%d)/
cp routes/web.php                             .sprint_backups/assets_$(date +%Y%m%d)/
```

### Paso 1: SQL — agregar columna y UNIQUE compuesto

phpMyAdmin → BD `evolucionamos_fycls_servicedesk` → tab SQL → ejecutar `sql/sprint_assets_fycls.sql`.

🔴 **IMPORTANTE**: el script intenta:

1. `DROP INDEX assets_code_unique` — quita el UNIQUE simple sobre `code`
2. `ADD UNIQUE INDEX uniq_company_code (company_id, code)` — UNIQUE compuesto
3. `ADD COLUMN parent_asset_id` — para jerarquía

Si el paso 2 falla con "Duplicate entry", significa que ya hay códigos
repetidos entre empresas (improbable porque venimos de single-tenant
recientemente, pero por si acaso). En ese caso:

```sql
-- Diagnóstico: ¿hay códigos duplicados entre empresas?
SELECT company_id, code, COUNT(*) AS reps 
FROM assets 
GROUP BY company_id, code 
HAVING reps > 1;
```

Debe devolver 0 filas. Si devuelve filas, avísame y lo resolvemos.

### Paso 2: Subir 7 archivos

| Archivo del paquete | Destino |
|---|---|
| `archivos/Http/Controllers/AssetController.php` | `app/Http/Controllers/` |
| `archivos/Models/Asset.php` | `app/Models/` |
| `archivos/views/assets/index.blade.php` | `resources/views/assets/` |
| `archivos/views/assets/show.blade.php` | `resources/views/assets/` |
| `archivos/views/assets/create.blade.php` | `resources/views/assets/` |
| `archivos/views/assets/edit.blade.php` | `resources/views/assets/` |
| `archivos/views/assets/import.blade.php` | `resources/views/assets/` (NUEVO) |

### Paso 3: Editar `routes/web.php`

Lee el archivo `archivos/routes_patch.txt` para ver el cambio exacto.

Resumen: Las 3 rutas nuevas van **dentro del grupo Sprint 3, ANTES de `Route::resource('assets', ...)`**:

```php
// ── SPRINT 3: Inventario CMDB ─────────────────────────────────────
Route::middleware(['auth','verified'])->group(function () {

    // ↓ AGREGAR ESTAS 3 LÍNEAS AQUÍ
    Route::get('assets/import',           [\App\Http\Controllers\AssetController::class, 'importForm'])->name('assets.import.form');
    Route::post('assets/import',          [\App\Http\Controllers\AssetController::class, 'import'])->name('assets.import');
    Route::get('assets/import/template',  [\App\Http\Controllers\AssetController::class, 'downloadTemplate'])->name('assets.template');

    Route::resource('assets', \App\Http\Controllers\AssetController::class);  // ← se queda
    ...
});
```

### Paso 4: Limpiar caché

```bash
cd ~/public_html/servicedesk
rm -f bootstrap/cache/*.php
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
```

### Paso 5: Verificar rutas

```bash
php artisan route:list | grep "assets"
```

Debes ver:
```
GET    assets/import          assets.import.form
POST   assets/import          assets.import
GET    assets/import/template assets.template
GET    assets                 assets.index
... (las del resource)
```

---

## ✅ Cómo probar

### Test 1: Aislamiento por empresa

```
1. Login como agente de Fycls
2. Ir a /assets → click en "Importar" → descargar plantilla
3. Crear un activo "Servidor Fycls" → genera ACT-0001
4. Crear otro "Workstation Fycls" → genera ACT-0002

5. Si eres miembro del operador Fycls, cambia al switcher → Gessig
6. Ir a /assets → debe estar VACÍO (no se ve nada de Fycls)
7. Crear "Servidor Gessig" → ALSO genera ACT-0001 (independiente!)
8. ✓ Cada empresa tiene su propia secuencia
```

### Test 2: Importación CSV

```
1. Estando en empresa "Fycls" (current_company_id = 1)
2. Ir a /assets → "Importar"
3. Descargar plantilla → editar → subir
4. ✓ Los assets se crean SOLO en Fycls (no contaminan Gessig)
5. Cambiar al switcher Gessig → /assets → no ves los nuevos
```

### Test 3: Jerarquía intra-empresa

```
1. En empresa Fycls, crear:
   - "Workstation Sala Gerencia" (sin padre)
   - "Portátil Dell" → "Depende de" = Workstation
   - "Mouse Logitech" → "Depende de" = Portátil
2. Abrir detalle del Mouse:
   ✓ Sección "Depende de" muestra árbol Workstation > Portátil
   ✓ Si Workstation está asignada a alguien, aparece su nombre
3. Abrir detalle del Portátil:
   ✓ "Depende de" muestra Workstation
   ✓ "Activos dependientes" muestra Mouse
```

### Test 4: Aislamiento de jerarquía cross-tenant

```
1. En Fycls, crear "Asset A" → ACT-0001 (en Fycls)
2. Switcher → Gessig
3. Crear "Asset B" → ACT-0001 (en Gessig)
4. Editar Asset B → en select "Depende de":
   ✓ NO aparece "Asset A" (es de otra empresa)
   ✓ Solo aparecen activos de Gessig
```

### Test 5: Prevención de ciclos

```
1. Tienes A → B → C (C depende de B, B depende de A)
2. Editar A → intentar parent = C
3. ✓ Error: "Este activo no puede depender de uno de sus descendientes"
```

### Test 6: CSV con jerarquía

```csv
name;...;parent_code;...
Workstation;...;;...
Portátil;...;ACT-0001;...      ← apunta al ACT-0001 que se creó arriba
Mouse;...;ACT-0002;...         ← apunta al Portátil
```

✓ Todos se crean en la empresa actual con la jerarquía correcta.

---

## ❓ Si algo falla

| Síntoma | Causa | Solución |
|---|---|---|
| `Duplicate entry 'X' for key 'uniq_company_code'` | Ya existe ese código en esa empresa | Verificar `SELECT company_id, code FROM assets WHERE code='X'` |
| Error 1062 al ejecutar SQL | Códigos duplicados entre empresas previos | Ver query de diagnóstico al final del SQL |
| `parent_asset_id` rechazado en form | Asset padre es de otra empresa o no existe | Solo se permiten padres de la misma empresa |
| Importación crea pero "no aparece" | Estás viendo otra empresa en el switcher | Cambia al switcher de la empresa donde importaste |
| Botón "Importar" da 404 | Rutas en orden incorrecto | Las 3 rutas nuevas deben ir ANTES de Route::resource |
| `Class 'AssetController' has no method 'importForm'` | Falta subir AssetController.php | Re-subir |

---

## 📝 Diferencias técnicas vs versión Fedef

| Aspecto | Fedef (single) | Fycls (multi-tenant) |
|---|---|---|
| `code` UNIQUE | UNIQUE simple | UNIQUE compuesto `(company_id, code)` |
| `generateCode()` | MAX global | MAX dentro de la empresa actual |
| Validación parent | Solo `exists:assets,id` | + Rule::where company_id |
| Importación FKs | Globales | Filtradas por empresa (CompanyScope automático) |
| INSERT al crear | `Asset::create([...])` | `Asset::create([...])` (BelongsToCompany asigna company_id solo) |
| `wouldCreateCycle` | Recursión global | Recursión intra-empresa (CompanyScope) |
| Vistas Blade | Iguales | Iguales (mismo HTML) |
| Botón Importar | Mismo | Mismo |

El usuario final NO nota diferencia: el sistema "se siente" como single-tenant 
para cada empresa, pero está completamente aislado.
