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
@@ -31,8 +31,12 @@ class TenantInsuranceService
float $coveragePercent,
int $franchiseRials = 0,
?int $annualCeilingRials = null,
?int $effectiveFrom = null,
?int $effectiveTo = null,
?string $kind = null,
): TenantInsurance {
if ($this->insuranceRepo->find($insuranceId) === null) {
$insurance = $this->insuranceRepo->find($insuranceId);
if ($insurance === null) {
throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'بیمه یافت نشد', 404);
}
@@ -45,8 +49,15 @@ class TenantInsuranceService
$contract->setCoveragePercent($coveragePercent)
->setFranchiseRials($franchiseRials)
->setAnnualCeilingRials($annualCeilingRials)
// kind defaults to the catalog type; caller may override to categorise the contract.
->setKind($kind ?? $insurance->getType()->value)
->setEffectiveTo($effectiveTo)
->setActive(true);
if ($effectiveFrom !== null) {
$contract->setEffectiveFrom($effectiveFrom);
}
$this->repo->save($contract);
return $contract;