# Rediseño de dashboards PWA — Fase 1 (frontend + datos existentes) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Reestructurar los tres dashboards de inicio de la PWA (admin/supervisor/cobrador) al Approach C — esqueleto compartido (header → quick-actions → carrusel KPI deslizable → secciones del rol) — usando **solo** datos que el backend ya devuelve, respetando el alcance de cada rol.

**Architecture:** Se extraen componentes Vue compartidos (`components/dashboard/*`) que hoy viven como markup inline duplicado en cada `*Home.vue`. Los tres Home pasan a **componer** esos bloques. Sin cambios de backend: los KPIs y secciones se llenan con los payloads actuales de `/pwa/dashboard`; los elementos que requieren backend nuevo (tendencias ▲%, sparkline, desglose por cobrador) quedan para la Fase 2 y aquí se omiten u ocultan con estado vacío. Un test de feature PHP blinda el principio "cada rol ve solo lo suyo".

**Tech Stack:** Vue 3 (`<script setup>`), Pinia, Vue Router, Tailwind + tokens de `resources/js/pwa/assets/pwa.css`, Vite (build). Backend: Laravel 13 / PHPUnit para el test de alcance.

**Nota sobre verificación:** el proyecto **no tiene runner de tests JS** (no hay vitest en `package.json`; los tests son PHP). Por eso los componentes Vue se verifican con `npm run build` (sin errores) + revisión visual en el preview del navegador (Task 11). Añadir un runner JS está fuera de alcance (cambiaría la postura del proyecto). El único test automatizado nuevo es el guard de alcance de datos en PHP (Task 1), que protege el principio rector durante la reestructura.

**Permisos disponibles en el store `auth`** (ya expuestos, ver `resources/js/pwa/stores/auth.js`): `isAdmin`, `isSupervisor`, `isCollector`, `userRole`, `canRegisterPayments`, `canRegisterVisits`, `canCreateClients`, `canCreateCredits`, `canViewAllCredits`, `canFilterByCollector`, `canEditClients`, `canVoidPayments`.

---

### Task 1: Guard de alcance de datos por rol (PHP)

Blinda que el payload de `/pwa/dashboard` de cada rol contiene **solo** su alcance, antes de tocar el frontend. Con el backend actual estos tests pasan en verde (son regresión/guard del principio); si alguno saliera rojo, hay una fuga de alcance que se corrige antes de seguir.

**Files:**
- Test: `tests/Feature/Pwa/DashboardDataScopeTest.php`

- [ ] **Step 1: Escribir el test de alcance**

```php
<?php

declare(strict_types=1);

namespace Tests\Feature\Pwa;

use App\Models\Company;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\Sanctum\Sanctum;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Principio rector del rediseño (#43 Fase B): cada rol ve SOLO lo suyo.
 * El P&L del dueño (caja/patrimonio/utilidad/gastos) no debe filtrarse al
 * supervisor ni al cobrador; los datos de equipo no deben llegar al cobrador.
 */
class DashboardDataScopeTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    protected function setUp(): void
    {
        parent::setUp();

        foreach (['admin', 'supervisor', 'collector'] as $role) {
            Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
        }

        $plan = Plan::factory()->create(['has_pwa_access' => true]);
        $this->company = Company::factory()->create(['interest_method' => 'flat_rate']);
        Subscription::factory()->active()->create([
            'company_id' => $this->company->id,
            'plan_id' => $plan->id,
            'ends_at' => now()->addYear(),
        ]);
    }

    private function actingAsRole(string $role): void
    {
        $user = User::factory()->create(['company_id' => $this->company->id]);
        $user->assignRole($role);
        Sanctum::actingAs($user->fresh());
    }

    #[Test]
    public function the_supervisor_dashboard_never_exposes_owner_pnl(): void
    {
        $this->actingAsRole('supervisor');

        $stats = $this->getJson('/api/pwa/dashboard')
            ->assertStatus(200)
            ->json('stats');

        foreach (['cash_base', 'business_total', 'interest_earned', 'operational_expenses_today', 'disbursements_today'] as $ownerOnly) {
            $this->assertArrayNotHasKey($ownerOnly, $stats, "supervisor no debe ver {$ownerOnly}");
        }
        // Sí ve su alcance de equipo:
        $this->assertArrayHasKey('active_portfolio', $stats);
        $this->assertArrayHasKey('active_collectors', $stats);
    }

    #[Test]
    public function the_collector_dashboard_is_strictly_personal(): void
    {
        $this->actingAsRole('collector');

        $stats = $this->getJson('/api/pwa/dashboard')
            ->assertStatus(200)
            ->json('stats');

        foreach (['cash_base', 'business_total', 'interest_earned', 'active_portfolio', 'active_collectors'] as $notPersonal) {
            $this->assertArrayNotHasKey($notPersonal, $stats, "cobrador no debe ver {$notPersonal}");
        }
        // Sí ve lo suyo:
        $this->assertArrayHasKey('due_today', $stats);
        $this->assertArrayHasKey('collected_today', $stats);
    }

    #[Test]
    public function the_admin_dashboard_exposes_the_owner_pnl(): void
    {
        $this->actingAsRole('admin');

        $stats = $this->getJson('/api/pwa/dashboard')
            ->assertStatus(200)
            ->json('stats');

        foreach (['cash_base', 'business_total', 'interest_earned', 'por_cobrar', 'collected_today'] as $ownerKey) {
            $this->assertArrayHasKey($ownerKey, $stats, "admin debe ver {$ownerKey}");
        }
    }
}
```

- [ ] **Step 2: Correr el test**

Run: `php artisan test --filter=DashboardDataScopeTest`
Expected: **PASS** (3 tests). Si algún assert de `assertArrayNotHasKey` falla → hay fuga de alcance en el método `*Dashboard()` correspondiente de `app/Http/Controllers/Api/Pwa/DashboardController.php`; corregir ese método para no incluir la clave antes de seguir.

- [ ] **Step 3: Commit**

```bash
git add tests/Feature/Pwa/DashboardDataScopeTest.php
git commit -m "test(pwa): guard de alcance de datos por rol en el dashboard"
```

---

### Task 2: Componente `KpiCard.vue`

Tarjeta KPI ancha con número completo. Usa tokens de `pwa.css` (`.pwa-card`, `.num`). Variante `accent` para el KPI protagonista. `trend` es opcional y en Fase 1 se pasa `null` (las tendencias son Fase 2).

**Files:**
- Create: `resources/js/pwa/components/dashboard/KpiCard.vue`

- [ ] **Step 1: Crear el componente**

```vue
<template>
    <component
        :is="to ? 'RouterLink' : 'div'"
        :to="to || undefined"
        class="shrink-0 rounded-2xl p-4 block card-press"
        :class="accent ? 'pwa-kpi-accent' : 'pwa-card'"
        style="min-width: 152px; scroll-snap-align: start;"
    >
        <p class="text-[10px] font-medium uppercase tracking-wider text-slate-500">{{ label }}</p>
        <p class="mt-1.5 text-lg font-semibold num" :class="valueClass">{{ value }}</p>
        <p v-if="trend" class="mt-1 text-[10px] font-medium" :class="trend.dir === 'down' ? 'text-emerald-600 dark:text-emerald-400' : 'text-emerald-600 dark:text-emerald-400'">
            <span aria-hidden="true">{{ trend.dir === 'down' ? '▼' : '▲' }}</span> {{ trend.text }}
        </p>
        <p v-else-if="hint" class="mt-1 text-[10px] text-slate-500">{{ hint }}</p>
    </component>
</template>

<script setup>
defineProps({
    label:  { type: String, required: true },
    value:  { type: String, required: true },
    hint:   { type: String, default: '' },
    trend:  { type: Object, default: null },   // { dir: 'up'|'down', text: String } — Fase 2
    accent: { type: Boolean, default: false },
    to:     { type: String, default: null },
    valueClass: { type: String, default: 'text-slate-900 dark:text-white' },
})
</script>
```

- [ ] **Step 2: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 3: Commit**

```bash
git add resources/js/pwa/components/dashboard/KpiCard.vue
git commit -m "feat(pwa): componente KpiCard para el carrusel de dashboards"
```

---

### Task 3: Componente `KpiCarousel.vue` + tokens CSS

Carrusel horizontal deslizable con `scroll-snap`. Recibe un array de items con las props de `KpiCard`.

**Files:**
- Create: `resources/js/pwa/components/dashboard/KpiCarousel.vue`
- Modify: `resources/js/pwa/assets/pwa.css` (añadir `.pwa-kpi-accent` y ocultar scrollbar)

- [ ] **Step 1: Añadir tokens a `pwa.css`**

Añadir al final de `resources/js/pwa/assets/pwa.css`:

```css
/* ── KPI carousel ─────────────────────────────────────────────────────────── */
.pwa-kpi-scroller {
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.pwa-kpi-scroller::-webkit-scrollbar { display: none; }

.pwa-kpi-accent {
    background: rgba(16, 185, 129, 0.08);
    border: 1px solid rgba(16, 185, 129, 0.22);
    backdrop-filter: blur(20px) saturate(140%);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
}
html.dark .pwa-kpi-accent {
    background: rgba(45, 212, 191, 0.10);
    border: 1px solid rgba(45, 212, 191, 0.22);
}
```

- [ ] **Step 2: Crear el componente**

```vue
<template>
    <div>
        <div v-if="label" class="flex items-center justify-between px-1 mb-1.5">
            <p class="text-[10px] font-medium uppercase tracking-widest text-slate-500">{{ label }}</p>
            <span v-if="items.length > 2" class="text-[10px] text-slate-400 dark:text-slate-600">← desliza →</span>
        </div>
        <div class="flex gap-2.5 overflow-x-auto pb-1.5 pwa-kpi-scroller">
            <KpiCard v-for="(kpi, i) in items" :key="i" v-bind="kpi" />
        </div>
    </div>
</template>

<script setup>
import KpiCard from './KpiCard.vue'

defineProps({
    label: { type: String, default: '' },
    items: { type: Array, required: true },   // array de props de KpiCard
})
</script>
```

- [ ] **Step 3: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 4: Commit**

```bash
git add resources/js/pwa/components/dashboard/KpiCarousel.vue resources/js/pwa/assets/pwa.css
git commit -m "feat(pwa): componente KpiCarousel deslizable + tokens"
```

---

### Task 4: Componente `QuickActionRow.vue`

Fila de acciones rápidas con hasta 3 primarias visibles + botón "Más" que abre un bottom-sheet con las secundarias. El padre pasa las acciones ya filtradas por permiso (usando los getters del store `auth`).

**Files:**
- Create: `resources/js/pwa/components/dashboard/QuickActionRow.vue`

- [ ] **Step 1: Crear el componente**

```vue
<template>
    <div>
        <div class="grid grid-cols-4 gap-2 p-3.5 rounded-[18px] pwa-card">
            <RouterLink v-for="a in primary" :key="a.label" :to="a.to"
                class="flex flex-col items-center gap-1.5 card-press">
                <span class="w-11 h-11 rounded-2xl flex items-center justify-center"
                      :class="a.iconBg || 'bg-emerald-500/12 text-emerald-600 dark:text-emerald-400'">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" v-html="a.icon" />
                </span>
                <span class="text-[10px] text-center leading-tight text-slate-600 dark:text-slate-300" v-html="a.label" />
            </RouterLink>

            <button type="button" @click="sheetOpen = true"
                class="flex flex-col items-center gap-1.5 card-press cursor-pointer">
                <span class="w-11 h-11 rounded-2xl flex items-center justify-center bg-slate-500/10 text-slate-600 dark:text-slate-300">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h.01M12 12h.01M19 12h.01" />
                    </svg>
                </span>
                <span class="text-[10px] text-slate-600 dark:text-slate-300">Más</span>
            </button>
        </div>

        <!-- Bottom sheet -->
        <Teleport to="body">
            <div v-if="sheetOpen" class="fixed inset-0 z-50 flex items-end" @click.self="sheetOpen = false">
                <div class="absolute inset-0 bg-black/40" @click="sheetOpen = false"></div>
                <div class="relative w-full rounded-t-3xl p-5 pb-8 pwa-card-elevated safe-area-bottom">
                    <div class="w-10 h-1 rounded-full bg-slate-300 dark:bg-white/15 mx-auto mb-4"></div>
                    <p class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Más acciones</p>
                    <RouterLink v-for="a in more" :key="a.label" :to="a.to" @click="sheetOpen = false"
                        class="flex items-center gap-3 py-3 border-b border-slate-100 dark:border-white/5 last:border-0 card-press">
                        <span class="w-9 h-9 rounded-xl flex items-center justify-center bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
                            <svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" v-html="a.icon" />
                        </span>
                        <span class="text-sm text-slate-700 dark:text-slate-200">{{ a.label }}</span>
                    </RouterLink>
                </div>
            </div>
        </Teleport>
    </div>
</template>

<script setup>
import { ref } from 'vue'

defineProps({
    primary: { type: Array, required: true },  // [{ icon, label, to, iconBg }] — máx 3
    more:    { type: Array, default: () => [] },
})

const sheetOpen = ref(false)
</script>
```

Nota: `a.icon` es la cadena de `<path>` SVG (heroicons outline) que ya se usa en la app. `a.label` admite `<br>` para dos líneas. El padre construye estos arrays (Tasks 8–10) filtrando por permiso.

- [ ] **Step 2: Añadir `.safe-area-bottom` a `pwa.css`** (si no existe)

```css
.safe-area-bottom { padding-bottom: max(2rem, calc(env(safe-area-inset-bottom) + 1rem)); }
```

- [ ] **Step 3: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 4: Commit**

```bash
git add resources/js/pwa/components/dashboard/QuickActionRow.vue resources/js/pwa/assets/pwa.css
git commit -m "feat(pwa): QuickActionRow con bottom-sheet de acciones secundarias"
```

---

### Task 5: Componente `SectionCard.vue`

Wrapper glass con título opcional y slot de acción a la derecha. Unifica el look de las tarjetas de sección.

**Files:**
- Create: `resources/js/pwa/components/dashboard/SectionCard.vue`

- [ ] **Step 1: Crear el componente**

```vue
<template>
    <div class="rounded-2xl p-4 pwa-card">
        <div v-if="title || $slots.action" class="flex items-center justify-between mb-3">
            <p class="text-xs font-medium uppercase tracking-wide text-slate-600 dark:text-slate-300">{{ title }}</p>
            <slot name="action" />
        </div>
        <slot />
    </div>
</template>

<script setup>
defineProps({ title: { type: String, default: '' } })
</script>
```

- [ ] **Step 2: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 3: Commit**

```bash
git add resources/js/pwa/components/dashboard/SectionCard.vue
git commit -m "feat(pwa): SectionCard para secciones del dashboard"
```

---

### Task 6: Componente `DashboardHeader.vue` (extraer de HomeView)

Extrae el header (saludo, fecha, badge de rol, campana) de `HomeView.vue` a un componente reutilizable. El badge se oculta para el cobrador (comportamiento actual).

**Files:**
- Create: `resources/js/pwa/components/home/DashboardHeader.vue`

- [ ] **Step 1: Crear el componente**

```vue
<template>
    <div class="home-header relative overflow-hidden px-5 pb-6 safe-area-top">
        <div class="absolute -top-8 -right-8 w-52 h-52 rounded-full pointer-events-none"
             style="background: radial-gradient(circle, rgba(16,185,129,0.18) 0%, transparent 70%); filter: blur(24px);"></div>
        <div class="relative z-10 flex justify-between items-start">
            <div>
                <p class="text-white/60 text-sm capitalize font-medium">{{ formattedDate }}</p>
                <h1 class="text-xl font-bold text-white mt-0.5 tracking-tight">Hola, {{ userName }}</h1>
                <div v-if="role !== 'collector'" class="mt-2">
                    <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold capitalize bg-white/15 text-white border border-white/25 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-400/25">
                        <span class="w-1.5 h-1.5 rounded-full bg-emerald-200 dark:bg-emerald-400 animate-pulse"></span>{{ role }}
                    </span>
                </div>
            </div>
            <RouterLink to="/pwa/profile" class="w-9 h-9 rounded-xl flex items-center justify-center bg-white/10 text-white shrink-0 card-press">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
                </svg>
            </RouterLink>
        </div>
    </div>
</template>

<script setup>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
import { useDashboardStore } from '../../stores/dashboard'

const authStore = useAuthStore()
const dashboardStore = useDashboardStore()

const userName = computed(() => (dashboardStore.user?.name || authStore.userName || '').split(' ')[0])
const role = computed(() => dashboardStore.role)
const formattedDate = computed(() =>
    new Date().toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' })
)
</script>

<style scoped>
.safe-area-top { padding-top: max(2.5rem, calc(env(safe-area-inset-top) + 1rem)); }
.home-header { background: linear-gradient(160deg, #064e3b 0%, #047857 55%, #059669 100%); border-bottom: 1px solid rgba(16, 185, 129, 0.20); }
:global(html.dark) .home-header { background: linear-gradient(160deg, #000000 0%, #022c22 50%, #064e3b 100%); border-bottom: none; }
</style>
```

- [ ] **Step 2: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 3: Commit**

```bash
git add resources/js/pwa/components/home/DashboardHeader.vue
git commit -m "feat(pwa): DashboardHeader extraído de HomeView"
```

---

### Task 7: Componente `PendingApprovalsAlert.vue`

Alerta que aparece **solo si hay gastos por aprobar**. La incluyen AdminHome y SupervisorHome. Reusa el store `approvals`.

**Files:**
- Create: `resources/js/pwa/components/dashboard/PendingApprovalsAlert.vue`

- [ ] **Step 1: Crear el componente**

```vue
<template>
    <RouterLink v-if="approvalsStore.hasPending" to="/pwa/approvals"
        class="flex items-center justify-between rounded-2xl px-4 py-3.5 card-press bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-500/20">
        <div class="flex items-center gap-2.5">
            <svg class="w-5 h-5 text-amber-500 dark:text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
            <span class="text-amber-700 dark:text-amber-300 text-sm font-medium">
                {{ approvalsStore.pendingCount }} gasto{{ approvalsStore.pendingCount !== 1 ? 's' : '' }} por aprobar
            </span>
        </div>
        <svg class="w-4 h-4 text-amber-400/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
        </svg>
    </RouterLink>
</template>

<script setup>
import { onMounted } from 'vue'
import { useApprovalsStore } from '../../stores/approvals'

const approvalsStore = useApprovalsStore()
onMounted(() => { approvalsStore.fetchPending() })
</script>
```

- [ ] **Step 2: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 3: Commit**

```bash
git add resources/js/pwa/components/dashboard/PendingApprovalsAlert.vue
git commit -m "feat(pwa): alerta de gastos por aprobar (admin/supervisor)"
```

---

### Task 8: Reescribir `AdminHome.vue` componiendo los bloques

**Files:**
- Modify (reescribir): `resources/js/pwa/components/home/AdminHome.vue`

- [ ] **Step 1: Reescribir el componente**

```vue
<template>
    <div class="px-4 -mt-3 space-y-3.5 pb-2">
        <QuickActionRow :primary="primaryActions" :more="moreActions" />

        <KpiCarousel :items="kpis" />

        <PendingApprovalsAlert />

        <!-- Hoy -->
        <SectionCard title="Hoy">
            <template #action><span class="text-[10px] text-slate-500">meta ${{ formatCompact(stats.due_today?.amount || 0) }}</span></template>
            <div class="flex items-baseline justify-between">
                <p class="text-2xl font-bold text-green-600 dark:text-green-400 num">${{ formatNumber(stats.collected_today?.amount || 0) }}</p>
                <p class="text-slate-500 text-xs">cobrado · {{ stats.collected_today?.count || 0 }} pagos</p>
            </div>
            <div class="h-2 rounded-full overflow-hidden bg-slate-200 dark:bg-white/6 mt-2.5">
                <div class="h-full rounded-full transition-all duration-700" style="background: linear-gradient(90deg,#059669,#34d399);"
                     :style="{ width: Math.min(todayProgress, 100) + '%' }"></div>
            </div>
            <div class="flex gap-2 mt-3">
                <RouterLink to="/pwa/credits" class="flex-1 rounded-xl p-2.5 bg-black/3 dark:bg-white/3 card-press">
                    <p class="text-slate-500 text-[10px]">Desembolsado</p>
                    <p class="text-emerald-600 dark:text-emerald-400 text-sm font-semibold num">${{ formatCompact(stats.disbursements_today?.amount || 0) }} <span class="text-slate-400 text-[10px] font-normal">· {{ stats.disbursements_today?.count || 0 }}</span></p>
                </RouterLink>
                <RouterLink to="/pwa/financials" class="flex-1 rounded-xl p-2.5 bg-black/3 dark:bg-white/3 card-press">
                    <p class="text-slate-500 text-[10px]">Gastos</p>
                    <p class="text-amber-600 dark:text-amber-400 text-sm font-semibold num">${{ formatCompact(stats.operational_expenses_today?.amount || 0) }} <span class="text-slate-400 text-[10px] font-normal">· {{ stats.operational_expenses_today?.count || 0 }}</span></p>
                </RouterLink>
            </div>
        </SectionCard>

        <!-- Cartera -->
        <RouterLink to="/pwa/delinquency" class="block rounded-2xl p-4 pwa-card card-press">
            <div class="flex items-center justify-between">
                <p class="text-slate-600 dark:text-slate-300 text-xs font-medium">Saldo por Cobrar</p>
                <p class="text-violet-600 dark:text-violet-400 text-lg font-semibold num">${{ formatNumber(stats.por_cobrar?.amount || 0) }}</p>
            </div>
            <div class="h-2 rounded-full overflow-hidden flex bg-slate-200 dark:bg-white/5 mt-3">
                <div class="h-full" style="background: linear-gradient(90deg,#059669,#10b981);" :style="{ width: vigentePercent + '%' }"></div>
                <div class="h-full" style="background: linear-gradient(90deg,#dc2626,#ef4444);" :style="{ width: (100 - vigentePercent) + '%' }"></div>
            </div>
            <div class="flex justify-between mt-2">
                <span class="text-[10px] text-slate-500"><span class="inline-block w-2 h-2 rounded-full bg-emerald-500 align-[1px] mr-1"></span>Vigente <span class="text-emerald-600 dark:text-emerald-400 font-semibold">${{ formatCompact(stats.por_cobrar?.current || 0) }}</span></span>
                <span class="text-[10px] text-slate-500"><span class="inline-block w-2 h-2 rounded-full bg-red-500 align-[1px] mr-1"></span>Vencida <span class="text-red-600 dark:text-red-400 font-semibold">${{ formatCompact(stats.por_cobrar?.overdue || 0) }}</span></span>
            </div>
        </RouterLink>
    </div>
</template>

<script setup>
import { computed } from 'vue'
import { useDashboardStore } from '../../stores/dashboard'
import QuickActionRow from '../dashboard/QuickActionRow.vue'
import KpiCarousel from '../dashboard/KpiCarousel.vue'
import SectionCard from '../dashboard/SectionCard.vue'
import PendingApprovalsAlert from '../dashboard/PendingApprovalsAlert.vue'

const dashboardStore = useDashboardStore()
const stats = computed(() => dashboardStore.stats)

const ICON_COIN = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />'
const ICON_FILE_PLUS = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />'
const ICON_RECEIPT = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l-2 2m0 0l-2-2m2 2V4m0 16h10a2 2 0 002-2V6a2 2 0 00-2-2H9" />'
const ICON_USER_PLUS = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />'
const ICON_CASH = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />'
const ICON_CHART = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />'

const primaryActions = [
    { icon: ICON_COIN, label: 'Registrar<br>pago', to: '/pwa/collections', iconBg: 'bg-emerald-500/12 text-emerald-600 dark:text-emerald-400' },
    { icon: ICON_FILE_PLUS, label: 'Nuevo<br>crédito', to: '/pwa/credits/new', iconBg: 'bg-teal-500/12 text-teal-600 dark:text-teal-400' },
    { icon: ICON_RECEIPT, label: 'Registrar<br>gasto', to: '/pwa/expenses/new', iconBg: 'bg-amber-500/12 text-amber-600 dark:text-amber-400' },
]
const moreActions = [
    { icon: ICON_USER_PLUS, label: 'Nuevo cliente', to: '/pwa/clients/new' },
    { icon: ICON_CASH, label: 'Finanzas / Caja', to: '/pwa/financials' },
    { icon: ICON_CHART, label: 'Cartera y mora', to: '/pwa/delinquency' },
]

const kpis = computed(() => [
    { label: 'Patrimonio',  value: money(stats.value.business_total?.amount), hint: 'caja + por cobrar', accent: true, to: '/pwa/financials', valueClass: 'text-slate-900 dark:text-white' },
    { label: 'Utilidad mes', value: money(stats.value.interest_earned),        hint: 'interés ganado',   to: '/pwa/financials', valueClass: 'text-emerald-600 dark:text-emerald-400' },
    { label: 'Caja',        value: money(stats.value.cash_base?.amount),       hint: 'disponible',       to: '/pwa/financials', valueClass: 'text-emerald-600 dark:text-emerald-400' },
    { label: 'Mora',        value: (stats.value.delinquency_rate || 0) + '%',  hint: 'de la cartera',    to: '/pwa/delinquency', valueClass: 'text-slate-900 dark:text-white' },
])

const vigentePercent = computed(() => {
    const total = stats.value.por_cobrar?.amount || 0
    if (total <= 0) return 0
    return Math.round(((stats.value.por_cobrar?.current || 0) / total) * 100)
})
const todayProgress = computed(() => {
    const meta = stats.value.due_today?.amount || 0
    if (meta <= 0) return 0
    return Math.round(((stats.value.collected_today?.amount || 0) / meta) * 100)
})

function formatNumber(num) { return new Intl.NumberFormat('es-CO', { maximumFractionDigits: 0 }).format(num || 0) }
function money(num) { return '$' + formatNumber(num) }
function formatCompact(num) {
    if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
    if (num >= 1000) return Math.round(num / 1000) + 'K'
    return formatNumber(num)
}
</script>
```

Nota: si `interest_earned` no está en el payload (Fase 2 lo afina a mensual), la tarjeta "Utilidad mes" muestra `$0` — aceptable en Fase 1; la Fase 2 lo corrige a utilidad mensual real + tendencia.

- [ ] **Step 2: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 3: Commit**

```bash
git add resources/js/pwa/components/home/AdminHome.vue
git commit -m "feat(pwa): AdminHome reestructurado (Approach C)"
```

---

### Task 9: Reescribir `SupervisorHome.vue`

Alcance de equipo. Sin caja/utilidad/gastos. En Fase 1 el "desglose por cobrador" se representa con una `SectionCard` puente que enlaza a `/pwa/team` (el desglose diario real es Fase 2).

**Files:**
- Modify (reescribir): `resources/js/pwa/components/home/SupervisorHome.vue`

- [ ] **Step 1: Reescribir el componente**

```vue
<template>
    <div class="px-4 -mt-3 space-y-3.5 pb-2">
        <QuickActionRow :primary="primaryActions" :more="moreActions" />

        <KpiCarousel :items="kpis" />

        <PendingApprovalsAlert />

        <!-- Equipo (puente Fase 1 → desglose diario en Fase 2) -->
        <SectionCard title="Mi equipo">
            <template #action>
                <RouterLink to="/pwa/team" class="text-[10px] text-emerald-600 dark:text-emerald-400">ver todos →</RouterLink>
            </template>
            <div class="flex items-center justify-between">
                <div class="flex items-center gap-2.5">
                    <span class="w-9 h-9 rounded-xl flex items-center justify-center bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
                        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
                    </span>
                    <div>
                        <p class="text-slate-900 dark:text-white text-sm font-semibold">{{ stats.active_collectors || 0 }} cobradores activos</p>
                        <p class="text-slate-500 text-[11px]">rendimiento del día por cobrador</p>
                    </div>
                </div>
                <svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
            </div>
        </SectionCard>

        <!-- Cartera del equipo -->
        <RouterLink to="/pwa/delinquency" class="block rounded-2xl p-4 pwa-card card-press">
            <div class="flex items-center justify-between">
                <p class="text-slate-600 dark:text-slate-300 text-xs font-medium">Cartera del equipo</p>
                <p class="text-emerald-600 dark:text-emerald-400 text-lg font-semibold num">${{ formatNumber(stats.active_portfolio?.amount || 0) }}</p>
            </div>
            <div class="flex justify-between mt-3">
                <div>
                    <p class="text-slate-500 text-[10px]">Vencido</p>
                    <p class="text-red-600 dark:text-red-400 text-sm font-semibold num">${{ formatCompact(stats.overdue?.amount || 0) }}</p>
                </div>
                <div class="text-right">
                    <p class="text-slate-500 text-[10px]">Cuotas vencidas</p>
                    <p class="text-slate-700 dark:text-slate-200 text-sm font-semibold num">{{ stats.overdue?.count || 0 }}</p>
                </div>
            </div>
        </RouterLink>
    </div>
</template>

<script setup>
import { computed } from 'vue'
import { useDashboardStore } from '../../stores/dashboard'
import QuickActionRow from '../dashboard/QuickActionRow.vue'
import KpiCarousel from '../dashboard/KpiCarousel.vue'
import SectionCard from '../dashboard/SectionCard.vue'
import PendingApprovalsAlert from '../dashboard/PendingApprovalsAlert.vue'

const dashboardStore = useDashboardStore()
const stats = computed(() => dashboardStore.stats)

const ICON_FILE_PLUS = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />'
const ICON_ROUTE = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />'
const ICON_USERS = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />'
const ICON_USER_PLUS = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />'
const ICON_EYE = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />'

const primaryActions = [
    { icon: ICON_FILE_PLUS, label: 'Nuevo<br>crédito', to: '/pwa/credits/new', iconBg: 'bg-teal-500/12 text-teal-600 dark:text-teal-400' },
    { icon: ICON_ROUTE, label: 'Cobros<br>del día', to: '/pwa/collections', iconBg: 'bg-emerald-500/12 text-emerald-600 dark:text-emerald-400' },
    { icon: ICON_USERS, label: 'Equipo', to: '/pwa/team', iconBg: 'bg-emerald-500/12 text-emerald-600 dark:text-emerald-400' },
]
const moreActions = [
    { icon: ICON_USER_PLUS, label: 'Nuevo cliente', to: '/pwa/clients/new' },
    { icon: ICON_EYE, label: 'Cartera y mora', to: '/pwa/delinquency' },
]

const kpis = computed(() => [
    { label: 'Cobrado equipo',  value: money(stats.value.collected_today?.amount), hint: (stats.value.collected_today?.count || 0) + ' pagos', accent: true, to: '/pwa/collections', valueClass: 'text-green-600 dark:text-green-400' },
    { label: 'Cartera vigente', value: money(stats.value.active_portfolio?.amount), hint: 'saldo por cobrar', to: '/pwa/delinquency', valueClass: 'text-emerald-600 dark:text-emerald-400' },
    { label: 'Vencido',         value: money(stats.value.overdue?.amount),          hint: (stats.value.overdue?.count || 0) + ' cuotas', to: '/pwa/delinquency', valueClass: 'text-red-600 dark:text-red-400' },
    { label: 'Cobradores',      value: String(stats.value.active_collectors || 0),  hint: 'activos', to: '/pwa/team', valueClass: 'text-slate-900 dark:text-white' },
])

function formatNumber(num) { return new Intl.NumberFormat('es-CO', { maximumFractionDigits: 0 }).format(num || 0) }
function money(num) { return '$' + formatNumber(num) }
function formatCompact(num) {
    if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
    if (num >= 1000) return Math.round(num / 1000) + 'K'
    return formatNumber(num)
}
</script>
```

- [ ] **Step 2: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 3: Commit**

```bash
git add resources/js/pwa/components/home/SupervisorHome.vue
git commit -m "feat(pwa): SupervisorHome reestructurado (alcance equipo)"
```

---

### Task 10: Reescribir `CollectorHome.vue`

Alcance personal. Añade quick-actions + carrusel KPI, **conservando** las secciones que ya funcionan (Meta del día, Mi cartera vencida, Mi semana, sync). Reutiliza `WeeklyProgressChart` existente y la lógica de aging ya presente.

**Files:**
- Modify (reescribir): `resources/js/pwa/components/home/CollectorHome.vue`

- [ ] **Step 1: Reescribir el componente**

Mantener **toda** la lógica `<script setup>` actual de `CollectorHome.vue` (computeds `dueToday`, `collectedToday`, `progressPercent`, `remainingAmount`, `overdueCount`, `overdueAmount`, `oldestOverdueDays`, `visibleBuckets`, `bucketTextColor`, `syncNow`, etc.) y **añadir**: imports de `QuickActionRow`, `KpiCarousel`; los arrays `primaryActions`/`moreActions`; el computed `kpis`. En el `<template>`, anteponer `<QuickActionRow>` + `<KpiCarousel>` antes de la tarjeta "Meta de hoy" existente, que se conserva tal cual (junto con Overdue Risk, WeeklyProgressChart y el banner de sync).

Añadir al `<script setup>` (además de lo existente):

```js
import QuickActionRow from '../dashboard/QuickActionRow.vue'
import KpiCarousel from '../dashboard/KpiCarousel.vue'

const ICON_COIN = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />'
const ICON_ROUTE = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />'
const ICON_USER_PLUS = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />'
const ICON_FILE_PLUS = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />'

const primaryActions = [
    { icon: ICON_COIN, label: 'Registrar<br>pago', to: '/pwa/collections', iconBg: 'bg-emerald-500/12 text-emerald-600 dark:text-emerald-400' },
    { icon: ICON_ROUTE, label: 'Mi<br>ruta', to: '/pwa/credits/order', iconBg: 'bg-teal-500/12 text-teal-600 dark:text-teal-400' },
    { icon: ICON_USER_PLUS, label: 'Nuevo<br>cliente', to: '/pwa/clients/new', iconBg: 'bg-emerald-500/12 text-emerald-600 dark:text-emerald-400' },
]
const moreActions = [
    { icon: ICON_FILE_PLUS, label: 'Nuevo crédito', to: '/pwa/credits/new' },
]

const kpis = computed(() => [
    { label: 'Meta hoy',    value: money(dueToday.value.amount),      hint: dueToday.value.count + ' cuotas', accent: true, valueClass: 'text-slate-900 dark:text-white' },
    { label: 'Cobrado hoy', value: money(collectedToday.value.amount), hint: collectedToday.value.count + ' pagos', valueClass: 'text-green-600 dark:text-green-400' },
    { label: 'Mi vencido',  value: money(overdueAmount.value),         hint: overdueCount.value + ' cuotas', to: '/pwa/collections?filter=overdue', valueClass: 'text-red-600 dark:text-red-400' },
    { label: 'Semana',      value: money(weekTotal.value),             hint: 'cobrado', valueClass: 'text-emerald-600 dark:text-emerald-400' },
])

function money(num) { return '$' + new Intl.NumberFormat('es-CO', { maximumFractionDigits: 0 }).format(num || 0) }
```

En el `<template>`, la primera línea dentro del `<div class="px-4 ...">` pasa a ser:

```vue
<QuickActionRow :primary="primaryActions" :more="moreActions" />
<KpiCarousel :items="kpis" />
```

seguidas de las secciones existentes sin cambios.

- [ ] **Step 2: Verificar build**

Run: `npm run build`
Expected: build sin errores.

- [ ] **Step 3: Commit**

```bash
git add resources/js/pwa/components/home/CollectorHome.vue
git commit -m "feat(pwa): CollectorHome con quick-actions + carrusel KPI (alcance personal)"
```

---

### Task 11: Cablear `HomeView.vue` + CHANGELOG + verificación holística

**Files:**
- Modify: `resources/js/pwa/views/HomeView.vue`
- Modify: `CHANGELOG.md`

- [ ] **Step 1: Simplificar `HomeView.vue` usando `DashboardHeader`**

Reemplazar el bloque `<!-- ── Header ── -->` inline por `<DashboardHeader />`, e importar el componente. El `<script setup>` conserva `onMounted` (fetchDashboard + loadPendingPayments) y el routing por rol. Resultado:

```vue
<template>
    <div class="pwa-bg-ambient min-h-dvh pb-28">
        <DashboardHeader />
        <AdminHome      v-if="role === 'admin'" />
        <SupervisorHome v-else-if="role === 'supervisor'" />
        <CollectorHome  v-else />
    </div>
</template>

<script setup>
import { computed, onMounted } from 'vue'
import { useDashboardStore } from '../stores/dashboard'
import { useSyncStore } from '../stores/sync'
import DashboardHeader from '../components/home/DashboardHeader.vue'
import AdminHome from '../components/home/AdminHome.vue'
import SupervisorHome from '../components/home/SupervisorHome.vue'
import CollectorHome from '../components/home/CollectorHome.vue'

const dashboardStore = useDashboardStore()
const syncStore = useSyncStore()
const role = computed(() => dashboardStore.role)

onMounted(() => {
    dashboardStore.fetchDashboard()
    syncStore.loadPendingPayments()
})
</script>
```

- [ ] **Step 2: Añadir entrada al CHANGELOG**

En `CHANGELOG.md`, bajo `## [Sin publicar]`, añadir una sección `### Añadido` (o dentro de la existente) con un bullet que resuma: rediseño de los tres dashboards de la PWA (Approach C — quick-actions, carrusel KPI deslizable, alcance de datos por rol), Fase 1 sobre datos existentes; referencia al spec y al plan; nota de que tendencias/sparkline/desglose por cobrador van en Fase 2.

- [ ] **Step 3: Build final + verificación visual**

Run: `npm run build`
Expected: build sin errores.

Verificación visual (preview del navegador, con dev server local `php artisan serve` + `npm run dev`):
- Login como **admin** → dashboard muestra quick-actions (Registrar pago/Nuevo crédito/Registrar gasto/Más), carrusel KPI deslizable con números completos, sección Hoy, Cartera.
- Login como **supervisor** → NO aparece "Registrar pago"; KPIs de equipo; sin caja/utilidad/gastos.
- Login como **cobrador** → quick-actions personales; Meta del día, cartera vencida y semana conservadas.
- Alternar claro/oscuro: legibilidad y glass correctos en ambos.
- El bottom-sheet "Más" abre y cierra.

- [ ] **Step 4: Correr el guard de alcance + Pint**

Run: `php artisan test --filter=DashboardDataScopeTest && ./vendor/bin/pint --dirty`
Expected: 3 tests PASS; Pint limpio.

- [ ] **Step 5: Commit**

```bash
git add resources/js/pwa/views/HomeView.vue CHANGELOG.md
git commit -m "feat(pwa): HomeView usa DashboardHeader + CHANGELOG rediseño Fase 1"
```

---

## Self-Review (cobertura vs. spec)

- **Esqueleto compartido** (header → quick-actions → KPI carousel → secciones): Tasks 2–7 (componentes) + 8–11 (composición). ✅
- **Carrusel KPI deslizable, número completo:** Tasks 2–3 (`KpiCard`/`KpiCarousel`, `money()` sin abreviar). ✅
- **Quick-actions por permiso real:** Tasks 8–10 construyen `primaryActions`/`moreActions` por rol; el supervisor no incluye "Registrar pago". ✅
- **Alcance por rol (principio rector):** Task 1 lo blinda con test; Tasks 8–10 solo consumen las claves del alcance de cada rol. ✅
- **Alerta de gastos por aprobar (admin/supervisor):** Task 7 + incluida en Tasks 8 y 9, no en 10. ✅
- **Cobrador conserva lo existente:** Task 10 preserva Meta/Overdue/Weekly/sync. ✅
- **Claro + oscuro:** todos los componentes usan tokens de `pwa.css` (`.pwa-card`, `dark:` variants) + Task 11 verifica ambos. ✅
- **Backend nuevo NO en Fase 1:** tendencias/sparkline/desglose diario por cobrador quedan fuera; `trend` se pasa `null`; supervisor usa `SectionCard` puente. Documentado en Task 8/9 notas. ✅

**Consistencia de tipos:** `KpiCard` props (`label,value,hint,trend,accent,to,valueClass`) coinciden con los objetos que arman los `kpis` en Tasks 8–10. `QuickActionRow` props (`primary,more`) coinciden con `primaryActions`/`moreActions`. `SectionCard` (`title` + slot `action`) usado consistentemente. ✅

**Placeholders:** ninguno `TBD/TODO`; el único dato que puede venir en `$0` (utilidad mensual) está documentado como afinamiento de Fase 2, no como hueco del plan. ✅
