feat(insurance): redesign insurance management page to match Figma

Rebuild the /admin/insurance-pricing contracts UI to the Figma "مدیریت بیمه"
design and inject the coverage/franchise/ceiling fields the design omitted.

Backend:
- Add contract-level `kind` column to TenantInsurance (basic|supplementary),
  defaulting to the catalog type; migration Version20260715093358.
- POST/PATCH /billing/tenant-insurances now accept effective_from,
  effective_to, kind; PATCH also toggles is_active without clobbering the
  user-set effective_to (unlike DELETE/deactivate).
- List returns the latest version of every insurance (active + inactive) via
  TenantInsuranceRepository::findLatestByTenant, for the فعال/غیرفعال toggle.

Frontend:
- New InsuranceModal (ui/Modal + SearchableSelect + PersianDateInput) with the
  seven fields; submit "ثبت بیمه".
- TenantInsuranceContracts rebuilt: header + search box, desktop table
  (ردیف/نام/کد/نوع/وضعیت/عملیات) and mobile cards, status toggle -> PATCH.
- utils: isoToUnix/unixToIso helpers for contract dates.

Tests: TenantInsuranceContractApiTest (create/edit/toggle/list, 5 cases),
InsuranceModal + TenantInsuranceContracts vitest suites, docs/api updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-15 13:23:38 +03:30
co-authored by Claude Opus 4.8
parent e94f832c8a
commit d7cb9ea5a3
12 changed files with 741 additions and 167 deletions
@@ -312,7 +312,7 @@ class InsuranceController extends BaseController
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
}
$contracts = $this->tenantInsuranceRepo->findActiveByTenant($entityType, $entityId);
$contracts = $this->tenantInsuranceRepo->findLatestByTenant($entityType, $entityId);
$byId = [];
foreach ($this->insuranceRepo->findActive(null) as $ins) {
@@ -322,7 +322,8 @@ class InsuranceController extends BaseController
$data = array_map(function (TenantInsurance $c) use ($byId) {
$row = $c->toArray();
$row['insurance_name'] = $byId[$c->getInsuranceId()]['name'] ?? null;
$row['insurance_kind'] = $byId[$c->getInsuranceId()]['type'] ?? null;
// Contract-level kind wins over the catalog type when the tenant categorised it.
$row['insurance_kind'] = $c->getKind() ?? ($byId[$c->getInsuranceId()]['type'] ?? null);
return $row;
}, $contracts);
@@ -352,6 +353,9 @@ class InsuranceController extends BaseController
(int) ($data['franchise_rials'] ?? 0),
isset($data['annual_ceiling_rials']) && $data['annual_ceiling_rials'] !== null
? (int) $data['annual_ceiling_rials'] : null,
isset($data['effective_from']) && $data['effective_from'] !== null ? (int) $data['effective_from'] : null,
isset($data['effective_to']) && $data['effective_to'] !== null ? (int) $data['effective_to'] : null,
isset($data['kind']) && $data['kind'] !== '' ? (string) $data['kind'] : null,
);
return $this->success(['data' => $contract->toArray()], 201);
@@ -377,6 +381,20 @@ class InsuranceController extends BaseController
if (array_key_exists('annual_ceiling_rials', $data)) {
$contract->setAnnualCeilingRials($data['annual_ceiling_rials'] !== null ? (int) $data['annual_ceiling_rials'] : null);
}
if (array_key_exists('kind', $data)) {
$contract->setKind($data['kind'] !== '' && $data['kind'] !== null ? (string) $data['kind'] : null);
}
if (array_key_exists('effective_from', $data) && $data['effective_from'] !== null) {
$contract->setEffectiveFrom((int) $data['effective_from']);
}
if (array_key_exists('effective_to', $data)) {
$contract->setEffectiveTo($data['effective_to'] !== null ? (int) $data['effective_to'] : null);
}
// Status toggle (فعال/غیرفعال) is set here directly so it does not clobber the
// user-chosen effective_to the way the DELETE/deactivate path does.
if (array_key_exists('is_active', $data)) {
$contract->setActive((bool) $data['is_active']);
}
$this->tenantInsuranceRepo->save($contract);