# TechTitans POS — IPTV Connections List (legacy parity)

**Date:** 2026-11-12 · **Branch:** `techtitans/integration` · Local commit only (no push/deploy).
Preserves internal DB tables / API endpoints / model names (no `iptv_subscriptions` rename, no domain-semantics
change). No NH POS / legacy PHP touched. Safe dev data only.

## Old portal vs new list — feature comparison

| Field / capability | Old TechTitans portal | New list (before) | New list (now) |
|---|---|---|---|
| Customer name | ✅ | ✅ | ✅ (links to CRM) |
| Customer phone / email | ✅ | ✗ | ✅ (under the name) |
| MC ID / user login | ✅ | ✅ | ✅ (links to Account Details) |
| Actual user | ✅ | ✅ (inline) | ✅ (under MC ID) |
| MAC ID | ✅ | ✅ | ✅ |
| Provider / Portal + Plan | ✅ | ✅ | ✅ |
| Expiry + remaining/overdue | ✅ | date only | ✅ date **+ "In N days" / "Overdue N days" / "Expires today" / "Not activated"** |
| Service status | ✅ | ✅ | ✅ (Active / Expiring soon / Expired / Pending activation / Disabled / Cancelled) |
| Last updated | ✅ | ✗ | ✅ |
| Search customer / phone / email / MC / actual user / MAC / ref | ✅ | number + MC only | ✅ all |
| Status + provider filters | ✅ | ✅ | ✅ |
| Expiry range / expired / expiring / pending filters | partial | state only | ✅ date range + all states |
| CSV export | ✅ | ✅ (no contact) | ✅ + Phone/Email/Last updated, injection-safe |
| Modern styling / responsive | ✗ | ✅ | ✅ (kept) |

## Terminology (user-facing only)

"IPTV subscriptions" → **IPTV connections**; "New subscription" → **New connection**; "Search" → **Search
connections**; CSV filename `iptv-connections.csv`. Internal `IptvSubscription` model, `/iptv/subscriptions` routes,
tables and services are unchanged.

## Backend

- `IptvResource::customer()` now projects `phone` + `email` (operational contact — never notes/credentials). Loaded
  from the already-eager-loaded `customer` relation (no N+1).
- `IptvSubscriptionService::applyTermSearch()` extended: `subscription_number`, `account_identifier`, `actual_user`,
  `mac_id`, **and** `whereHas('customer', …)` on display_name / first_name / last_name / phone / email (reuses the
  existing relationship — no duplicate customer store). Search + all filters compose; server-side pagination + sorting
  unchanged.
- `search()` + `exportRows()` gained an **expiry date range** (`expiry_from` / `expiry_to`), validated in
  `ListIptvSubscriptionsRequest` (which already allowed `pending_activation`). Tenant/location scoping + `iptv.view`
  permission unchanged.
- CSV export: added **Phone, Email, Last updated** columns; every text cell is passed through
  `csvCell()` which prefixes a leading `= + - @` (or tab/CR) with `'` to **neutralise CSV/formula injection**. Still
  no credential column; `iptv.export` permission + scope + filters respected.

## Frontend (`IptvListPage.tsx`)

Compact, operational columns on the premium Cacart table: **Customer** (name → CRM, phone·email beneath) ·
**Connection** (MC ID → Account Details, actual user beneath) · **Portal / Plan** · **MAC ID** (tabular) · **Expiry**
(date + coloured remaining/overdue hint) · **Status** (coloured badge) · **Updated** · **View**. Search box (all
fields), Status, Provider, and Expiry-from/to filters at the top; CSV export + prominent **New connection**. Distinct
links per cell (customer vs MC ID vs View) avoid accidental navigation; links are keyboard-accessible. Mobile falls
back to the card layout (name, contact, status, MC ID · provider · expiry) and the table scrolls **inside its card**
on tablet (no page overflow). Empty/loading states and pagination retained.

## Testing

- **Backend** `IptvOperationsTest` → **16 passed**, incl. the new
  `test_connections_list_exposes_contact_searches_all_fields_and_exports_safely`: projection has phone/email; search
  matches phone / email / actual user / MAC / MC; non-match returns nothing; expiry range filters in/out; CSV has
  Phone/Email/Last updated + contact values + a **formula-injection cell neutralised** (`'=CMD()…`) + no credential.
  Full `tests/Feature/Iptv` → **65 passed** (unchanged behaviour).
- **Frontend** `IptvPages.test.tsx` → **25 passed** (updated: MC ID link → connection, customer link → `/customers/:id`,
  "IPTV connections" heading).
- **Pint** clean; **PHPStan** `IptvResource` + `ListIptvSubscriptionsRequest` clean (the 1 controller + 6 service
  pre-existing `stdClass` typing errors are in untouched methods — not introduced here); `tsc -b`, `eslint`,
  `npm run build` clean.
- **Live Playwright** (real list): heading "IPTV connections" + "New connection"; search placeholder covers all
  fields; columns Customer/Connection/Portal·Plan/MAC/Expiry/Status/Updated present; **customer name → `/customers/:id`,
  MC ID → `/iptv/:id`** (both navigated); 25 rows; **no horizontal overflow at desktop 1440 / tablet 768 (table scrolls
  in-card) / mobile 390 (cards)**; 0 console errors. Screenshots in `screenshots/iptv-connections-list/`.

## Files changed

Backend: `app/Iptv/IptvResource.php`, `app/Iptv/IptvSubscriptionService.php`,
`app/Http/Controllers/Api/V1/Iptv/IptvSubscriptionController.php` (CSV columns + `csvCell`),
`app/Http/Requests/Api/V1/Iptv/ListIptvSubscriptionsRequest.php`, `tests/Feature/Iptv/IptvOperationsTest.php`.
Frontend: `src/lib/api/iptv.ts` (customer contact + expiry params), `src/pages/iptv/IptvListPage.tsx`,
`src/pages/iptv/iptv.css`, `src/pages/iptv/IptvPages.test.tsx`.

## Commits (no push)

- **Backend:** `3b5899c` — list projection/search/export parity + tests + this report + screenshots.
- **Frontend:** `b966d4e` — connections list redesign + rename + click-through.
