01 / PROJECT

SALESTRACK — PROPERTY LEAD DISPATCH

Time-boxed lead dispatch with client-server deadline enforcement.

SYSTEMFull Stack Engineer2026
SALESTRACK — PROPERTY LEAD DISPATCH - cover visual
PROJECT ARTIFACTPRIMARY VISUAL

Flutter client for property sales teams that pushes time-boxed leads to agents, enforces an accept-or-expire window, and tracks each lead from assignment to conversion. It replaces manual lead distribution with a deterministic, auditable status lifecycle.

02 / THE CHALLENGE

Orders were simple on paper.
Fulfillment was fragmented.

Property sales teams receive leads from many channels such as social ads, WhatsApp, and property portals, and must contact them within minutes before the prospect goes cold. Before a system like this, leads were handed out over chat and spreadsheets, so ownership was ambiguous and response time was unmeasurable. When multiple agents chase or drop the same lead, duplicate calls damage the prospect experience and distort conversion metrics. The hardest constraint is time: a lead not answered inside its accept window must be released and re-dispatched, and that deadline has to hold even when the agent's phone is asleep or the app is backgrounded. Any timer that lives only in the UI will drift, freeze, or disagree with the server, producing leads that are accepted after the backend already reassigned them.

KEY FAILURE MODES ADDRESSED
01

Leads were distributed manually over chat and spreadsheets, so ownership was ambiguous and two agents could contact the same prospect.

02

Acceptance deadlines lived in convention rather than code, so slow responses went undetected and could not be enforced or measured.

03

Without a server-committed accept step, client and backend could disagree on who owned a lead once it was reassigned after expiry.

03 / SYSTEM FLOW & DECISIONS

One order. Multiple fulfillment paths.

Explore how the system splits each line item into independent operational branches while enforcing strict transactional convergence before dispatch.

Lead Acceptance (Direct Processing)

Runs when a high-priority lead is dispatched and the agent accepts within the auto-reject window.

Happy Path
STEP 01Dispatch Service → FCM

Push Dispatch

Backend emits a data message tagged type=lead_assigned with priority=high; the client classifies it as an incoming lead.

STEP 02Notification Service

Full-Screen Alert

Raises a max-importance notification with fullScreenIntent, starts looping lead_alert audio, and opens IncomingLeadScreen with leadId and timeout.

STEP 03IncomingLeadProvider / Sales Agent

Countdown Reconciliation

Provider registers arrivalTime, derives remaining seconds from the wall clock, and the screen enriches fields from GET /mobile/leads/{id}.

STEP 04LeadsProvider → ApiService

Accept Persist

POST /mobile/leads/{id}/accept commits ownership; the provider removes the pending lead, stops the sound, and routes to /leads.

KEY ARCHITECTURAL DECISIONS & TRADE-OFFS
01

WALL-CLOCK COUNTDOWN INSTEAD OF A DECREMENTING TIMER

IncomingLeadProvider stores arrivalTime and derives remainingSeconds from DateTime.now() on read, while a single 1-second timer only schedules re-render and pruning.

RATIONALE

A decrementing counter drifts or freezes when the OS suspends the app or backgrounds the process. Deriving from the wall clock keeps the deadline consistent across foreground, background, and screen re-entry, and makes the timer a scheduler rather than a source of truth.

02

DUAL FCM CHANNELS WITH A CENTRALIZED CLASSIFIER

Two Android channels are declared at startup — a normal channel and a max-importance fullScreenIntent channel — and a single isIncomingLead() classifier decides routing for foreground, background, and terminated states.

RATIONALE

Android channel importance is immutable after creation, so urgent lead alerts require a dedicated channel that cannot be reused for routine notifications. Centralizing classification prevents three divergent interpretations of the same payload and reserves fullScreenIntent and custom sound strictly for high-priority leads.

03

TYPED MODEL LAYER WITH NULL-SAFE NESTED ADAPTERS

Every API response is parsed into typed Dart models (LeadModel, DashboardModel, UserProfile) with defensive fromJson casting, and the UI never indexes raw maps.

RATIONALE

The backend returns inconsistent shapes — wrapped data, nested source/property/assignments objects, nullable timestamps. Parsing at the boundary turns runtime cast failures into controlled defaults (empty lists, fallbacks) and decouples UI rebuilds from transport-format changes.

04

GLOBAL SESSION INTERCEPTOR IN THE HTTP LAYER

ApiService centrally injects the Bearer token and userId and converts any 401 on a non-auth endpoint into a full logout that clears all ChangeNotifier providers.

RATIONALE

Scattering auth handling across features leaves orphaned state and half-authenticated screens. A single interceptor guarantees that an expired token purges cached leads, dashboard, notifications, and pending alerts atomically, preventing stale data from leaking after session expiry.

04 / ARCHITECTURE & STACK

Engineered for relational integrity.

The app is organized into feature modules (auth, dashboard, leads, incoming_lead, notifications, profile) over a shared core of API clients, model adapters, and services. Every HTTP call flows through a single ApiService that attaches the Bearer token, auto-injects the userId for /mobile endpoints, enforces a 30-second timeout, and normalizes errors; a 401 on any non-auth endpoint triggers a global logout that clears all providers. Responses are parsed into typed models at the boundary, with defensive casting for nested and nullable fields such as source, property, assignments, and timestamps, so malformed data degrades to defaults instead of crashing a rebuild. Notifications run through a dual-channel pipeline: one classifier decides whether a payload is an incoming lead, and a dedicated max-importance channel with fullScreenIntent and a custom sound is reserved for those alerts. Session state persists behind one storage accessor and is purged atomically on logout or token expiry.

ARCHITECTURE TIERS
01|

CLIENT LAYER

  • Flutter (Dart SDK ^3.6.0)
  • Provider / ChangeNotifier with Selector and AutomaticKeepAliveClientMixin
  • Typed fromJson model adapters (no runtime map casting)
  • flutter_localizations (en/id) with custom TikTokSans theme
02|

BUSINESS & DOMAIN

  • Layered feature modules (auth, dashboard, leads, incoming_lead, notifications, profile)
  • IncomingLeadProvider deadline engine with status lifecycle (assigned → contacted → interested → converted/rejected)
  • Role/session model with global 401 invalidation and userId auto-injection
  • Debug-gated request/response logging with diagnostic hints
03|

DATA & STORAGE

  • REST API at sales-track.net/api via package:http with 30-second timeouts
  • Central ApiService with per-domain clients (leads, dashboard, profile, devices, notifications)
  • Firebase Cloud Messaging plus flutter_local_notifications dual-channel pipeline
  • shared_preferences session persistence with atomic clear on logout or 401
CORE PRODUCTION STACK
FlutterDartProviderFirebase Cloud Messagingflutter_local_notificationspackage:httpshared_preferences
OPERATIONAL OUTCOME & RESULT

The result is a lead pipeline whose deadlines and ownership are enforced the same way on the device and on the server, instead of being inferred from chat threads. Agents see a single, prioritized incoming alert with a countdown they cannot easily ignore, and every accepted lead carries an auditable sequence of assignment and status transitions. Managers get dashboard metrics — assigned, contacted, converted, rejected, and per-channel pipeline — computed from that consistent state. Because the deadline is recomputed rather than decremented and the session is invalidated atomically, the app stays predictable through backgrounding, poor connectivity, and token expiry. This turns lead handling from a manual, unmeasurable process into a deterministic one that can scale with volume.

✓Deadline-based incoming lead engine that derives remaining time from the wall clock, so countdowns survive app suspension and screen re-entry without drift.
✓Payload classifier with dedicated high-priority channel delivering fullScreenIntent, custom lead_alert sound, and looping audio only for urgent assignments.
✓Centralized 401 interceptor that atomically purges Auth, Leads, Dashboard, Notification, and IncomingLead providers before resetting navigation to login.
✓Typed boundary parsing of nested, nullable API payloads (source, property, assignments) with time-bound caches and optimistic lead updates.
NEXT WORK

More systems worth exploring.

Explore another project where software, business processes, and practical engineering meet.

BACK TO WORKS