# Editar clientes desde la PWA — 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:** Permitir editar un cliente existente desde la PWA (datos de contacto, cédula y dirección primaria), solo-online, reflejando el cambio en IndexedDB.

**Architecture:** Nuevo endpoint `PUT /api/pwa/clients/{id}` (autorizado por rol + visibilidad por empresa, cédula única ignorando al propio cliente, upsert de la dirección primaria). En el frontend se extrae un componente `ClientForm.vue` compartido por crear y una nueva `ClientEditView`, con botón "Editar" en el detalle y actualización inmediata de `db.clients`.

**Tech Stack:** Laravel 13 / PHP 8.3, Filament aparte; PWA en Vue 3 (script setup) + vue-router + Pinia + Dexie (IndexedDB); tests backend con PHPUnit + Sanctum. Spec: `docs/superpowers/specs/2026-07-18-pwa-edit-clients-design.md`.

---

## File Structure

**Backend**
- Modify `app/Http/Controllers/Api/Pwa/Traits/RoleAwareQueries.php` — add `canEditClients()`.
- Create `app/Http/Requests/Pwa/UpdateClientRequest.php` — validación de edición (cédula única ignorando al propio).
- Modify `app/Http/Controllers/Api/Pwa/ClientController.php` — add `update()`, add private `visibleClientsQuery()` helper, refactor `show()` to use it, and extend `show()` response with `state`/`city`/`address_line_1`.
- Modify `routes/api.php` — add `PUT /clients/{id}`.
- Create `tests/Feature/Pwa/UpdateClientTest.php` — feature tests.

**Frontend**
- Create `resources/js/pwa/components/ClientForm.vue` — formulario compartido (extraído de create).
- Modify `resources/js/pwa/views/ClientCreateView.vue` — usar `ClientForm`.
- Create `resources/js/pwa/views/ClientEditView.vue` — vista de edición.
- Modify `resources/js/pwa/router/index.js` — import + ruta `/pwa/clients/:id/edit`.
- Modify `resources/js/pwa/stores/auth.js` — `canEditClients`.
- Modify `resources/js/pwa/views/ClientDetailView.vue` — botón "Editar".

---

## Task 1: Extend `show()` with structured address + extract visibility helper

**Files:**
- Modify: `app/Http/Controllers/Api/Pwa/ClientController.php`
- Test: `tests/Feature/Pwa/UpdateClientTest.php`

- [ ] **Step 1: Write the failing test**

Create `tests/Feature/Pwa/UpdateClientTest.php` with the setup and the first test:

```php
<?php

declare(strict_types=1);

namespace Tests\Feature\Pwa;

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

class UpdateClientTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $admin;

    private User $collector;

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

        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();
        Subscription::factory()->active()->create([
            'company_id' => $this->company->id,
            'plan_id' => $plan->id,
            'ends_at' => now()->addYear(),
        ]);

        $this->admin = User::factory()->create(['company_id' => $this->company->id]);
        $this->admin->assignRole('admin');
        $this->admin = $this->admin->fresh();

        $this->collector = User::factory()->create(['company_id' => $this->company->id]);
        $this->collector->assignRole('collector');
        $this->collector = $this->collector->fresh();
    }

    private function makeClient(?int $createdBy = null): Client
    {
        return Client::factory()->create([
            'company_id' => $this->company->id,
            'created_by_user_id' => $createdBy ?? $this->admin->id,
            'name' => 'Original Name',
            'identification' => '1000',
            'phone' => '3000000000',
            'email' => null,
            'is_active' => true,
        ]);
    }

    #[Test]
    public function show_returns_structured_address_fields_for_prefill(): void
    {
        $client = $this->makeClient();
        $client->addresses()->create([
            'is_primary' => true,
            'state' => 'Antioquia',
            'city' => 'Medellin',
            'address_line_1' => 'Cra 10 #20-30',
        ]);

        Sanctum::actingAs($this->admin);

        $this->getJson("/api/pwa/clients/{$client->id}")
            ->assertStatus(200)
            ->assertJsonPath('state', 'Antioquia')
            ->assertJsonPath('city', 'Medellin')
            ->assertJsonPath('address_line_1', 'Cra 10 #20-30');
    }
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `wsl bash -lc "cd /var/www/html/credify && php artisan test tests/Feature/Pwa/UpdateClientTest.php --filter=show_returns_structured_address_fields_for_prefill"`
Expected: FAIL — `state`/`city`/`address_line_1` missing from the `show` JSON.

- [ ] **Step 3: Add the `Builder` import**

In `app/Http/Controllers/Api/Pwa/ClientController.php`, add to the `use` block (after `use Illuminate\Http\Request;`):

```php
use Illuminate\Database\Eloquent\Builder;
```

- [ ] **Step 4: Add the `visibleClientsQuery()` helper**

Add this private method to `ClientController` (e.g. just above `show()`):

```php
    /**
     * Query base de clientes visibles para el usuario segun su rol.
     * - admin/supervisor: todos los de su empresa.
     * - collector: clientes con creditos asignados a el O creados por el.
     * Fuente unica de visibilidad para show() y update().
     */
    private function visibleClientsQuery(Request $request): Builder
    {
        $user = $request->user();
        $role = $this->getUserPwaRole($request);

        $query = Client::query()->where('company_id', $user->company_id);

        if ($role === 'collector') {
            $query->where(function ($q) use ($user) {
                $q->whereHas('credits', function ($creditQuery) use ($user) {
                    $creditQuery->where('collector_user_id', $user->id);
                })->orWhere('created_by_user_id', $user->id);
            });
        }

        return $query;
    }
```

- [ ] **Step 5: Refactor `show()` to use the helper and return structured address**

In `show()`, replace the query construction (the block that builds `$query = Client::query()->where('company_id', ...)` plus the collector `if ($role === 'collector') { ... }` and the `->with([...])->find($id)`) so it starts from the helper. Concretely, change:

```php
        $query = Client::query()
            ->where('company_id', $user->company_id);

        // Collectors solo pueden ver clientes con creditos asignados o que ellos crearon
        if ($role === 'collector') {
            $query->where(function ($q) use ($user) {
                $q->whereHas('credits', function ($creditQuery) use ($user) {
                    $creditQuery->where('collector_user_id', $user->id);
                })
                    ->orWhere('created_by_user_id', $user->id);
            });
        }

        $client = $query
            ->with([
```

to:

```php
        $client = $this->visibleClientsQuery($request)
            ->with([
```

(The `->with([...])->find($id)` tail stays unchanged.) Then, in the final `response()->json([...])` of `show()`, add the three structured fields right after `'address' => $client->formatted_address,`:

```php
            'address' => $client->formatted_address,
            'state' => $client->primaryAddress?->state,
            'city' => $client->primaryAddress?->city,
            'address_line_1' => $client->primaryAddress?->address_line_1,
```

(`primaryAddress` is already eager-loaded in `show()`.)

- [ ] **Step 6: Run test to verify it passes**

Run: `wsl bash -lc "cd /var/www/html/credify && php artisan test tests/Feature/Pwa/UpdateClientTest.php --filter=show_returns_structured_address_fields_for_prefill"`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
wsl bash -lc "cd /var/www/html/credify && git add app/Http/Controllers/Api/Pwa/ClientController.php tests/Feature/Pwa/UpdateClientTest.php && git commit -m 'feat(pwa): show() de cliente devuelve direccion estructurada + helper de visibilidad'"
```

---

## Task 2: `PUT /clients/{id}` — update endpoint (permission, visibility, address upsert, uniqueness)

**Files:**
- Modify: `app/Http/Controllers/Api/Pwa/Traits/RoleAwareQueries.php`
- Create: `app/Http/Requests/Pwa/UpdateClientRequest.php`
- Modify: `app/Http/Controllers/Api/Pwa/ClientController.php`
- Modify: `routes/api.php`
- Test: `tests/Feature/Pwa/UpdateClientTest.php`

- [ ] **Step 1: Write the failing tests**

Append these test methods to `tests/Feature/Pwa/UpdateClientTest.php` (inside the class):

```php
    #[Test]
    public function an_admin_can_update_a_client_and_upsert_its_primary_address(): void
    {
        $client = $this->makeClient();
        Sanctum::actingAs($this->admin);

        $this->putJson("/api/pwa/clients/{$client->id}", [
            'name' => 'Nuevo Nombre',
            'identification' => '2000',
            'phone' => '3111111111',
            'email' => 'nuevo@mail.com',
            'state' => 'Santander',
            'city' => 'Bucaramanga',
            'address' => 'Calle 1 #2-3',
        ])->assertStatus(200)
            ->assertJsonPath('client.name', 'Nuevo Nombre');

        $this->assertDatabaseHas('clients', [
            'id' => $client->id,
            'name' => 'Nuevo Nombre',
            'identification' => '2000',
            'phone' => '3111111111',
            'email' => 'nuevo@mail.com',
        ]);
        $this->assertDatabaseHas('client_addresses', [
            'client_id' => $client->id,
            'is_primary' => true,
            'state' => 'Santander',
            'city' => 'Bucaramanga',
            'address_line_1' => 'Calle 1 #2-3',
        ]);
    }

    #[Test]
    public function a_collector_can_update_a_client_they_created(): void
    {
        $client = $this->makeClient(createdBy: $this->collector->id);
        Sanctum::actingAs($this->collector);

        $this->putJson("/api/pwa/clients/{$client->id}", [
            'name' => 'Editado Cobrador',
            'identification' => '1000',
            'phone' => '3000000000',
        ])->assertStatus(200);

        $this->assertDatabaseHas('clients', ['id' => $client->id, 'name' => 'Editado Cobrador']);
    }

    #[Test]
    public function a_collector_cannot_update_a_client_outside_their_scope(): void
    {
        $client = $this->makeClient(createdBy: $this->admin->id);
        Sanctum::actingAs($this->collector);

        $this->putJson("/api/pwa/clients/{$client->id}", [
            'name' => 'X',
            'identification' => '1000',
            'phone' => '3000000000',
        ])->assertStatus(404);

        $this->assertDatabaseHas('clients', ['id' => $client->id, 'name' => 'Original Name']);
    }

    #[Test]
    public function a_client_from_another_company_cannot_be_updated(): void
    {
        $otherCompany = Company::factory()->create();
        $foreign = Client::factory()->create([
            'company_id' => $otherCompany->id,
            'created_by_user_id' => $this->admin->id,
            'identification' => '9999',
        ]);
        Sanctum::actingAs($this->admin);

        $this->putJson("/api/pwa/clients/{$foreign->id}", [
            'name' => 'Hack',
            'identification' => '9999',
            'phone' => '3000000000',
        ])->assertStatus(404);
    }

    #[Test]
    public function keeping_own_identification_is_allowed_but_duplicating_another_is_rejected(): void
    {
        $client = $this->makeClient(); // identification 1000
        Client::factory()->create([
            'company_id' => $this->company->id,
            'created_by_user_id' => $this->admin->id,
            'identification' => '5000',
        ]);
        Sanctum::actingAs($this->admin);

        $this->putJson("/api/pwa/clients/{$client->id}", [
            'name' => 'Same Ced', 'identification' => '1000', 'phone' => '3000000000',
        ])->assertStatus(200);

        $this->putJson("/api/pwa/clients/{$client->id}", [
            'name' => 'Dup', 'identification' => '5000', 'phone' => '3000000000',
        ])->assertStatus(422);
    }
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `wsl bash -lc "cd /var/www/html/credify && php artisan test tests/Feature/Pwa/UpdateClientTest.php"`
Expected: the 5 new tests FAIL (route `PUT /clients/{id}` returns 405/404).

- [ ] **Step 3: Add `canEditClients()` to the trait**

In `app/Http/Controllers/Api/Pwa/Traits/RoleAwareQueries.php`, right after the `canCreateClients()` method, add:

```php
    /**
     * Verifica si el usuario puede editar clientes.
     * Permitido para: admin, supervisor, collector (igual que crear).
     */
    protected function canEditClients(Request $request): bool
    {
        $role = $this->getUserPwaRole($request);

        return in_array($role, ['admin', 'supervisor', 'collector'], true);
    }
```

- [ ] **Step 4: Create `UpdateClientRequest`**

Create `app/Http/Requests/Pwa/UpdateClientRequest.php`:

```php
<?php

declare(strict_types=1);

namespace App\Http\Requests\Pwa;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateClientRequest extends FormRequest
{
    public function authorize(): bool
    {
        // Autorizacion real en el controlador (canEditClients + visibilidad).
        return true;
    }

    public function rules(): array
    {
        $companyId = $this->user()->company_id;
        $clientId = (int) $this->route('id');

        return [
            'name' => ['required', 'string', 'max:255'],
            'identification' => [
                'required',
                'string',
                'max:50',
                Rule::unique('clients', 'identification')
                    ->where('company_id', $companyId)
                    ->ignore($clientId),
            ],
            'phone' => ['required', 'string', 'max:20'],
            'email' => ['nullable', 'email', 'max:255'],
            'state' => ['nullable', 'string', 'max:100'],
            'city' => ['nullable', 'string', 'max:100'],
            'address' => ['nullable', 'string', 'max:500'],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => 'El nombre es obligatorio.',
            'identification.required' => 'La cedula/identificacion es obligatoria.',
            'identification.unique' => 'Ya existe un cliente con esta cedula.',
            'phone.required' => 'El telefono es obligatorio.',
            'email.email' => 'El email no es valido.',
        ];
    }
}
```

- [ ] **Step 5: Add the `DB` and request imports to the controller**

In `app/Http/Controllers/Api/Pwa/ClientController.php`, add to the `use` block:

```php
use App\Http\Requests\Pwa\UpdateClientRequest;
use Illuminate\Support\Facades\DB;
```

(`use App\Http\Requests\Pwa\StoreClientRequest;` already exists; add the Update one alongside it. Keep imports alphabetically consistent with the existing block.)

- [ ] **Step 6: Add the `update()` method**

Add to `ClientController` (e.g. right after `store()`):

```php
    /**
     * PUT /api/pwa/clients/{id}
     *
     * Edita un cliente existente (datos de contacto, cedula y direccion primaria).
     * Solo-online desde la PWA. No crea cliente nuevo → sin guard de cupo.
     */
    public function update(UpdateClientRequest $request, int $id): JsonResponse
    {
        if (! $this->canEditClients($request)) {
            return response()->json([
                'message' => 'No tienes permiso para editar clientes.',
            ], 403);
        }

        $client = $this->visibleClientsQuery($request)->find($id);

        if (! $client) {
            return response()->json([
                'message' => 'Cliente no encontrado o no tienes acceso.',
            ], 404);
        }

        DB::transaction(function () use ($client, $request) {
            $client->update([
                'name' => $request->name,
                'identification' => $request->identification,
                'phone' => $request->phone,
                'email' => $request->email,
            ]);

            $hasAddress = $request->filled('address')
                || $request->filled('state')
                || $request->filled('city');

            if ($hasAddress) {
                $payload = [
                    'address_line_1' => $request->address,
                    'state' => $request->state,
                    'city' => $request->city,
                ];

                $address = $client->addresses()->where('is_primary', true)->first();

                if ($address) {
                    $address->update($payload);
                } else {
                    $client->addresses()->create($payload + ['is_primary' => true]);
                }
            }
        });

        $client->refresh()->load('primaryAddress');

        return response()->json([
            'message' => 'Cliente actualizado correctamente.',
            'client' => [
                'id' => $client->id,
                'name' => $client->name,
                'identification' => $client->identification,
                'phone' => $client->phone,
                'email' => $client->email,
                'state' => $client->primaryAddress?->state,
                'city' => $client->primaryAddress?->city,
                'address_line_1' => $client->primaryAddress?->address_line_1,
            ],
        ]);
    }
```

- [ ] **Step 7: Register the route**

In `routes/api.php`, right after the `POST /clients` route (`clients.store`), add:

```php
        Route::put('/clients/{id}', [ClientController::class, 'update'])
            ->name('clients.update');
```

- [ ] **Step 8: Run tests to verify they pass**

Run: `wsl bash -lc "cd /var/www/html/credify && php artisan test tests/Feature/Pwa/UpdateClientTest.php"`
Expected: all 6 tests PASS.

- [ ] **Step 9: Static analysis + style**

Run: `wsl bash -lc "cd /var/www/html/credify && vendor/bin/pint app/Http/Controllers/Api/Pwa/ClientController.php app/Http/Requests/Pwa/UpdateClientRequest.php app/Http/Controllers/Api/Pwa/Traits/RoleAwareQueries.php tests/Feature/Pwa/UpdateClientTest.php && vendor/bin/phpstan analyse app/Http/Controllers/Api/Pwa/ClientController.php app/Http/Requests/Pwa/UpdateClientRequest.php --no-progress"`
Expected: Pint PASS, PHPStan `[OK] No errors`.

- [ ] **Step 10: Commit**

```bash
wsl bash -lc "cd /var/www/html/credify && git add app/Http/Controllers/Api/Pwa/ClientController.php app/Http/Requests/Pwa/UpdateClientRequest.php app/Http/Controllers/Api/Pwa/Traits/RoleAwareQueries.php routes/api.php tests/Feature/Pwa/UpdateClientTest.php && git commit -m 'feat(pwa): endpoint PUT /clients/{id} para editar clientes'"
```

---

## Task 3: Extract `ClientForm.vue` and refactor `ClientCreateView`

**Files:**
- Create: `resources/js/pwa/components/ClientForm.vue`
- Modify: `resources/js/pwa/views/ClientCreateView.vue`

> No hay infraestructura de tests JS en el repo → se verifica con `npm run build` + revisión en navegador.

- [ ] **Step 1: Create `ClientForm.vue`**

Create `resources/js/pwa/components/ClientForm.vue`:

```vue
<template>
    <form @submit.prevent="onSubmit" class="px-4 py-4 space-y-3">
        <!-- Name -->
        <div class="rounded-2xl px-4 py-4 pwa-card">
            <label class="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-2">Nombre completo *</label>
            <input v-model="form.name" type="text" required maxlength="255" placeholder="Ej: Juan Carlos Pérez" :disabled="submitting"
                class="w-full bg-transparent text-sm placeholder-slate-400 dark:placeholder-slate-700 border-none outline-hidden" />
        </div>

        <!-- Identification -->
        <div class="rounded-2xl px-4 py-4 pwa-card">
            <label class="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-2">Cédula / Identificación *</label>
            <input v-model="form.identification" type="text" required maxlength="50" placeholder="Ej: 12345678" :disabled="submitting"
                class="w-full bg-transparent text-sm placeholder-slate-400 dark:placeholder-slate-700 border-none outline-hidden" />
        </div>

        <!-- Phone -->
        <div class="rounded-2xl px-4 py-4 pwa-card">
            <label class="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-2">Teléfono *</label>
            <input v-model="form.phone" type="tel" required maxlength="20" placeholder="Ej: 3001234567" :disabled="submitting"
                class="w-full bg-transparent text-sm placeholder-slate-400 dark:placeholder-slate-700 border-none outline-hidden" />
        </div>

        <!-- Email -->
        <div class="rounded-2xl px-4 py-4 pwa-card">
            <label class="text-xs font-bold text-slate-500 uppercase tracking-widest block mb-2">Email <span class="text-slate-400 dark:text-slate-600 normal-case font-normal">(opcional)</span></label>
            <input v-model="form.email" type="email" maxlength="255" placeholder="Ej: cliente@email.com" :disabled="submitting"
                class="w-full bg-transparent text-sm placeholder-slate-400 dark:placeholder-slate-700 border-none outline-hidden" />
        </div>

        <!-- Location -->
        <div class="rounded-2xl px-4 py-4 space-y-3 pwa-card">
            <label class="text-xs font-bold text-slate-500 uppercase tracking-widest block">Ubicación</label>
            <div>
                <p class="text-slate-500 text-xs mb-1.5">Departamento</p>
                <select v-model="form.state" :disabled="submitting || loadingDepartments" @change="onDepartmentChange"
                    class="w-full rounded-xl px-3 py-2.5 text-sm focus:outline-hidden focus:ring-1 focus:ring-emerald-500/50 appearance-none pwa-select">
                    <option value="">Seleccionar departamento</option>
                    <option v-for="dept in departments" :key="dept" :value="dept">{{ dept }}</option>
                </select>
            </div>
            <div>
                <p class="text-slate-500 text-xs mb-1.5">Ciudad / Municipio</p>
                <select v-model="form.city" :disabled="submitting || !form.state || loadingCities"
                    class="w-full rounded-xl px-3 py-2.5 text-sm focus:outline-hidden focus:ring-1 focus:ring-emerald-500/50 appearance-none pwa-select">
                    <option value="">{{ form.state ? 'Seleccionar ciudad' : 'Primero seleccione departamento' }}</option>
                    <option v-for="city in cities" :key="city" :value="city">{{ city }}</option>
                </select>
            </div>
            <div>
                <p class="text-slate-500 text-xs mb-1.5">Dirección exacta</p>
                <textarea v-model="form.address" rows="2" maxlength="500" placeholder="Ej: Calle 123 #45-67, Barrio Centro" :disabled="submitting"
                    class="w-full rounded-xl px-3 py-2.5 text-sm resize-none focus:outline-hidden focus:ring-1 focus:ring-emerald-500/50 pwa-input"></textarea>
            </div>
        </div>

        <!-- Error -->
        <div v-if="error" class="rounded-2xl px-4 py-3 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-400/20">
            <p class="text-red-700 dark:text-red-300 text-sm">{{ error }}</p>
        </div>

        <!-- Offline notice -->
        <div v-if="!isOnline" class="rounded-2xl px-4 py-3 flex items-start gap-2 pwa-notice-amber">
            <svg class="w-4 h-4 text-amber-500 dark:text-amber-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
            <p class="text-amber-700 dark:text-amber-300 text-sm">Sin conexión — se requiere internet para guardar y sincronizar.</p>
        </div>

        <!-- Submit -->
        <button type="submit" :disabled="submitting || !isFormValid || !isOnline"
            class="w-full py-4 rounded-2xl font-bold text-lg text-white disabled:opacity-40 card-press"
            style="background: linear-gradient(135deg, #059669, #10b981); box-shadow: 0 4px 20px rgba(16,185,129,0.25);">
            <span v-if="submitting" class="flex items-center justify-center gap-2">
                <svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
                </svg>
                Guardando...
            </span>
            <span v-else>{{ submitLabel }}</span>
        </button>
    </form>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import api from '../services/api'

const props = defineProps({
    initial: { type: Object, default: () => ({}) },
    submitting: { type: Boolean, default: false },
    error: { type: String, default: null },
    submitLabel: { type: String, default: 'Guardar' },
})

const emit = defineEmits(['submit'])

const form = ref({
    name: props.initial.name || '',
    identification: props.initial.identification || '',
    phone: props.initial.phone || '',
    email: props.initial.email || '',
    state: props.initial.state || '',
    city: props.initial.city || '',
    address: props.initial.address || '',
})

const isOnline = ref(navigator.onLine)
const onOnline = () => { isOnline.value = true }
const onOffline = () => { isOnline.value = false }
window.addEventListener('online', onOnline)
window.addEventListener('offline', onOffline)
onUnmounted(() => {
    window.removeEventListener('online', onOnline)
    window.removeEventListener('offline', onOffline)
})

const departments = ref([])
const cities = ref([])
const loadingDepartments = ref(false)
const loadingCities = ref(false)

const isFormValid = computed(() =>
    form.value.name.trim() && form.value.identification.trim() && form.value.phone.trim()
)

onMounted(async () => {
    await fetchDepartments()
    if (form.value.state) {
        await fetchCities(form.value.state)
    }
})

async function fetchDepartments() {
    loadingDepartments.value = true
    try {
        const response = await api.get('/pwa/locations/departments')
        departments.value = response.data.data || []
    } catch (err) {
        console.error('[ClientForm] Error loading departments:', err)
    } finally {
        loadingDepartments.value = false
    }
}

async function fetchCities(department) {
    if (!department) { cities.value = []; return }
    loadingCities.value = true
    try {
        const response = await api.get('/pwa/locations/cities', { params: { department } })
        cities.value = response.data.data || []
    } catch (err) {
        console.error('[ClientForm] Error loading cities:', err)
        cities.value = []
    } finally {
        loadingCities.value = false
    }
}

function onDepartmentChange() {
    form.value.city = ''
    fetchCities(form.value.state)
}

function onSubmit() {
    if (!isFormValid.value || props.submitting) return
    emit('submit', {
        name: form.value.name.trim(),
        identification: form.value.identification.trim(),
        phone: form.value.phone.trim(),
        email: form.value.email.trim() || null,
        state: form.value.state || null,
        city: form.value.city || null,
        address: form.value.address.trim() || null,
    })
}
</script>
```

- [ ] **Step 2: Rewrite `ClientCreateView.vue` to use `ClientForm`**

Replace the ENTIRE contents of `resources/js/pwa/views/ClientCreateView.vue` with:

```vue
<template>
    <div class="min-h-screen pb-28" style="background-color: var(--pwa-bg);">
        <PwaHeader title="Nuevo Cliente" subtitle="Completa la información del cliente" :back="goBack" />
        <ClientForm :submitting="submitting" :error="error" submit-label="Crear Cliente" @submit="submitClient" />
    </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useToast } from '../composables/useToast'
import api from '../services/api'
import ClientForm from '../components/ClientForm.vue'

const router = useRouter()
const authStore = useAuthStore()
const toast = useToast()

const submitting = ref(false)
const error = ref(null)

onMounted(() => {
    if (!authStore.canCreateClients) {
        toast.error('No tienes permiso para crear clientes')
        router.replace('/pwa/clients')
    }
})

function goBack() {
    router.back()
}

async function submitClient(payload) {
    submitting.value = true
    error.value = null

    try {
        const response = await api.post('/pwa/clients', payload)
        toast.success('Cliente creado correctamente')

        if (response.data.client?.id) {
            router.push(`/pwa/clients/${response.data.client.id}`)
        } else {
            router.push('/pwa/clients')
        }
    } catch (err) {
        console.error('[ClientCreate] Error:', err)
        if (err.response?.status === 422) {
            const errors = err.response.data.errors
            const firstError = Object.values(errors || {})[0]
            error.value = Array.isArray(firstError) ? firstError[0] : err.response.data.message
        } else if (err.response?.status === 403) {
            error.value = 'No tienes permiso para crear clientes'
        } else {
            error.value = err.response?.data?.message || 'Error al crear el cliente'
        }
    } finally {
        submitting.value = false
    }
}
</script>
```

(`PwaHeader` is globally registered — no import needed, matching the previous version.)

- [ ] **Step 3: Build to verify no errors**

Run: `wsl bash -lc "cd /var/www/html/credify && npm run build 2>&1 | tail -15"`
Expected: build succeeds (no Vue/Vite compile errors).

- [ ] **Step 4: Commit**

```bash
wsl bash -lc "cd /var/www/html/credify && git add resources/js/pwa/components/ClientForm.vue resources/js/pwa/views/ClientCreateView.vue && git commit -m 'refactor(pwa): extraer ClientForm compartido; ClientCreateView lo usa'"
```

---

## Task 4: `canEditClients` in the auth store

**Files:**
- Modify: `resources/js/pwa/stores/auth.js`

- [ ] **Step 1: Add the computed permission**

In `resources/js/pwa/stores/auth.js`, right after the `canCreateClients` computed, add:

```javascript
    const canEditClients = computed(() =>
        permissions.value.can_edit_clients ?? ['admin', 'supervisor', 'collector'].includes(userRole.value)
    )
```

- [ ] **Step 2: Export it from the store**

In the store's `return { ... }` object, add `canEditClients` next to `canCreateClients` (the return list that includes `canCreateClients, canCreateCredits, canViewAllCredits`):

```javascript
        canCreateClients,
        canEditClients,
        canCreateCredits,
```

- [ ] **Step 3: Build to verify no errors**

Run: `wsl bash -lc "cd /var/www/html/credify && npm run build 2>&1 | tail -8"`
Expected: build succeeds.

- [ ] **Step 4: Commit**

```bash
wsl bash -lc "cd /var/www/html/credify && git add resources/js/pwa/stores/auth.js && git commit -m 'feat(pwa): permiso canEditClients en el auth store'"
```

---

## Task 5: `ClientEditView` + route + "Editar" button + IndexedDB update

**Files:**
- Create: `resources/js/pwa/views/ClientEditView.vue`
- Modify: `resources/js/pwa/router/index.js`
- Modify: `resources/js/pwa/views/ClientDetailView.vue`

- [ ] **Step 1: Create `ClientEditView.vue`**

Create `resources/js/pwa/views/ClientEditView.vue`:

```vue
<template>
    <div class="min-h-screen pb-28" style="background-color: var(--pwa-bg);">
        <PwaHeader title="Editar Cliente" :subtitle="clientName" :back="goBack" />

        <div v-if="loading" class="flex items-center justify-center py-20">
            <svg class="animate-spin w-8 h-8 text-emerald-500" fill="none" viewBox="0 0 24 24">
                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
            </svg>
        </div>

        <ClientForm v-else-if="initial" :initial="initial" :submitting="submitting" :error="error"
            submit-label="Guardar Cambios" @submit="submitEdit" />
    </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useToast } from '../composables/useToast'
import api from '../services/api'
import { db } from '../db'
import ClientForm from '../components/ClientForm.vue'

const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const toast = useToast()

const clientId = Number(route.params.id)
const loading = ref(true)
const submitting = ref(false)
const error = ref(null)
const initial = ref(null)
const clientName = ref('')

onMounted(async () => {
    if (!authStore.canEditClients) {
        toast.error('No tienes permiso para editar clientes')
        router.replace(`/pwa/clients/${clientId}`)
        return
    }
    await fetchClient()
})

function goBack() {
    router.back()
}

async function fetchClient() {
    loading.value = true
    try {
        const response = await api.get(`/pwa/clients/${clientId}`)
        const c = response.data
        clientName.value = c.name
        initial.value = {
            name: c.name || '',
            identification: c.identification || '',
            phone: c.phone || '',
            email: c.email || '',
            state: c.state || '',
            city: c.city || '',
            address: c.address_line_1 || '',
        }
    } catch (err) {
        console.error('[ClientEdit] Error loading client:', err)
        toast.error('No se pudo cargar el cliente')
        router.replace(`/pwa/clients/${clientId}`)
    } finally {
        loading.value = false
    }
}

async function submitEdit(payload) {
    submitting.value = true
    error.value = null

    try {
        await api.put(`/pwa/clients/${clientId}`, payload)
        toast.success('Cliente actualizado correctamente')

        // Consistencia offline inmediata: refleja los cambios en IndexedDB.
        await db.clients.update(clientId, {
            name: payload.name,
            identification: payload.identification,
            phone: payload.phone,
            email: payload.email,
        })

        router.push(`/pwa/clients/${clientId}`)
    } catch (err) {
        console.error('[ClientEdit] Error:', err)
        if (err.response?.status === 422) {
            const errors = err.response.data.errors
            const firstError = Object.values(errors || {})[0]
            error.value = Array.isArray(firstError) ? firstError[0] : err.response.data.message
        } else if (err.response?.status === 403) {
            error.value = 'No tienes permiso para editar clientes'
        } else if (err.response?.status === 404) {
            error.value = 'Cliente no encontrado'
        } else {
            error.value = err.response?.data?.message || 'Error al actualizar el cliente'
        }
    } finally {
        submitting.value = false
    }
}
</script>
```

- [ ] **Step 2: Register the route**

In `resources/js/pwa/router/index.js`, add the lazy import next to the other client views:

```javascript
const ClientEditView = () => import('../views/ClientEditView.vue')
```

Then add the route immediately after the `client-detail` route object:

```javascript
    {
        path: '/pwa/clients/:id/edit',
        name: 'client-edit',
        component: ClientEditView,
        meta: { requiresAuth: true },
        props: true
    },
```

- [ ] **Step 3: Add the "Editar" button to `ClientDetailView`**

In `resources/js/pwa/views/ClientDetailView.vue`, immediately after the contact-card wrapper `</div>` that closes the `<div class="px-4 -mt-3">` block (the one containing Llamar/WhatsApp/Dirección), add:

```vue
            <!-- Edit client -->
            <div v-if="authStore.canEditClients" class="px-4 mt-3">
                <RouterLink
                    :to="{ name: 'client-edit', params: { id: client.id } }"
                    class="w-full flex items-center justify-center gap-2 py-3 rounded-2xl font-medium text-sm text-slate-700 dark:text-slate-300 card-press pwa-card"
                >
                    <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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
                    </svg>
                    Editar cliente
                </RouterLink>
            </div>
```

(`authStore` is already used in this view — e.g. `authStore.canCreateCredits` — so no new import is needed. `RouterLink` is globally registered.)

- [ ] **Step 4: Build to verify no errors**

Run: `wsl bash -lc "cd /var/www/html/credify && npm run build 2>&1 | tail -15"`
Expected: build succeeds.

- [ ] **Step 5: Commit**

```bash
wsl bash -lc "cd /var/www/html/credify && git add resources/js/pwa/views/ClientEditView.vue resources/js/pwa/router/index.js resources/js/pwa/views/ClientDetailView.vue && git commit -m 'feat(pwa): vista de edicion de cliente + boton Editar + update de IndexedDB'"
```

---

## Task 6: Full verification + CHANGELOG

**Files:**
- Modify: `CHANGELOG.md`

- [ ] **Step 1: Run the full PWA test suite**

Run: `wsl bash -lc "cd /var/www/html/credify && php artisan test tests/Feature/Pwa"`
Expected: all PASS (including the 6 new `UpdateClientTest` cases).

- [ ] **Step 2: Add a CHANGELOG entry**

In `CHANGELOG.md`, under `## [Sin publicar]` → `### Añadido`, add as the first bullet:

```markdown
- **Editar clientes desde la PWA.** Nueva vista `/pwa/clients/:id/edit` (botón "Editar" en el detalle) con endpoint `PUT /api/pwa/clients/{id}`: edita nombre, cédula, teléfono, email y dirección primaria. Permiso admin/supervisor/cobrador por empresa (misma visibilidad que el detalle); cédula única ignorando al propio cliente. Solo-online (igual que crear) y actualiza IndexedDB (`db.clients`) al guardar. Se extrajo un `ClientForm.vue` compartido por crear y editar. Tests `UpdateClientTest` (6: admin/cobrador editan, fuera de alcance 404, cross-company 404, unicidad de cédula, `show()` devuelve dirección estructurada). PHPStan + Pint limpios; `npm run build` OK.
```

- [ ] **Step 3: Commit**

```bash
wsl bash -lc "cd /var/www/html/credify && git add CHANGELOG.md && git commit -m 'docs(changelog): editar clientes desde la PWA'"
```

- [ ] **Step 4: Manual browser check (dev server)**

Verify in the browser: open a client detail → "Editar" appears (for an allowed role) → the form prefills (including departamento/ciudad/dirección) → change a field → save → returns to detail with the change → the client list reflects the change offline (IndexedDB updated).

---

## Self-Review

**Spec coverage:**
- PUT endpoint + permission + visibility → Task 2. ✓
- `UpdateClientRequest` (cédula única ignorando al propio) → Task 2. ✓
- Address upsert (update or create primary) → Task 2 (`update()`), tested in Task 2. ✓
- `show()` structured address for prefill → Task 1. ✓
- `ClientForm.vue` shared + create refactor → Task 3. ✓
- `canEditClients` (auth store) → Task 4; (backend trait) → Task 2. ✓
- `ClientEditView` + route + "Editar" button → Task 5. ✓
- IndexedDB update on save → Task 5. ✓
- Online-only → `ClientForm` disables submit when `!isOnline` (Task 3). ✓
- Tests (admin/collector edit, out-of-scope 404, cross-company 404, uniqueness, show address) → Tasks 1-2. ✓
- Out of scope (is_active, preferred_payment_method, offline queue, multiple addresses, audit) → not implemented. ✓

**Placeholder scan:** No TBD/TODO. All code blocks complete.

**Type/name consistency:** `visibleClientsQuery()` used in Task 1 (show) and Task 2 (update). `canEditClients` used in backend (Task 2 trait) and frontend (Task 4 store, Tasks 5 views). Route name `client-edit` used in Task 5 (router + detail button + edit view redirects). `db.clients.update(clientId, ...)` matches Dexie's merge API. Field names (`state`/`city`/`address_line_1`) consistent between `show()` response, `ClientEditView` prefill, and `ClientForm` payload (`address` → `address_line_1`).

**Scope:** Focused on a single feature; one plan.
