Skip to main content

05 — Production Cycle Workflows

Domain owner: Field Agent (primary), Administrator (review/oversight) Mobile entry point: ProductionCycleMenuScreen via AppRoutes.productionCycleMenu

The Production Cycle domain tracks the full lifecycle of a crop from planting through harvest. Field agents record production data during farmer visits, including crop selection, planting details, growth inputs (fertilizers, pesticides, seeds), and harvest declarations. The entire domain follows Offline-first Pattern A — all reads come from local SQLite (instant, no network), all writes go to local SQLite with sync_status='pending', and a background sync layer pushes pending records to the server.

Domain Summary

graph LR
subgraph "Production Cycle Domain"
WF01["WF-PROD-01\nProduction Cycle Capture"]
WF02["WF-PROD-02\nGrowth Monitoring Visit"]
WF03["WF-PROD-03\nHarvest Recording"]
WF04["WF-PROD-04\nReview & History"]
WF05["WF-PROD-05\nProduction Outlook"]
end

AGENT((Field Agent)) --> WF01
AGENT --> WF02
AGENT --> WF03
ADMIN((Administrator)) --> WF04
ADMIN --> WF05
AGENT --> WF04

WF01 -->|sync| SYNC[[Sync Engine]]
WF02 -->|sync| SYNC
WF03 -->|sync| SYNC
SYNC -->|push/pull| API[Backend API]
API --> DB[(PostgreSQL)]
WF05 -->|GET /outlook| API

Key Files

LayerFilePurpose
Entry Pointmobile/.../presentation/production_cycle/production_cycle_menu_screen.dartMenu: "New Production Record" / "Production History"
Servicemobile/.../data/services/production_cycle_service.dartOffline-first singleton (Pattern A)
Draftmobile/.../data/services/production_cycle_draft_service.dartSharedPreferences draft persistence
Local DBmobile/.../data/local/production_cycle_local_service.dartSQLite CRUD for visits, cycles, inputs, harvests
Syncmobile/.../data/services/sync/production_cycle_sync.dartPUSH (pending->server) and PULL (server->local)
Modelsmobile/.../data/local/models/local_production_cycle.dartLocal model with sync management fields
Modelsmobile/.../data/local/models/local_production_input.dartInput model (Fertilizer/Pesticide/Seed/Other)
Modelsmobile/.../data/local/models/local_production_harvest.dartHarvest model with quantity, quality grade
DTOsmobile/.../data/models/production_cycle_models.dartAPI DTOs: ProductionCycleListItem, ProductionDetail, ProductionOutlook
Backendbackend/Almafrica.API/Controllers/ProductionsController.csREST API at api/productions

Lifecycle States

Planned --> Planted --> Growing --> Ready --> Harvested
\ /
+------> Cancelled <-------+

WF-PROD-01: Production Cycle Capture

PropertyValue
Workflow IDWF-PROD-01
TriggerAgent taps "New Production Record" on ProductionCycleMenuScreen
FrequencyOn-demand, during farmer visits
Offline SupportFull — visit created locally, cycle saved to SQLite, draft auto-saved to SharedPreferences, sync deferred to background

Workflow Diagram

graph TD
T((Agent taps\n"New Production Record")) --> NAV_VISIT[Navigate to\nStartVisitScreen]
NAV_VISIT --> LOAD_FARMERS[Load farmers from\nFarmerLocalService.instance]
LOAD_FARMERS --> PULL_CHECK{PullScopeResolver:\nfarmers fresh?}
PULL_CHECK -->|stale| PULL_FARMERS[Pull farmers\nfrom server]
PULL_CHECK -->|fresh| SHOW_LIST[Show farmer\nselection list]
PULL_FARMERS --> SHOW_LIST

SHOW_LIST --> SELECT_FARMER[Agent selects\na farmer]
SELECT_FARMER --> GEN_VISIT_ID["Generate local visit ID\nvisit_DateTime.now()\n.microsecondsSinceEpoch"]
GEN_VISIT_ID --> START_VISIT["ProductionCycleService\n.startVisit()\npushToServer: false"]
START_VISIT --> SAVE_VISIT_LOCAL[[Save LocalFarmerVisit\nto SQLite\nsync_status=pending]]
SAVE_VISIT_LOCAL --> SET_ACTIVE["ActiveVisitProvider\n.setActiveVisit()"]
SET_ACTIVE --> NAV_CAPTURE[Navigate to\nProductionCycleCaptureScreen]

NAV_CAPTURE --> RESTORE_DRAFT{Draft exists in\nSharedPreferences?}
RESTORE_DRAFT -->|yes| LOAD_DRAFT["ProductionCycleDraftService\n.loadDraft()"]
RESTORE_DRAFT -->|no| SHOW_FORM[Show empty\ncapture form]
LOAD_DRAFT --> SHOW_FORM

SHOW_FORM --> MASTER_DATA["DropdownLoadingMixin\nloads crop categories,\nunits, soil types"]
MASTER_DATA --> FILL_FORM["Agent fills form:\n- Crop category/crop\n- Area planted\n- Plot identifier\n- Soil/Terrain/Tenure\n- Planting date\n- Expected harvest dates\n- Estimated yield"]

FILL_FORM --> AUTO_DRAFT["Auto-save draft\n350ms debounce\nProductionCycleDraftService\n.saveDraft()"]
AUTO_DRAFT --> ADD_INPUTS{Add production\ninputs?}
ADD_INPUTS -->|yes| INPUT_FORM["Inline input form:\ntype, quantity, unit,\napplied date, notes"]
INPUT_FORM --> ADD_INPUTS
ADD_INPUTS -->|no| ADD_PHOTO{Add planting\nphoto?}

ADD_PHOTO -->|yes| PICK_PHOTO["ImagePicker:\ncamera or gallery"]
PICK_PHOTO --> ADD_PHOTO
ADD_PHOTO -->|no| SUBMIT_CHOICE{Save as Draft\nor Mark Planted?}

SUBMIT_CHOICE -->|draft| SAVE_DRAFT["LocalProductionCycle.create()\nisDraft: true\nstatus: Planned"]
SUBMIT_CHOICE -->|planted| SAVE_PLANTED["LocalProductionCycle.create()\nisDraft: false\nstatus: Planted"]

SAVE_DRAFT --> SAVE_LOCAL[[ProductionCycleLocalService\n.saveCycle()\nsync_status=pending]]
SAVE_PLANTED --> SAVE_LOCAL
SAVE_LOCAL --> SAVE_INPUTS_LOCAL[[Save LocalProductionInput\nrecords to SQLite]]
SAVE_INPUTS_LOCAL --> CLEAR_DRAFT["ProductionCycleDraftService\n.clearDraft()"]
CLEAR_DRAFT --> END_VISIT["ProductionCycleService\n.endVisit()"]
END_VISIT --> NAVIGATE_BACK[Navigate back\nto menu]

NAVIGATE_BACK -.->|background| BG_SYNC["ProductionCycleSyncService\n.pushPendingCycles()"]

Node Descriptions

NodeTypeImplementation
Navigate to StartVisitScreenActionNavigator.pushNamed(AppRoutes.productionCycleStartVisit) in production_cycle_menu_screen.dart
Load farmersDataFarmerLocalService.instance — reads from SQLite farmers table
PullScopeResolver checkDecisionPullScopeResolver determines if farmer cache is stale and triggers a pull if needed
Generate local visit IDAction'visit_${DateTime.now().microsecondsSinceEpoch}' in start_visit_screen.dart
Start visitActionProductionCycleService.instance.startVisit(farmerId, agentId, localVisitId, pushToServer: false)
Save visit to SQLiteDatabaseProductionCycleLocalService.saveVisit(LocalFarmerVisit) inserts into farmer_visits table
Set active visitActionActiveVisitProvider.setActiveVisit() — provider state for visit context banner
Restore draftDecisionProductionCycleDraftService.loadDraft() checks SharedPreferences key production_cycle_capture_draft_v1
DropdownLoadingMixinActionLoads crop categories, measurement units, soil types from master data providers
Auto-save draftAction_draftSaveDebounce — 350ms debounce Timer triggers ProductionCycleDraftService.saveDraft(formData)
Inline input formAction_LocalInput objects collected in _pendingInputs list — Seed, Fertilizer, Pesticide, or Other
Image pickerActionImagePicker from image_picker package — camera or gallery source
Create LocalProductionCycleActionLocalProductionCycle.create(...) factory — sets syncStatus: SyncStatus.pending
Save cycle to SQLiteDatabaseProductionCycleLocalService.saveCycle(cycle) inserts into production_cycles table
Save inputs to SQLiteDatabaseProductionCycleLocalService.saveInput(LocalProductionInput) for each collected input
Clear draftActionProductionCycleDraftService.clearDraft() removes SharedPreferences key
End visitActionProductionCycleService.endVisit() marks LocalFarmerVisit.ended_at and sets sync_status=pending
Background syncActionProductionCycleSyncService.pushPendingCycles() — runs via SyncManager

Data Transformations

StepInputOutputTransform
Form -> Local modelForm field valuesLocalProductionCycleLocalProductionCycle.create() maps form fields to model properties; generates UUID id, sets createdAt, syncStatus=pending
Input form -> Local inputInput form fieldsLocalProductionInputMaps inputType, type-specific IDs (fertilizerTypeId, pesticideTypeId, seedSourceId), quantity, unit, applied date
Local model -> API payloadLocalProductionCycleMap<String, dynamic>toCreatePayload() builds DTO for POST /api/productions; resolves farmerId to server GUID
Draft -> SharedPreferencesForm state mapJSON stringProductionCycleDraftService.saveDraft() serializes to JSON via SharedPreferences

Error Handling

ErrorHandlerRecovery
SQLite insert failureProductionCycleLocalService.saveCycle() returns nullSnackbar error shown; draft preserved in SharedPreferences for retry
Master data not loadedDropdownLoadingMixinShows loading indicator; retries fetch; falls back to cached master data
Photo picker failuretry/catch in capture screenGraceful degradation — cycle saved without photo; photo can be added later
Visit start failure (offline)pushToServer: false pathVisit saved locally with sync_status=pending; sync layer pushes later

Cross-References

  • WF-FARM-01 (Farmer Registration) — farmer selection list reads from same local farmer cache
  • WF-SYNC-03 (Production Cycle Sync) — ProductionCycleSyncService.pushPendingCycles() handles push of locally-saved cycles
  • WF-PROD-02 — growth monitoring adds inputs to existing cycles created here

WF-PROD-02: Growth Monitoring Visit

PropertyValue
Workflow IDWF-PROD-02
TriggerAgent opens an existing production cycle from history or detail screen and adds inputs
FrequencyOn-demand, multiple times per production cycle throughout growing season
Offline SupportFull — inputs saved to local SQLite, synced in background

Workflow Diagram

graph TD
T((Agent opens\nexisting cycle)) --> LOAD_DETAIL["ProductionCycleService\n.getProductionDetail(id)"]
LOAD_DETAIL --> READ_LOCAL[[Read from SQLite:\ncycle + inputs + harvests]]
READ_LOCAL --> SHOW_DETAIL[Show\nProductionCycleDetailScreen]

SHOW_DETAIL --> ADD_INPUT[Agent taps\n"Add Input"]
ADD_INPUT --> INPUT_SHEET["Show bottom sheet:\ninput type selector"]

INPUT_SHEET --> SELECT_TYPE{Input type?}
SELECT_TYPE -->|Seed| SEED_FORM["Seed form:\nseedSourceId, quantity,\nunit, applied date"]
SELECT_TYPE -->|Fertilizer| FERT_FORM["Fertilizer form:\nfertilizerTypeId, quantity,\nunit, applied date"]
SELECT_TYPE -->|Pesticide| PEST_FORM["Pesticide form:\npesticideTypeId, quantity,\nunit, applied date"]
SELECT_TYPE -->|Other| OTHER_FORM["Other form:\nname, quantity,\nunit, applied date"]

SEED_FORM --> VALIDATE[Validate required\nfields]
FERT_FORM --> VALIDATE
PEST_FORM --> VALIDATE
OTHER_FORM --> VALIDATE

VALIDATE --> SAVE_INPUT[[ProductionCycleLocalService\n.saveInput(LocalProductionInput)\nsync_status=pending]]
SAVE_INPUT --> REFRESH[Refresh detail\nscreen]
REFRESH --> UPDATE_STATUS{Update cycle\nstatus?}

UPDATE_STATUS -->|yes| STATUS_CHANGE["ProductionCycleLocalService\n.updateCycle()\nstatus: Growing"]
UPDATE_STATUS -->|no| DONE[Return to\ndetail view]
STATUS_CHANGE --> DONE

DONE -.->|background| BG_SYNC["ProductionCycleSyncService\n._pushPendingInputs()"]

Node Descriptions

NodeTypeImplementation
Load production detailDataProductionCycleService.getProductionDetail(id) reads from ProductionCycleLocalService
Read from SQLiteDatabase_local.getCycleByAnyId(id) + _local.getInputsByCycle(id) + _local.getHarvestsByCycle(id)
Show detail screenActionProductionCycleDetailScreen displays cycle info, inputs list, harvests list, audit trail
Add Input bottom sheetActionModal bottom sheet in production_cycle_detail_screen.dart with input type selection
Input type formsActionType-specific form fields: fertilizerTypeId, pesticideTypeId, seedSourceId, or free-text name for Other
Save input to SQLiteDatabaseProductionCycleLocalService.saveInput(LocalProductionInput) — sets sync_status=pending
Update cycle statusActionProductionCycleLocalService.updateCycle() — transitions status (e.g., Planted -> Growing)
Background syncActionProductionCycleSyncService._pushPendingInputs() iterates pending inputs and calls POST /api/productions/{id}/inputs

Data Transformations

StepInputOutputTransform
Bottom sheet form -> Local inputForm fieldsLocalProductionInputMaps type, type-specific ID, quantity, unitId, appliedDate; generates UUID id; sets syncStatus=pending
Local input -> API payloadLocalProductionInputMap<String, dynamic>toCreatePayload() builds DTO for POST /api/productions/{id}/inputs
Input displayNameType + type-specific fieldsStringGetter resolves name from inputType and specific type name fields

Error Handling

ErrorHandlerRecovery
Cycle not found locallygetProductionDetail() returns failureError screen shown; user navigates back
Input save failuresaveInput() returns nullSnackbar error; form state preserved for retry
Status update conflictupdateCycle() checks current statusOnly valid transitions allowed (e.g., cannot go from Harvested back to Planted)
Sync push failure for input_pushPendingInputs() catches per-inputmarkInputAsFailed() sets error; retry on next sync cycle

Cross-References

  • WF-PROD-01 — creates the production cycle that this workflow adds inputs to
  • WF-PROD-03 — harvest recording is the next lifecycle step after growth monitoring
  • WF-SYNC-03_pushPendingInputs() syncs inputs via POST /api/productions/{id}/inputs

WF-PROD-03: Harvest Recording

PropertyValue
Workflow IDWF-PROD-03
TriggerAgent taps "Declare Harvest" on production cycle detail screen
FrequencyTypically once per production cycle at harvest time
Offline SupportFull — harvest saved to local SQLite, status updated locally, synced in background

Workflow Diagram

graph TD
T((Agent taps\n"Declare Harvest")) --> SHOW_SHEET["Show harvest\nbottom sheet"]
SHOW_SHEET --> FILL_HARVEST["Agent fills:\n- Harvest date\n- Quantity\n- Unit\n- Quality grade\n- Notes"]
FILL_HARVEST --> VALIDATE{Form\nvalid?}
VALIDATE -->|no| SHOW_ERRORS[Show validation\nerrors]
SHOW_ERRORS --> FILL_HARVEST
VALIDATE -->|yes| CREATE_HARVEST["Create\nLocalProductionHarvest"]

CREATE_HARVEST --> SAVE_HARVEST[[ProductionCycleLocalService\n.saveHarvest()\nsync_status=pending]]
SAVE_HARVEST --> UPDATE_STATUS["ProductionCycleLocalService\n.updateCycle()\nstatus: Harvested"]
UPDATE_STATUS --> SAVE_STATUS[[Update cycle in SQLite\nsync_status=pending]]
SAVE_STATUS --> REFRESH[Refresh\ndetail screen]

REFRESH -.->|background| PUSH_HARVEST["ProductionCycleSyncService\n._pushPendingHarvests()"]
PUSH_HARVEST --> API_CALL["POST /api/productions\n/{id}/harvest"]
API_CALL --> MARK_SYNCED[[markHarvestAsSynced()]]

PUSH_HARVEST -.->|error| MARK_FAILED[[markHarvestAsFailed()\nincrement retryCount]]

Node Descriptions

NodeTypeImplementation
Show harvest bottom sheetActionModal bottom sheet in production_cycle_detail_screen.dart
Harvest formActionFields: harvest date picker, quantity (numeric), unit dropdown, quality grade (A/B/C/D), notes text field
Create LocalProductionHarvestActionConstructor with productionCycleId, serverProductionCycleId, harvestDate, quantity, unitId, qualityGrade, notes
Save harvest to SQLiteDatabaseProductionCycleLocalService.saveHarvest(LocalProductionHarvest) — inserts into production_harvests table
Update cycle statusDatabaseProductionCycleLocalService.updateCycle() — sets status='Harvested', sync_status='pending'
Push pending harvestsActionProductionCycleSyncService._pushPendingHarvests() — iterates pending harvests
API callActionProductionCycleService.apiDeclareHarvest(serverId, payload) -> POST /api/productions/{id}/harvest
Mark synced/failedDatabasemarkHarvestAsSynced() or markHarvestAsFailed() with error message and retry count

Data Transformations

StepInputOutputTransform
Bottom sheet form -> Local harvestForm fieldsLocalProductionHarvestMaps harvest date, quantity, unitId, qualityGrade, notes; links to productionCycleId (local) and serverProductionCycleId
Local harvest -> API payloadLocalProductionHarvestMap<String, dynamic>toCreatePayload() builds DTO for POST /api/productions/{id}/harvest
Status transitionCurrent cycle status'Harvested'Cycle status updated from Growing/Ready to Harvested; sync_status set to pending

Error Handling

ErrorHandlerRecovery
Harvest save failuresaveHarvest() returns nullSnackbar error; bottom sheet stays open for retry
Duplicate harvest declarationBackend validationAPI returns 409 Conflict; sync marks as failed with clear error
Server unreachablePush fails silentlyHarvest remains in local SQLite with sync_status=pending; retried on next sync
Invalid quantity/dateForm validationInline validation errors before save attempt

Cross-References

  • WF-PROD-02 — growth monitoring precedes harvest in the lifecycle
  • WF-PROD-01 — the production cycle being harvested was created in this workflow
  • WF-SYNC-03_pushPendingHarvests() syncs via POST /api/productions/{id}/harvest

WF-PROD-04: Production Cycle Review & History

PropertyValue
Workflow IDWF-PROD-04
TriggerAgent or admin opens "Production History" from menu; admin reviews cycles on detail screen
FrequencyOn-demand
Offline SupportFull — history reads from local SQLite; review actions saved locally and synced

Workflow Diagram

graph TD
T((Agent/Admin taps\n"Production History")) --> LOAD_LIST["ProductionCycleService\n.getProductionCyclesForCurrentUser()"]
LOAD_LIST --> SCOPE_CHECK{User scope?}
SCOPE_CHECK -->|admin| ALL_CYCLES[[Read all cycles\nfrom SQLite]]
SCOPE_CHECK -->|agent| AGENT_CYCLES[["Filter cycles by\nagent's farmers\nor created_by_id"]]

ALL_CYCLES --> SHOW_LIST[Show\nProductionCycleHistoryScreen]
AGENT_CYCLES --> SHOW_LIST

SHOW_LIST --> FILTER_STATUS["Status filter chips:\nAll, Planned, Planted,\nHarvested, Cancelled"]
SHOW_LIST --> SEARCH["Search by crop name\nor farmer name"]
SHOW_LIST --> SYNC_COUNTS["Display synced/pending\ncounts"]

FILTER_STATUS --> FILTERED_LIST[Filtered list\nof cycle cards]
SEARCH --> FILTERED_LIST
FILTERED_LIST --> TAP_CARD[Agent/Admin taps\na cycle card]

TAP_CARD --> LOAD_DETAIL["ProductionCycleService\n.getProductionDetail(id)"]
LOAD_DETAIL --> READ_DETAIL[[Read cycle +\ninputs + harvests\nfrom SQLite]]
READ_DETAIL --> SHOW_DETAIL[Show\nProductionCycleDetailScreen]

SHOW_DETAIL --> IS_ADMIN{User is\nadmin?}
IS_ADMIN -->|yes| REVIEW_ACTIONS["Admin review actions:\n- Lock/Unlock cycle\n- Add review notes\n- Approve/Flag"]
IS_ADMIN -->|no| AGENT_ACTIONS["Agent actions:\n- Edit cycle\n- Add input\n- Declare harvest\n- Cancel cycle"]

REVIEW_ACTIONS --> SAVE_REVIEW[[Update cycle in SQLite\nisLocked, needsReview\nsync_status=pending]]
AGENT_ACTIONS --> EDIT_CYCLE{Edit cycle?}
EDIT_CYCLE -->|yes| NAV_EDIT["Navigate to\nCaptureScreen\nwith editingDetail"]
EDIT_CYCLE -->|no| STATUS_ACTION{Status action?}
STATUS_ACTION -->|cancel| CANCEL["Update status:\nCancelled"]
STATUS_ACTION -->|other| OTHER_WF[See WF-PROD-02\nor WF-PROD-03]

CANCEL --> SAVE_STATUS[[Update cycle in SQLite\nstatus: Cancelled\nsync_status=pending]]

Node Descriptions

NodeTypeImplementation
Load cycles for current userDataProductionCycleService.getProductionCyclesForCurrentUser() — admin sees all, agent sees own farmers
User scope checkDecisionAgentContext.instance.getIdentity() determines admin vs agent; _filterCyclesForCurrentUser() applies scoping
Read all/filtered cyclesDatabaseProductionCycleLocalService.getAllCycles() or filtered by getCyclesByAgent(agentId)
Status filter chipsActionProductionCycleHistoryScreen — filters: All, Planned, Planted, Harvested, Cancelled
SearchActionClient-side filter on cropName and farmerName fields
Sync counts displayActionShows count of synced vs pending records from local database
Load detailDataProductionCycleService.getProductionDetail(id) -> getCycleByAnyId() + getInputsByCycle() + getHarvestsByCycle()
Admin review actionsActionLock/unlock toggle, review notes text field, approve/flag in production_cycle_detail_screen.dart
Edit cycle navigationActionNavigator.pushNamed(AppRoutes.productionCycleCapture, arguments: editingDetail)
Cancel cycleActionProductionCycleLocalService.updateCycle() with status: 'Cancelled', sync_status: 'pending'

Data Transformations

StepInputOutputTransform
Local cycles -> List itemsList<LocalProductionCycle>List<ProductionCycleListItem>cycle.toApiJson() then ProductionCycleListItem.fromJson() — normalizes local model to DTO
Detail assemblyCycle + inputs + harvestsProductionDetail_buildProductionDetail() in service combines cycle, inputs list, and harvests list into composite DTO
Review updateAdmin form fieldsUpdated LocalProductionCycleSets isLocked, needsReview flags; sync_status=pending
Status cancellationUser actionUpdated LocalProductionCycleSets status='Cancelled'; sync_status=pending

Error Handling

ErrorHandlerRecovery
No cycles in local DBgetAllCycles() returns emptyEmpty state UI with message and refresh option
Cycle not found by IDgetCycleByAnyId() returns nullError shown on detail screen; user navigates back
Edit conflict (locked cycle)isLocked flag checkEdit button hidden/disabled when cycle is locked by admin
Status transition invalidBusiness logic in detail screenOnly valid status transitions exposed as action buttons

Cross-References

  • WF-PROD-01 — cycles listed here were created in the capture workflow
  • WF-PROD-02 / WF-PROD-03 — detail screen provides entry points to add inputs and declare harvests
  • WF-SYNC-03 — status changes and review actions sync via pushPendingCycles()
  • WF-FARM-01 — farmer names displayed on cycle cards come from the local farmer cache

WF-PROD-05: Production Outlook (Analytics Dashboard)

PropertyValue
Workflow IDWF-PROD-05
TriggerAdmin/Manager navigates to Production Outlook screen
FrequencyOn-demand
Offline SupportPartial — API-first with SharedPreferences cache fallback; no local SQLite aggregation

Workflow Diagram

graph TD
T((Admin opens\nProduction Outlook)) --> CALL_API["ProductionCycleService\n.getProductionOutlook()"]
CALL_API --> API_REQUEST["GET /api/productions\n/outlook"]

API_REQUEST --> API_RESULT{API\nresponse?}
API_RESULT -->|200 OK| CACHE_RESPONSE["Cache response in\nSharedPreferences\nproduction_outlook_cache_v1"]
API_RESULT -->|error| LOAD_CACHE{Cached outlook\navailable?}

CACHE_RESPONSE --> PARSE["Parse\nProductionOutlook\n.fromJson()"]
LOAD_CACHE -->|yes| PARSE_CACHED["Parse cached\nProductionOutlook"]
LOAD_CACHE -->|no| SHOW_ERROR[Show error\nstate]

PARSE --> SHOW_DASHBOARD[Show\nProductionOutlookScreen]
PARSE_CACHED --> SHOW_DASHBOARD

SHOW_DASHBOARD --> KPI_GRID["KPI Grid:\n- Total cycles\n- Total area (ha)\n- Estimated yield\n- Harvested quantity"]
SHOW_DASHBOARD --> STATUS_CHART["Status Distribution\nPie Chart\n(fl_chart)"]
SHOW_DASHBOARD --> YIELD_CHART["Yield by Crop\nBar Chart\n(fl_chart)"]
SHOW_DASHBOARD --> GEO_SECTION["Geography Section:\n- By province\n- By territory"]

Node Descriptions

NodeTypeImplementation
Call outlook APIActionProductionCycleService.getProductionOutlook() — API-first, cache fallback
GET /api/productions/outlookDataApiClient.instance.get('/api/productions/outlook') — backend aggregates across all production cycles
Cache responseAction_cacheProductionOutlook(payload) — stores JSON in SharedPreferences key production_outlook_cache_v1
Load cache fallbackDecision_getCachedProductionOutlook() — reads from SharedPreferences when API unavailable
Parse ProductionOutlookActionProductionOutlook.fromJson(payload) — includes OutlookByCrop, OutlookByGeography, OutlookByStatus
KPI GridOutputFour metric cards: total cycles, total area, estimated yield, harvested quantity
Status Distribution PieOutputfl_chart PieChart widget showing Planned/Planted/Growing/Ready/Harvested/Cancelled distribution
Yield by Crop BarOutputfl_chart BarChart widget showing estimated vs actual yield per crop
Geography SectionOutputProvince and territory breakdown with cycle counts and area totals

Data Transformations

StepInputOutputTransform
API response -> DTOMap<String, dynamic>ProductionOutlookProductionOutlook.fromJson() parses top-level KPIs + nested OutlookByCrop, OutlookByGeography, OutlookByStatus lists
DTO -> ChartsProductionOutlookChart dataOutlookByStatus list maps to pie chart sections; OutlookByCrop maps to bar chart groups
Response -> CacheMap<String, dynamic>JSON stringSerialized to SharedPreferences for offline fallback

Error Handling

ErrorHandlerRecovery
API unreachabletry/catch in getProductionOutlook()Falls back to _getCachedProductionOutlook() from SharedPreferences
Cache miss + API failureBoth paths return nullError state shown with retry button
Invalid API responseresponse.data is! Map<String, dynamic> checkFalls back to cache; logs error
Chart rendering errorfl_chart error handlingGraceful degradation — individual charts may show error state without crashing screen

Cross-References

  • WF-PROD-01 through WF-PROD-03 — the data aggregated here originates from cycle capture, input recording, and harvest declaration
  • Backend ProductionsController.GetOutlook()GET /api/productions/outlook with Permission.ViewProductions authorization

Sync Architecture (Cross-Cutting)

The production cycle sync layer is documented in detail under WF-SYNC-03, but the key push/pull mechanics specific to this domain are summarized here.

Push Flow (ProductionCycleSyncService.pushPendingCycles())

graph TD
START((SyncManager\ntriggers push)) --> PUSH_VISITS["_pushPendingVisits()\nPush farmer visits first"]
PUSH_VISITS --> GET_PENDING["_local.getRetryableCyclesByPriority()\nOrdered by retry count ASC,\ncreated_at ASC"]

GET_PENDING --> LOOP{More pending\ncycles?}
LOOP -->|yes| RESOLVE_FARMER["_resolveServerFarmerId()\nMap local farmer ID\nto server GUID"]
RESOLVE_FARMER --> HAS_SERVER_ID{Cycle has\nserver ID?}

HAS_SERVER_ID -->|yes| UPDATE["_service.apiUpdateProductionCycle()\nPUT /api/productions/{id}"]
HAS_SERVER_ID -->|no| DEDUP{Duplicate\ndetection}
DEDUP -->|duplicate found| MERGE["Mark local as synced\nwith existing server ID"]
DEDUP -->|no duplicate| CREATE["_service.apiCreateProductionCycle()\nPOST /api/productions"]

UPDATE --> MARK_SYNCED[[markAsSynced()]]
MERGE --> MARK_SYNCED
CREATE --> MARK_SYNCED
MARK_SYNCED --> LOOP

UPDATE -.->|error| MARK_FAILED[[markAsFailed()\nincrementRetryCount()]]
CREATE -.->|error| MARK_FAILED
MARK_FAILED --> LOOP

LOOP -->|no| PUSH_INPUTS["_pushPendingInputs()"]
PUSH_INPUTS --> PUSH_HARVESTS["_pushPendingHarvests()"]
PUSH_HARVESTS --> PUSH_PHOTOS["_pushPendingPlantingPhotos()"]
PUSH_PHOTOS --> UPDATE_SYNC_TIME["UnifiedSyncStatusService\n.updateSyncTime()"]

Pull Flow (ProductionCycleSyncService.pullCyclesForFarmers())

graph TD
START((SyncManager\ntriggers pull)) --> FOR_EACH_FARMER["Iterate farmer\nserver IDs"]
FOR_EACH_FARMER --> API_FETCH["GET /api/productions\n?farmerId={id}"]
API_FETCH --> UPSERT[[ProductionCycleLocalService\n.upsertManyFromApi()\nConflict detection]]
UPSERT --> PULL_DETAIL["Pull detail for\neach new cycle"]
PULL_DETAIL --> PULL_INPUTS[[Upsert inputs\nfrom API]]
PULL_INPUTS --> PULL_HARVESTS[[Upsert harvests\nfrom API]]
PULL_HARVESTS --> FOR_EACH_FARMER

Status Normalization

The sync layer normalizes production cycle statuses using _syncableStatusAliases to handle multilingual and enum-style values:

AliasesNormalized Value
planned, planified, planifie, planifie, 0Planned
planted, plante, plante, 1Planted
growing, en croissance, 2Growing
ready, pret, pret, 3Ready
harvested, harvest, recolte, recolte, 4Harvested
cancelled, canceled, annule, annule, 5Cancelled

Backend API Surface

All endpoints are under api/productions in ProductionsController.cs.

MethodEndpointPermissionPurpose
GET/ViewProductionsPaginated list with filters (farmerId, cropId, status, province, territory, dateRange)
GET/exportViewProductionsExcel export of filtered production cycles
GET/farmer/{farmerId}ViewProductionsCycles for a specific farmer
GET/outlookViewProductionsAggregated analytics (KPIs, by-crop, by-geography, by-status)
GET/sync?lastSyncAt=ViewProductionsDelta sync — returns records modified since timestamp
GET/suggestions/deliveryViewProductionsDelivery suggestions for harvested cycles
GET/{id}ViewProductionsFull detail with inputs, harvests, audit trail
POST/CreateProductionsCreate new production cycle
POST/{id}/photoCreateProductionsUpload planting photo (chunked upload)
PUT/{id}EditProductionsUpdate production cycle
PATCH/{id}/statusEditProductionsUpdate status (Planned/Planted/Growing/Ready/Harvested/Cancelled)
DELETE/{id}DeleteProductionsSoft delete production cycle
POST/{id}/harvestCreateProductionsDeclare harvest with quantity, quality grade
GET/{id}/inputsViewProductionsList inputs for a cycle
POST/{id}/inputsCreateProductionsAdd production input
PUT/{id}/inputs/{inputId}EditProductionsUpdate production input
DELETE/{id}/inputs/{inputId}DeleteProductionsRemove production input

SQLite Tables

The local storage layer uses four tables managed by DatabaseHelper:

TableConstantKey Columns
farmer_visitsDatabaseHelper.tableFarmerVisitsid, farmer_id, agent_id, server_id, started_at, ended_at, status, sync_status, sync_error, retry_count
production_cyclesDatabaseHelper.tableProductionCyclesid, farmer_id, crop_id, visit_id, area_planted, planting_date, status, is_draft, is_locked, needs_review, sync_status, server_id, sync_error, retry_count
production_inputsDatabaseHelper.tableProductionInputsid, production_cycle_id, server_production_cycle_id, input_type, fertilizer_type_id, pesticide_type_id, seed_source_id, quantity, unit_id, applied_date, sync_status
production_harvestsDatabaseHelper.tableProductionHarvestsid, production_cycle_id, server_production_cycle_id, harvest_date, quantity, unit_id, quality_grade, notes, sync_status