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.
What Taskeen is, in data
Section titled “What Taskeen is, in data”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 products → unittypes → preferences
(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.
The whole trip, in one diagram
Section titled “The whole trip, in one diagram”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)The two endpoints
Section titled “The two endpoints”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.
-
GET taskeen→TaskeenController@index(app/Http/Controllers/Api/SalesMan/TaskeenController.php:25-43). Validated byListTaskeenRequest— whose only field issearch(nullable string,app/Http/Requests/Taskeen/ListTaskeenRequest.php:14-19). It callsGetTaskeenList->handle, maps the raw middlewareResponsethroughClientResponseMapper::mapFromMiddlewareToCms(a straightmessage/data/errorspass-through,app/Http/Controllers/Api/ClientResponseMapper.php:10-27), then enriches viaaddToTaskeenListand returnsrespondSuccess(['data' => …]). No filtering and no sorting happen here — onlysearchis forwarded to the CRM. -
GET taskeen/export→TaskeenController@export(:45-89). Validated by the richerFiltersRequest—search,unit_types[],launches[],sales_managers[],sort_by(units_countorpayment_date),sort_dir(ascordesc), andextension(app/Http/Requests/Taskeen/FiltersRequest.php:15-34). It re-runs the sameGetTaskeenList->handlefetch, and only if the middleware returned200: appliesapplyFiltersAndSortingToEOIs(EOIExportFiltersService.php:16-53), flattens rows throughTaskeenExportBuilderServiceReport::build(TaskeenExportBuilderServiceReport.php:11-23), and streams an Excel file namedTaskeen Report {datetime}—.csvwhenextension == 'csv', otherwise.xlsx(:71-85). On a non-200 it returns the mapped JSON error, not a file (:88). -
The data source —
GetTaskeenList->handle(app/Actions/Taskeen/GetTaskeenList.php:14-41). For aSalesManit resolves the caller’s CRM identity withSalesManUtil::getSourceIdByCountryIdAndUserId($country->id, $user->id)(:21-24), then issuesgetWithAuthentication(bearer-token cached clientget,app/Clients/AbstractClient.php:97-107) to{CRMS_MIDDLEWARE_API_URL}/taskeenwith headercountryand querysearch+salesman_source_id(:29-40). The middleware base comes fromconfig/services.php:34-37(envCRMS_MIDDLEWARE_API_URL). It returns the rawIlluminate\Http\Client\Responseuntouched.
Enrichment and the export shape
Section titled “Enrichment and the export shape”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).
Gotchas
Section titled “Gotchas”- Dead filter keys on export.
applyFiltersalso referenceslaunch_ids,sales_man,brokersandpayment_status_code/statuses(EOIExportFiltersService.php:29-35), butFiltersRequestnever accepts those keys — the method is shared with the EOI export. For Taskeen those filters are always empty, so onlylaunches,unit_typesandsales_managersactually 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 showslaunch_id'Unknown'and a blank/nullprice/name — even though the CRM returned it (:88,:93-101).payment_datesort silently no-ops off Egypt. The sort key ispayment_date_timestamp, populated only by the Egypt-only date map (:65-85); for any non-Egypt country every row’s timestamp isnull, so the sort has nothing to order by.- Salesman scoping depends on a CRM-id mapping.
salesman_source_idcomes fromSalesManUtil::getSourceIdByCountryIdAndUserId; if that mapping is missing it is passed asnulland the scope is entirely at the CRM’s mercy (GetTaskeenList.php:21-24). - The Broker branch is dead for Taskeen.
GetTaskeenListhas anelsepath that hardcodes a salesman source id'282DD55C-5BD0-E711-A94A-000D3A2CD91F'for aBroker(:25-27), but no broker route reachestaskeen— onlyroutes/api/sales-man.phpexposes it (routes/api/broker.phphas none). The broker app reuses theTaskeenExportReportclass 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.
