Skip to content

Taskeen (handover & occupancy)

Taskeen (Arabic تسكين — “housing / occupancy assignment”) is a Sales-Man-only feature that lists handover/occupancy records for a salesman and lets them export the list as a spreadsheet. In the UI it is not its own page — it is a tab inside the EOI listing screen (pages/eoi-listing hosts the Taskeen tab; frontend calls listSalesmanTaskeen/taskeen and getTaskeenReport/taskeen/export, per _inventory/frontend-apps.md:287,297). Everything below is verified against the current backend.

The portal does not define what a “taskeen record” means — it proxies whatever the CRM returns and enriches it. The shape the export builder consumes is EOI-like: each record has client_name, serial_number, paid_amount, collected_amount, payment_method, payment_date, agent/manager fields, attachments, and nested productsunittypespreferences (TaskeenExportBuilderServiceReport::buildRow, app/Services/TaskeenExportBuilderServiceReport.php:45-104). In practice it is the CRM’s occupancy/handover view of confirmed, paid interest — sourced CRM-side, read by the portal, never authored by it.

sequenceDiagram
actor SM as Sales-Man (SPA — Taskeen tab)
participant API as Backend (Laravel)
participant MW as crms-middleware
participant CRM as CRM (Egypt)
participant DB as Portal DB (launch / unit-type mirror)
SM->>API: GET /taskeen search (Taskeen tab in EOI listing)
API->>API: resolve salesman_source_id (SalesManUtil)
API->>MW: GET {api}/taskeen · header country=slug · query search + salesman_source_id
MW->>CRM: GET api_get_taskeen_url (Egypt-only binding)
CRM-->>MW: taskeen records (products unittypes preferences)
MW-->>API: 200 data (pass-through)
API->>DB: load Launch Project UnitType for enrichment
API->>API: addToTaskeenList maps launch_id project_id unit_type price
API-->>SM: 200 respondSuccess data (NO filter or sort applied)
Note over SM,API: Export is a separate call
SM->>API: GET /taskeen/export filters + extension
API->>MW: same GetTaskeenList fetch (live again)
API->>API: applyFiltersAndSortingToEOIs then TaskeenExportBuilderServiceReport build
API-->>SM: Excel download xlsx or csv (or JSON error if non-200)
Taskeen — list and export are both live CRM reads (verified)

Both live under the Sales-Man group (auth:sanctum + user-provider:sales_man, routes/api/sales-man.php:227-228) and are catalogued as SM227 / SM228 in _inventory/backend-api.md:504-505.

  1. GET taskeenTaskeenController@index (app/Http/Controllers/Api/SalesMan/TaskeenController.php:25-43). Validated by ListTaskeenRequest — whose only field is search (nullable string, app/Http/Requests/Taskeen/ListTaskeenRequest.php:14-19). It calls GetTaskeenList->handle, maps the raw middleware Response through ClientResponseMapper::mapFromMiddlewareToCms (a straight message / data / errors pass-through, app/Http/Controllers/Api/ClientResponseMapper.php:10-27), then enriches via addToTaskeenList and returns respondSuccess(['data' => …]). No filtering and no sorting happen here — only search is forwarded to the CRM.

  2. GET taskeen/exportTaskeenController@export (:45-89). Validated by the richer FiltersRequestsearch, unit_types[], launches[], sales_managers[], sort_by (units_count or payment_date), sort_dir (asc or desc), and extension (app/Http/Requests/Taskeen/FiltersRequest.php:15-34). It re-runs the same GetTaskeenList->handle fetch, and only if the middleware returned 200: applies applyFiltersAndSortingToEOIs (EOIExportFiltersService.php:16-53), flattens rows through TaskeenExportBuilderServiceReport::build (TaskeenExportBuilderServiceReport.php:11-23), and streams an Excel file named Taskeen Report {datetime}.csv when extension == 'csv', otherwise .xlsx (:71-85). On a non-200 it returns the mapped JSON error, not a file (:88).

  3. The data source — GetTaskeenList->handle (app/Actions/Taskeen/GetTaskeenList.php:14-41). For a SalesMan it resolves the caller’s CRM identity with SalesManUtil::getSourceIdByCountryIdAndUserId($country->id, $user->id) (:21-24), then issues getWithAuthentication (bearer-token cached client get, app/Clients/AbstractClient.php:97-107) to {CRMS_MIDDLEWARE_API_URL}/taskeen with header country and query search + salesman_source_id (:29-40). The middleware base comes from config/services.php:34-37 (env CRMS_MIDDLEWARE_API_URL). It returns the raw Illuminate\Http\Client\Response untouched.

addToTaskeenList (EOIExportFiltersService.php:55-108) is the join between the CRM payload and local data. For each record it: adds {field}_timestamp fields (Egypt-only date map, :65-85), resolves launch_id from the local Launch table — falling back to the literal string 'Unknown' when the launch_source_id isn’t mirrored locally (:88) — and, per nested unit-type, resolves project_id, unit_type_id, unit_type_name and price from local Project / UnitType / launch pricing, leaving them null when not found locally (:93-101).

The export then flattens the tree into one row per unit-type (normalizeEOI, TaskeenExportBuilderServiceReport.php:25-43), with a dynamic number of Preference N columns (padded to the max across all records, :106-117) and comma-joined attachment URLs for passports/IDs, cheque and InstaPay images (:99-101, :119-150). The final TaskeenExportReport exporter is a generic array→spreadsheet writer whose column headings are just the keys of the first row (app/Exports/TaskeenExportReport.php:22-30).

  • Dead filter keys on export. applyFilters also references launch_ids, sales_man, brokers and payment_status_code/statuses (EOIExportFiltersService.php:29-35), but FiltersRequest never accepts those keys — the method is shared with the EOI export. For Taskeen those filters are always empty, so only launches, unit_types and sales_managers actually narrow the file.
  • 'Unknown' launches and blank prices. Because enrichment is a local lookup, a Taskeen row for a launch or unit-type that the CRM knows about but the portal hasn’t mirrored yet shows launch_id 'Unknown' and a blank/null price/name — even though the CRM returned it (:88,:93-101).
  • payment_date sort silently no-ops off Egypt. The sort key is payment_date_timestamp, populated only by the Egypt-only date map (:65-85); for any non-Egypt country every row’s timestamp is null, so the sort has nothing to order by.
  • Salesman scoping depends on a CRM-id mapping. salesman_source_id comes from SalesManUtil::getSourceIdByCountryIdAndUserId; if that mapping is missing it is passed as null and the scope is entirely at the CRM’s mercy (GetTaskeenList.php:21-24).
  • The Broker branch is dead for Taskeen. GetTaskeenList has an else path that hardcodes a salesman source id '282DD55C-5BD0-E711-A94A-000D3A2CD91F' for a Broker (:25-27), but no broker route reaches taskeen — only routes/api/sales-man.php exposes it (routes/api/broker.php has none). The broker app reuses the TaskeenExportReport class for its own EOI export, which is a different flow.
  • No signature/idempotency on the read. It is a plain authenticated GET; there is nothing to replay incorrectly, but equally nothing is stored, so there is no audit trail of what a salesman saw or exported.