Skip to content

The Sales-Man app

The Sales-Man app (apps/orascom-sales-man-app) is the internal CRM portal for Orascom’s own sales staff. Its spine is a classic sales pipeline — leads → pipelines → customers → sales — with units inventory, EOI submission, Taskeen (handover), and analytics bolted on. It brands itself the “Internal Sales Portal” and, unlike the customer-facing shopper app, every screen is behind Microsoft SSO.

  • Entry: src/main.tsx nests HelmetProviderI18nextProviderQueryClientProvider (React Query) → ProSidebarProvider + ToastContainer. src/app/app.tsx wraps UserContextCountrySelectedContextCompareUnitsCurrency around the router (app.tsx:22-38).
  • Router: basename = process.env['NX_SALES_MAN_PATH'] ?? '/' (routes.tsx:297).
  • 35 routes / screens (1 auth + 34 main).
  • API: one ENDPOINTS map (src/api/endpoints.ts), ~112 keys, prefixed with NX__SALESMAN_ENDPOINT_PREFIX over base host NX__SALESMAN_API_ENDPOINT. A few are deliberately prefix-less (/user, cross-portal /shopper/* lookups, currency).
  • A top-bar “Currently Selling in” country + currency selector scopes the whole app to one country org at a time (CountrySelectedContext).

There is no password or OTP form. /login renders a single anchor to {baseUrl}/login/microsoft (where baseUrl = NX__SALESMAN_API_ENDPOINT with a trailing /api stripped, login.tsx:12,47-53). The backend runs the Microsoft OAuth dance and redirects back with ?token=; ProtectedRoutes / userDataLoader detect the param, call storeTokenDetails(token)localStorage[NX_SALESMAN_ACCESS_TOKEN_KEY], strip the param, and navigate to /.

Sales-Man app login — a single “Sign in with Microsoft” action; there is no password/OTP form.

The landing dashboard: recent updates (payment/EOI notifications), upcoming meetings, new launches, and saved-unit / new-property shortcuts. pages/overview/overview.tsx.

Sales-Man Overview dashboard: recent updates, upcoming meetings, new launches, and saved-units / new-properties shortcuts (client names blurred).

The left sidebar is the map to the rest of the app: Overview · Analytics · My Leads · Sales Deals · EOIs · My Customers · Destinations · Primary Units · My Sales · Saved Units.

This is the core workflow and the reason the app exists. It moves a prospect left-to-right across these screens, and almost every step is :countryId-scoped.

  1. Capture a lead. /new-lead (pages/new-deal) creates a lead via createLead (POST /leads), with a large set of lookup dropdowns (sources, sub-sources, ambassadors, brokers, events, exhibitions, walk-in offices, desired CRM units…).

  2. Work the pipeline. /deals (“My Deals”) lists leads; /leads/:dealId/pipelines/:countryId (pages/deal-details) is the pipeline board — discovery pipelines (createLeadDiscoveryPipeline), scheduled meetings (createMeeting/pipelines/{id}/meetings), feedback, and unit-type interest. A lead can be disqualified (/leads/:leadId/disqualify-lead/:countryId) or marked lost (/leads/:leadId/lost-lead/:countryId).

    Sales-Man “Sales Deals” — the leads pipeline list with lead name, mobile, quality, owner, status, channel, source and last-engagement columns (lead name + mobile blurred).

  3. Qualify into a customer. /qualify-customer/:countryId/:leadId (qualifyCustomerPOST /customers) promotes a qualified lead into a customer record.

  4. Manage customers. /customers (“My Customers”) lists them with contact details; /customers/:customerId/pipelines/:countryId is the customer pipeline. Nationalities / cities / countries / occupations lookups come from prefix-less cross-portal /shopper/sales-* endpoints.

    Sales-Man “My Customers” — full name, phone, and email columns per customer, each with a Customer Details action (name/phone/email blurred).

  5. Close a sale. /customers/:customerId/pipelines/:pipelineId/:countryId/create-sale (pages/create-sale) creates a sale (createSalePOST /units/{unit}/sales) — choosing the unit, payment plan, and reservation. /sales (“My Sales”) lists them by status (Pending / Reserved / Contracted), each with unit, customer, reservation date, and unit vs selling price.

    Sales-Man “My Sales” — unit name, customer name, status, reservation date, and unit / selling price per sale, with All / Pending / Reserved / Contracted tabs (customer name blurred).

Agents browse and share the sellable inventory across several screens, all driven by api/unit.ts.

  • Primary Units (/primary-units) — the new-units inventory grid with filters.

    Sales-Man “Primary Units” — the new-units inventory grid with per-unit cards and filters.

  • New Properties (/new-properties) — new launches / properties browsing.

    Sales-Man “New Properties” — new launches and properties browsing.

  • Unit details (/unit-details/:unitId) — payment terms, sales-offer generation (async: generateSalesOfferAsync → poll checkSalesOfferStatus), and the entry points to start a deal or submit a reservation.

    Sales-Man unit-details — a single unit with media, payment terms, and the sales-offer / start-deal actions.

  • Saved Units (/saved-units) — the agent’s favorited units (/user/saved-units).

    Sales-Man “Saved Units” — the agent’s favorited units grid.

Also here: Resale units (/resale-units), Compare (/compare-properties, /units/compare), and per-Destination pages (/destination/:destinationSlug) with locations, facilities, projects, FAQs, and downloadable marketing materials.

The app also drives EOI (Expression of Interest) submission against launches — create (/eoi/create/:launchSlug), the listing (/expression-of-interest), and single-EOI details (/expression-of-interest-details/:launchId/:eoiId) with attachments, a resend-payment-link action, and cancel. The full agent-side EOI flow is documented in Submitting an EOI. Taskeen (handover / occupancy) is surfaced as a tab inside the EOI listing page (pages/eoi-listing), with its own listing + CSV export.

The create-EOI form is what the agent fills on the client’s behalf — client info + ID type, the launch, unit preferences (min 3, max 5), and the payment method (payment link / bank transfer / instapay):

Sales-Man New EOI Submission form — client information, personal-ID type, launch, add-unit preferences, and payment method (blank form, no PII).

A sales & meetings analytics dashboard (/analytics, pages/analytics): revenue insights (sales value / total sales / sales-vs-target), deal-source and unit-category breakdowns, year-over-year revenue, an agents ranking leaderboard, and meetings insights (total meetings, meetings with no feedback).

Sales-Man Analytics — revenue insights, direct/indirect/referral and single/multi-family breakdowns, year-over-year revenue, agents leaderboard, and meetings insights (agent + lead names blurred).

This is distinct from the standalone Shopper-Analytics app: this one is an agent’s own sales/meetings view (/analytics/sales/*, /agents-ranking, meetings endpoints), scoped to the selected country.

/notifications (pages/updates) is the recent-updates feed — payment confirmations, “add payment details” prompts, and EOI updates — from getRecentUpdates (GET {prefix}/user/notifications).

Sales-Man notifications — a paginated feed of payment-completed and “add payment details” updates (client names blurred).

  • Microsoft SSO only. No password/OTP. The token comes back as a ?token= query param and is stored in localStorage[NX_SALESMAN_ACCESS_TOKEN_KEY]; every request sends Authorization: Bearer <token> (network.ts:140-149). No refresh token, no 401-retry interceptor — an expired token bounces to /login.
  • Country scoping is everywhere. The top-bar “Currently Selling in” selector (CountrySelectedContext) and the :countryId in lead/customer/pipeline routes mean the same agent works multiple country orgs; passing the wrong countryId targets the wrong Dynamics org.
  • Walkthrough routes are flag-gated. /unit-details/walkthrough and /leads/walkthrough/pipelines/1 redirect to home unless WALKTHROUGH_LOCALSTORAGE_KEY is set — trigger the guided tour before expecting them to render.
  • React Query is used here (unlike the Shopper-Analytics app), so list/detail data is cached and deduped.