@rashidee/co2 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
# Specification Template — Flutter Mobile App with Riverpod
|
|
2
|
+
|
|
3
|
+
This is the authoritative template for the generated specification. The specification is
|
|
4
|
+
split into **two types of files**:
|
|
5
|
+
|
|
6
|
+
1. **`SPECIFICATION.md`** (root) — Table of Contents, shared infrastructure, and all
|
|
7
|
+
application-level sections. Generated once per application.
|
|
8
|
+
2. **`<module>/SPEC.md`** (per-module) — Self-contained module blueprint.
|
|
9
|
+
Generated once per module from PRD.md.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
spec/
|
|
13
|
+
├── SPECIFICATION.md
|
|
14
|
+
├── task/
|
|
15
|
+
│ └── SPEC.md
|
|
16
|
+
├── order/
|
|
17
|
+
│ └── SPEC.md
|
|
18
|
+
└── ...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Placeholders use `{{VARIABLE}}` syntax and must be replaced with actual values gathered
|
|
22
|
+
from context files.
|
|
23
|
+
|
|
24
|
+
Sections marked **[CONDITIONAL]** should only be included when the corresponding
|
|
25
|
+
integration is selected.
|
|
26
|
+
|
|
27
|
+
**CRITICAL: Sample code requirement.** Every widget/class in the spec MUST include a
|
|
28
|
+
complete, continuous Dart code sample in a fenced code block. The code must be
|
|
29
|
+
self-explanatory and directly usable as a reference for a coding agent. Do not describe
|
|
30
|
+
classes with bullet points alone — always accompany descriptions with full Dart code.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# Part A: Root SPECIFICATION.md
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Table of Contents
|
|
39
|
+
|
|
40
|
+
Generate a TOC with clickable Markdown anchor links to every H2 and H3 section.
|
|
41
|
+
Additionally, include a **Modules** section in the TOC that links to each module's
|
|
42
|
+
`SPEC.md` file:
|
|
43
|
+
|
|
44
|
+
```markdown
|
|
45
|
+
## Table of Contents
|
|
46
|
+
|
|
47
|
+
### Shared Infrastructure
|
|
48
|
+
- [1. Project Overview](#1-project-overview)
|
|
49
|
+
- [2. Package Configuration](#2-package-configuration)
|
|
50
|
+
- ...all shared sections...
|
|
51
|
+
|
|
52
|
+
### Modules
|
|
53
|
+
- [Task](task/SPEC.md)
|
|
54
|
+
- [Order](order/SPEC.md)
|
|
55
|
+
- [Catalogue](catalogue/SPEC.md)
|
|
56
|
+
- ...(one link per module)...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Only include conditional sections that apply based on determined selections.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Section 1: Project Overview
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
# {{APPLICATION_NAME}} — Technical Specification
|
|
67
|
+
|
|
68
|
+
## 1. Project Overview
|
|
69
|
+
|
|
70
|
+
**Application Name**: {{APPLICATION_NAME}}
|
|
71
|
+
**Project Slug**: {{PROJECT_SLUG}}
|
|
72
|
+
**App Display Name**: {{APP_TITLE}}
|
|
73
|
+
**Application ID**: {{APPLICATION_ID}}
|
|
74
|
+
**Framework**: Flutter Mobile (Android + iOS)
|
|
75
|
+
**Description**: {{APP_DESCRIPTION}}
|
|
76
|
+
**Versions Covered**: v1.0.0 — v{{LATEST_VERSION}}
|
|
77
|
+
|
|
78
|
+
### Target Platforms
|
|
79
|
+
- **Android**: minSdk {{MIN_ANDROID_SDK}} (Android 6.0+), targetSdk 34
|
|
80
|
+
- **iOS**: deployment target {{MIN_IOS_VERSION}}+
|
|
81
|
+
|
|
82
|
+
### Optional Components (Auto-Determined)
|
|
83
|
+
**Backend API Base URL**: {{API_BASE_URL}}
|
|
84
|
+
**Authentication**: {{AUTH_TYPE}}
|
|
85
|
+
**WebSocket**: {{WEBSOCKET}}
|
|
86
|
+
**i18n**: {{I18N}}
|
|
87
|
+
**Analytics**: {{ANALYTICS}}
|
|
88
|
+
**Crashlytics**: {{CRASHLYTICS}}
|
|
89
|
+
**ImagePicker**: {{IMAGE_PICKER}}
|
|
90
|
+
**FilePicker**: {{FILE_PICKER}}
|
|
91
|
+
**Sharing**: {{SHARING}}
|
|
92
|
+
**Permissions**: {{PERMISSIONS}}
|
|
93
|
+
|
|
94
|
+
### Technology Stack
|
|
95
|
+
(Render the core stack version table from SKILL.md, plus selected optional integration versions)
|
|
96
|
+
|
|
97
|
+
### User Roles
|
|
98
|
+
(List each user role extracted from mockup role folders, with role constant and access scope.)
|
|
99
|
+
|
|
100
|
+
| Role | Description | Constant | Accessible Modules |
|
|
101
|
+
|------|-------------|----------|-------------------|
|
|
102
|
+
| Worker | Field worker | `WORKER` | Task, Job, Profile |
|
|
103
|
+
| Admin | Full system | `ADMIN` | All modules |
|
|
104
|
+
|
|
105
|
+
### Modules
|
|
106
|
+
(List each module from PRD.md, with type and link to its SPEC.md.)
|
|
107
|
+
|
|
108
|
+
| Module | Type | Stories | Versions | Spec |
|
|
109
|
+
|--------|------|---------|----------|------|
|
|
110
|
+
| Task | Business | 5 | 1.0.0, 1.0.4 | [SPEC](task/SPEC.md) |
|
|
111
|
+
| Profile | System | 3 | 1.0.0 | [SPEC](profile/SPEC.md) |
|
|
112
|
+
|
|
113
|
+
### Input Sources
|
|
114
|
+
- CLAUDE.md: {{path}}
|
|
115
|
+
- PRD.md: {{path}}
|
|
116
|
+
- Module Model: {{path to MODEL.md}}
|
|
117
|
+
- Mockup Screens: {{path to MOCKUP.html}}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Section 2: Package Configuration
|
|
123
|
+
|
|
124
|
+
### 2.1 pubspec.yaml
|
|
125
|
+
|
|
126
|
+
Generate a complete `pubspec.yaml` inside a code block. Include all core and
|
|
127
|
+
conditional dependencies based on the determined selections.
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
name: {{PROJECT_SLUG}}
|
|
131
|
+
description: {{APP_DESCRIPTION}}
|
|
132
|
+
publish_to: 'none'
|
|
133
|
+
version: {{APP_VERSION}}+1
|
|
134
|
+
|
|
135
|
+
environment:
|
|
136
|
+
sdk: '>=3.5.0 <4.0.0'
|
|
137
|
+
flutter: '>=3.24.0'
|
|
138
|
+
|
|
139
|
+
dependencies:
|
|
140
|
+
flutter:
|
|
141
|
+
sdk: flutter
|
|
142
|
+
|
|
143
|
+
# State management
|
|
144
|
+
flutter_riverpod: ^2.5.1
|
|
145
|
+
hooks_riverpod: ^2.5.1
|
|
146
|
+
flutter_hooks: ^0.20.5
|
|
147
|
+
riverpod_annotation: ^2.3.5
|
|
148
|
+
|
|
149
|
+
# Local storage
|
|
150
|
+
hive: ^2.2.3
|
|
151
|
+
hive_flutter: ^1.1.0
|
|
152
|
+
flutter_secure_storage: ^9.2.2
|
|
153
|
+
path_provider: ^2.1.4
|
|
154
|
+
|
|
155
|
+
# Networking
|
|
156
|
+
dio: ^5.7.0
|
|
157
|
+
dio_smart_retry: ^6.0.0
|
|
158
|
+
pretty_dio_logger: ^1.4.0
|
|
159
|
+
|
|
160
|
+
# Navigation
|
|
161
|
+
go_router: ^14.2.7
|
|
162
|
+
|
|
163
|
+
# Code generation models
|
|
164
|
+
freezed_annotation: ^2.4.4
|
|
165
|
+
json_annotation: ^4.9.0
|
|
166
|
+
|
|
167
|
+
# Firebase + notifications
|
|
168
|
+
firebase_core: ^3.6.0
|
|
169
|
+
firebase_messaging: ^15.1.3
|
|
170
|
+
flutter_local_notifications: ^17.2.3
|
|
171
|
+
|
|
172
|
+
# Media / assets
|
|
173
|
+
cached_network_image: ^3.4.1
|
|
174
|
+
flutter_svg: ^2.0.10+1
|
|
175
|
+
|
|
176
|
+
# UI extras
|
|
177
|
+
pull_to_refresh: ^2.0.0
|
|
178
|
+
font_awesome_flutter: ^10.7.0
|
|
179
|
+
material_design_icons_flutter: ^7.0.7296
|
|
180
|
+
|
|
181
|
+
# Utilities
|
|
182
|
+
intl: ^0.19.0
|
|
183
|
+
url_launcher: ^6.3.1
|
|
184
|
+
flutter_dotenv: ^5.2.1
|
|
185
|
+
package_info_plus: ^8.0.2
|
|
186
|
+
|
|
187
|
+
# [If Auth = Keycloak]
|
|
188
|
+
flutter_appauth: ^8.0.0+1
|
|
189
|
+
openid_client: ^0.4.9
|
|
190
|
+
|
|
191
|
+
# [If WebSocket = yes]
|
|
192
|
+
web_socket_channel: ^3.0.1
|
|
193
|
+
|
|
194
|
+
# [If Analytics = yes]
|
|
195
|
+
firebase_analytics: ^11.3.3
|
|
196
|
+
|
|
197
|
+
# [If Crashlytics = yes]
|
|
198
|
+
firebase_crashlytics: ^4.1.3
|
|
199
|
+
|
|
200
|
+
# [If ImagePicker = yes]
|
|
201
|
+
image_picker: ^1.1.2
|
|
202
|
+
permission_handler: ^11.3.1
|
|
203
|
+
|
|
204
|
+
# [If FilePicker = yes]
|
|
205
|
+
file_picker: ^8.1.2
|
|
206
|
+
|
|
207
|
+
# [If Sharing = yes]
|
|
208
|
+
share_plus: ^10.0.2
|
|
209
|
+
|
|
210
|
+
dev_dependencies:
|
|
211
|
+
flutter_test:
|
|
212
|
+
sdk: flutter
|
|
213
|
+
flutter_lints: ^4.0.0
|
|
214
|
+
build_runner: ^2.4.13
|
|
215
|
+
freezed: ^2.5.7
|
|
216
|
+
json_serializable: ^6.8.0
|
|
217
|
+
riverpod_generator: ^2.4.3
|
|
218
|
+
hive_generator: ^2.0.1
|
|
219
|
+
flutter_native_splash: ^2.4.1
|
|
220
|
+
mocktail: ^1.0.4
|
|
221
|
+
|
|
222
|
+
flutter:
|
|
223
|
+
uses-material-design: true
|
|
224
|
+
assets:
|
|
225
|
+
- .env.development
|
|
226
|
+
- .env.production
|
|
227
|
+
- assets/images/
|
|
228
|
+
- assets/icons/
|
|
229
|
+
|
|
230
|
+
fonts:
|
|
231
|
+
- family: {{PRIMARY_FONT_FAMILY}}
|
|
232
|
+
fonts:
|
|
233
|
+
- asset: assets/fonts/{{PRIMARY_FONT_FAMILY}}-Regular.ttf
|
|
234
|
+
- asset: assets/fonts/{{PRIMARY_FONT_FAMILY}}-Medium.ttf
|
|
235
|
+
weight: 500
|
|
236
|
+
- asset: assets/fonts/{{PRIMARY_FONT_FAMILY}}-Bold.ttf
|
|
237
|
+
weight: 700
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 2.2 analysis_options.yaml
|
|
241
|
+
|
|
242
|
+
```yaml
|
|
243
|
+
include: package:flutter_lints/flutter.yaml
|
|
244
|
+
|
|
245
|
+
analyzer:
|
|
246
|
+
exclude:
|
|
247
|
+
- "**/*.g.dart"
|
|
248
|
+
- "**/*.freezed.dart"
|
|
249
|
+
- "**/generated_plugin_registrant.dart"
|
|
250
|
+
- "lib/generated/**"
|
|
251
|
+
language:
|
|
252
|
+
strict-casts: true
|
|
253
|
+
strict-inference: true
|
|
254
|
+
strict-raw-types: true
|
|
255
|
+
|
|
256
|
+
linter:
|
|
257
|
+
rules:
|
|
258
|
+
- prefer_const_constructors
|
|
259
|
+
- prefer_const_literals_to_create_immutables
|
|
260
|
+
- prefer_final_locals
|
|
261
|
+
- prefer_final_in_for_each
|
|
262
|
+
- avoid_print
|
|
263
|
+
- use_super_parameters
|
|
264
|
+
- require_trailing_commas
|
|
265
|
+
- sort_pub_dependencies
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 2.3 build_runner Scripts
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# One-shot code generation
|
|
272
|
+
dart run build_runner build --delete-conflicting-outputs
|
|
273
|
+
|
|
274
|
+
# Watch mode (rebuilds on save)
|
|
275
|
+
dart run build_runner watch --delete-conflicting-outputs
|
|
276
|
+
|
|
277
|
+
# Generate native splash screens (after editing pubspec config)
|
|
278
|
+
dart run flutter_native_splash:create
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 2.4 Environment Files
|
|
282
|
+
|
|
283
|
+
```env
|
|
284
|
+
# .env.development
|
|
285
|
+
API_BASE_URL=http://10.0.2.2:{{BACKEND_PORT}}/api/v1
|
|
286
|
+
API_TIMEOUT_MS=30000
|
|
287
|
+
API_RETRY_ATTEMPTS=3
|
|
288
|
+
|
|
289
|
+
# [If Auth = Keycloak]
|
|
290
|
+
KEYCLOAK_ISSUER=http://10.0.2.2:8180/realms/{{KEYCLOAK_REALM}}
|
|
291
|
+
KEYCLOAK_CLIENT_ID={{KEYCLOAK_CLIENT_ID}}
|
|
292
|
+
KEYCLOAK_REDIRECT_URI={{APPLICATION_ID}}:/oauth2redirect
|
|
293
|
+
KEYCLOAK_POST_LOGOUT_REDIRECT_URI={{APPLICATION_ID}}:/oauth2redirect
|
|
294
|
+
|
|
295
|
+
# [If WebSocket = yes]
|
|
296
|
+
WS_URL=ws://10.0.2.2:{{BACKEND_PORT}}/ws
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```env
|
|
300
|
+
# .env.production
|
|
301
|
+
API_BASE_URL=https://api.example.com/api/v1
|
|
302
|
+
API_TIMEOUT_MS=30000
|
|
303
|
+
API_RETRY_ATTEMPTS=3
|
|
304
|
+
|
|
305
|
+
# [If Auth = Keycloak]
|
|
306
|
+
KEYCLOAK_ISSUER=https://sso.example.com/realms/{{KEYCLOAK_REALM}}
|
|
307
|
+
KEYCLOAK_CLIENT_ID={{KEYCLOAK_CLIENT_ID}}
|
|
308
|
+
KEYCLOAK_REDIRECT_URI={{APPLICATION_ID}}:/oauth2redirect
|
|
309
|
+
KEYCLOAK_POST_LOGOUT_REDIRECT_URI={{APPLICATION_ID}}:/oauth2redirect
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Section 3: Application Configuration
|
|
315
|
+
|
|
316
|
+
### 3.1 .gitignore (Flutter)
|
|
317
|
+
|
|
318
|
+
```gitignore
|
|
319
|
+
# Miscellaneous
|
|
320
|
+
*.class
|
|
321
|
+
*.log
|
|
322
|
+
*.pyc
|
|
323
|
+
*.swp
|
|
324
|
+
.DS_Store
|
|
325
|
+
.atom/
|
|
326
|
+
.buildlog/
|
|
327
|
+
.history
|
|
328
|
+
.svn/
|
|
329
|
+
migrate_working_dir/
|
|
330
|
+
|
|
331
|
+
# IntelliJ / VSCode
|
|
332
|
+
.idea/
|
|
333
|
+
.vscode/
|
|
334
|
+
*.iml
|
|
335
|
+
*.ipr
|
|
336
|
+
*.iws
|
|
337
|
+
|
|
338
|
+
# Flutter / Dart / Pub-related
|
|
339
|
+
**/doc/api/
|
|
340
|
+
**/ios/Flutter/.last_build_id
|
|
341
|
+
.dart_tool/
|
|
342
|
+
.flutter-plugins
|
|
343
|
+
.flutter-plugins-dependencies
|
|
344
|
+
.packages
|
|
345
|
+
.pub-cache/
|
|
346
|
+
.pub/
|
|
347
|
+
/build/
|
|
348
|
+
/coverage/
|
|
349
|
+
pubspec.lock
|
|
350
|
+
|
|
351
|
+
# Symbolication
|
|
352
|
+
app.*.symbols
|
|
353
|
+
app.*.map.json
|
|
354
|
+
|
|
355
|
+
# Obfuscation
|
|
356
|
+
app.*.symbols
|
|
357
|
+
|
|
358
|
+
# Android
|
|
359
|
+
**/android/**/gradle-wrapper.jar
|
|
360
|
+
**/android/.gradle
|
|
361
|
+
**/android/captures/
|
|
362
|
+
**/android/gradlew
|
|
363
|
+
**/android/gradlew.bat
|
|
364
|
+
**/android/local.properties
|
|
365
|
+
**/android/**/GeneratedPluginRegistrant.java
|
|
366
|
+
**/android/key.properties
|
|
367
|
+
**/android/app/release/
|
|
368
|
+
**/android/app/google-services.json
|
|
369
|
+
|
|
370
|
+
# iOS
|
|
371
|
+
**/ios/**/*.mode1v3
|
|
372
|
+
**/ios/**/*.mode2v3
|
|
373
|
+
**/ios/**/*.moved-aside
|
|
374
|
+
**/ios/**/*.pbxuser
|
|
375
|
+
**/ios/**/*.perspectivev3
|
|
376
|
+
**/ios/**/Pods/
|
|
377
|
+
**/ios/**/.symlinks/
|
|
378
|
+
**/ios/**/profile
|
|
379
|
+
**/ios/**/xcuserdata
|
|
380
|
+
**/ios/.generated/
|
|
381
|
+
**/ios/Flutter/App.framework
|
|
382
|
+
**/ios/Flutter/Flutter.framework
|
|
383
|
+
**/ios/Flutter/Flutter.podspec
|
|
384
|
+
**/ios/Flutter/Generated.xcconfig
|
|
385
|
+
**/ios/Flutter/ephemeral
|
|
386
|
+
**/ios/Flutter/app.flx
|
|
387
|
+
**/ios/Flutter/app.zip
|
|
388
|
+
**/ios/Flutter/flutter_assets/
|
|
389
|
+
**/ios/Flutter/flutter_export_environment.sh
|
|
390
|
+
**/ios/ServiceDefinitions.json
|
|
391
|
+
**/ios/Runner/GeneratedPluginRegistrant.*
|
|
392
|
+
**/ios/Runner/GoogleService-Info.plist
|
|
393
|
+
|
|
394
|
+
# Environment (never commit secrets)
|
|
395
|
+
.env
|
|
396
|
+
.env.local
|
|
397
|
+
.env.development
|
|
398
|
+
.env.production
|
|
399
|
+
.env.*.local
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### 3.2 main.dart
|
|
403
|
+
|
|
404
|
+
```dart
|
|
405
|
+
import 'package:firebase_core/firebase_core.dart';
|
|
406
|
+
import 'package:flutter/foundation.dart';
|
|
407
|
+
import 'package:flutter/material.dart';
|
|
408
|
+
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
409
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
410
|
+
import 'package:hive_flutter/hive_flutter.dart';
|
|
411
|
+
|
|
412
|
+
import 'firebase_options.dart';
|
|
413
|
+
import 'app.dart';
|
|
414
|
+
import 'core/local_storage/hive_setup.dart';
|
|
415
|
+
import 'core/notification/notification_service.dart';
|
|
416
|
+
|
|
417
|
+
@pragma('vm:entry-point')
|
|
418
|
+
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
419
|
+
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
Future<void> main() async {
|
|
423
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
424
|
+
|
|
425
|
+
// Load environment-specific .env
|
|
426
|
+
final env = kReleaseMode ? '.env.production' : '.env.development';
|
|
427
|
+
await dotenv.load(fileName: env);
|
|
428
|
+
|
|
429
|
+
// Initialize Firebase
|
|
430
|
+
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
|
431
|
+
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
|
432
|
+
|
|
433
|
+
// Initialize Hive + register adapters
|
|
434
|
+
await HiveSetup.init();
|
|
435
|
+
|
|
436
|
+
// Initialize local notifications
|
|
437
|
+
await NotificationService.instance.init();
|
|
438
|
+
|
|
439
|
+
runApp(const ProviderScope(child: MyApp()));
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 3.3 Android Configuration Notes
|
|
444
|
+
|
|
445
|
+
```text
|
|
446
|
+
android/app/build.gradle:
|
|
447
|
+
- applicationId "{{APPLICATION_ID}}"
|
|
448
|
+
- minSdkVersion {{MIN_ANDROID_SDK}}
|
|
449
|
+
- targetSdkVersion 34
|
|
450
|
+
- compileSdkVersion 34
|
|
451
|
+
|
|
452
|
+
android/app/src/main/AndroidManifest.xml:
|
|
453
|
+
- <uses-permission android:name="android.permission.INTERNET" />
|
|
454
|
+
- <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Android 13+ -->
|
|
455
|
+
- [If ImagePicker = yes] <uses-permission android:name="android.permission.CAMERA" />
|
|
456
|
+
- Deep link intent filter for Keycloak callback:
|
|
457
|
+
<intent-filter android:autoVerify="true">
|
|
458
|
+
<action android:name="android.intent.action.VIEW"/>
|
|
459
|
+
<category android:name="android.intent.category.DEFAULT"/>
|
|
460
|
+
<category android:name="android.intent.category.BROWSABLE"/>
|
|
461
|
+
<data android:scheme="{{APPLICATION_ID}}"/>
|
|
462
|
+
</intent-filter>
|
|
463
|
+
- Default notification channel meta-data (FCM):
|
|
464
|
+
<meta-data
|
|
465
|
+
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
466
|
+
android:value="default" />
|
|
467
|
+
|
|
468
|
+
android/app/google-services.json:
|
|
469
|
+
- Add file (gitignored — pulled from Firebase project)
|
|
470
|
+
- Add `apply plugin: 'com.google.gms.google-services'` at the bottom of build.gradle
|
|
471
|
+
- Add classpath in android/build.gradle: 'com.google.gms:google-services:4.4.2'
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 3.4 iOS Configuration Notes
|
|
475
|
+
|
|
476
|
+
```text
|
|
477
|
+
ios/Runner/Info.plist:
|
|
478
|
+
- CFBundleURLTypes for Keycloak callback (URL scheme = {{APPLICATION_ID}})
|
|
479
|
+
- [If ImagePicker = yes] NSCameraUsageDescription, NSPhotoLibraryUsageDescription
|
|
480
|
+
- [If Permissions = yes] NSLocationWhenInUseUsageDescription (if location used)
|
|
481
|
+
- UIBackgroundModes: remote-notification (for FCM background delivery)
|
|
482
|
+
|
|
483
|
+
ios/Runner/AppDelegate.swift:
|
|
484
|
+
- Register for remote notifications and pass FCM token to Flutter
|
|
485
|
+
- Set `application.registerForRemoteNotifications()` and the
|
|
486
|
+
`UNUserNotificationCenter.current().delegate = self`
|
|
487
|
+
|
|
488
|
+
ios/Runner/GoogleService-Info.plist:
|
|
489
|
+
- Add file (gitignored — pulled from Firebase project)
|
|
490
|
+
- Drag into Runner target in Xcode
|
|
491
|
+
|
|
492
|
+
ios/Podfile:
|
|
493
|
+
- platform :ios, '{{MIN_IOS_VERSION}}'
|
|
494
|
+
- post_install hook adds GCC_PREPROCESSOR_DEFINITIONS for flutter_local_notifications
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### 3.5 flutter_native_splash Configuration (pubspec.yaml)
|
|
498
|
+
|
|
499
|
+
```yaml
|
|
500
|
+
flutter_native_splash:
|
|
501
|
+
color: "{{PAGE_BACKGROUND_HEX}}"
|
|
502
|
+
image: assets/images/splash.png
|
|
503
|
+
color_dark: "#121212"
|
|
504
|
+
image_dark: assets/images/splash_dark.png
|
|
505
|
+
android_12:
|
|
506
|
+
image: assets/images/splash_android12.png
|
|
507
|
+
icon_background_color: "{{PRIMARY_COLOR_HEX}}"
|
|
508
|
+
ios: true
|
|
509
|
+
android: true
|
|
510
|
+
web: false
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Run: `dart run flutter_native_splash:create`
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Section 4: Directory Structure
|
|
518
|
+
|
|
519
|
+
Generate the full `lib/` directory tree. Use actual module names from PRD.md and MODEL.md.
|
|
520
|
+
Follow the feature-based structure from `references/routing-patterns.md`.
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
lib/
|
|
524
|
+
├── main.dart
|
|
525
|
+
├── app.dart # MaterialApp.router + theme + locale
|
|
526
|
+
├── firebase_options.dart # Generated by `flutterfire configure`
|
|
527
|
+
├── router/
|
|
528
|
+
│ └── app_router.dart # go_router configuration
|
|
529
|
+
├── core/
|
|
530
|
+
│ ├── api/
|
|
531
|
+
│ │ ├── dio_provider.dart # Dio instance + interceptors
|
|
532
|
+
│ │ ├── api_failure.dart # Sealed-class error type
|
|
533
|
+
│ │ └── auth_interceptor.dart # [If Auth != none] Bearer token injector
|
|
534
|
+
│ ├── auth/
|
|
535
|
+
│ │ ├── auth_repository.dart # signIn/signOut/refresh
|
|
536
|
+
│ │ ├── auth_provider.dart # Riverpod auth state
|
|
537
|
+
│ │ ├── auth_state.dart # Sealed-class auth state (freezed)
|
|
538
|
+
│ │ └── roles.dart # Role constants
|
|
539
|
+
│ ├── local_storage/
|
|
540
|
+
│ │ ├── hive_setup.dart # Hive.initFlutter + adapter registration
|
|
541
|
+
│ │ ├── secure_storage_provider.dart # flutter_secure_storage wrapper
|
|
542
|
+
│ │ └── box_provider.dart # Typed Box<T> providers
|
|
543
|
+
│ ├── notification/
|
|
544
|
+
│ │ ├── notification_service.dart # FCM + local notifications init
|
|
545
|
+
│ │ ├── notification_handler.dart # Foreground / background / tap handlers
|
|
546
|
+
│ │ └── notification_navigation.dart # Routes deep-linked from notifications
|
|
547
|
+
│ ├── theme/
|
|
548
|
+
│ │ ├── app_theme.dart # lightTheme + darkTheme
|
|
549
|
+
│ │ ├── app_colors.dart # Color tokens from mockup
|
|
550
|
+
│ │ └── app_typography.dart # TextTheme tokens
|
|
551
|
+
│ ├── env/
|
|
552
|
+
│ │ └── env_config.dart # Typed accessor over dotenv
|
|
553
|
+
│ └── utils/
|
|
554
|
+
│ ├── validators.dart # Form field validators
|
|
555
|
+
│ ├── formatters.dart # intl-based formatters
|
|
556
|
+
│ └── url_helper.dart # url_launcher wrappers
|
|
557
|
+
├── shared/
|
|
558
|
+
│ ├── widgets/
|
|
559
|
+
│ │ ├── app_scaffold.dart
|
|
560
|
+
│ │ ├── loading_indicator.dart
|
|
561
|
+
│ │ ├── empty_state.dart
|
|
562
|
+
│ │ ├── error_state.dart
|
|
563
|
+
│ │ ├── confirm_dialog.dart
|
|
564
|
+
│ │ ├── status_chip.dart
|
|
565
|
+
│ │ ├── search_bar_field.dart
|
|
566
|
+
│ │ ├── pull_to_refresh_list.dart
|
|
567
|
+
│ │ ├── infinite_scroll_list.dart
|
|
568
|
+
│ │ ├── app_network_image.dart # Wraps cached_network_image
|
|
569
|
+
│ │ ├── cached_svg_icon.dart # Wraps flutter_svg
|
|
570
|
+
│ │ └── shell_screen.dart # AppShell with bottom navigation
|
|
571
|
+
│ ├── form/
|
|
572
|
+
│ │ ├── form_text_field.dart
|
|
573
|
+
│ │ ├── form_dropdown_field.dart
|
|
574
|
+
│ │ ├── form_date_picker_field.dart
|
|
575
|
+
│ │ ├── form_switch_field.dart
|
|
576
|
+
│ │ └── form_image_picker_field.dart # [If ImagePicker = yes]
|
|
577
|
+
│ └── navigation/
|
|
578
|
+
│ └── nav_config.dart # Bottom-nav / drawer items
|
|
579
|
+
├── features/
|
|
580
|
+
│ ├── auth/ # [If Auth != none]
|
|
581
|
+
│ │ ├── presentation/
|
|
582
|
+
│ │ │ ├── login_screen.dart # [If Auth = Local]
|
|
583
|
+
│ │ │ └── auth_callback_screen.dart # [If Auth = Keycloak]
|
|
584
|
+
│ │ └── auth.dart # Barrel
|
|
585
|
+
│ ├── dashboard/
|
|
586
|
+
│ │ ├── presentation/
|
|
587
|
+
│ │ │ └── dashboard_screen.dart
|
|
588
|
+
│ │ └── dashboard.dart
|
|
589
|
+
│ ├── {{module1}}/
|
|
590
|
+
│ │ ├── data/
|
|
591
|
+
│ │ │ ├── {{module1}}_repository.dart
|
|
592
|
+
│ │ │ └── {{module1}}_remote_data_source.dart
|
|
593
|
+
│ │ ├── domain/
|
|
594
|
+
│ │ │ └── {{module1}}.dart # @freezed model + Hive adapter
|
|
595
|
+
│ │ ├── application/
|
|
596
|
+
│ │ │ └── {{module1}}_providers.dart # Riverpod providers (generated)
|
|
597
|
+
│ │ ├── presentation/
|
|
598
|
+
│ │ │ ├── {{module1}}_list_screen.dart
|
|
599
|
+
│ │ │ ├── {{module1}}_detail_screen.dart
|
|
600
|
+
│ │ │ └── {{module1}}_form_screen.dart
|
|
601
|
+
│ │ └── {{module1}}.dart # Barrel
|
|
602
|
+
│ └── {{module2}}/
|
|
603
|
+
│ └── (same structure)
|
|
604
|
+
└── l10n/ # [If i18n = yes]
|
|
605
|
+
├── app_en.arb
|
|
606
|
+
├── app_ms.arb
|
|
607
|
+
└── intl_helper.dart
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Section 5: Theme Configuration
|
|
613
|
+
|
|
614
|
+
Generate the complete Material 3 `ThemeData` using design tokens extracted from MOCKUP.html.
|
|
615
|
+
Reference `references/component-patterns.md` for the theme setup pattern.
|
|
616
|
+
|
|
617
|
+
Include:
|
|
618
|
+
- `lib/core/theme/app_colors.dart` — color tokens with actual hex values from mockup
|
|
619
|
+
- `lib/core/theme/app_typography.dart` — TextTheme tokens from mockup
|
|
620
|
+
- `lib/core/theme/app_theme.dart` — `ThemeData lightTheme` + `ThemeData darkTheme` factory,
|
|
621
|
+
component themes (ElevatedButtonTheme, AppBarTheme, CardTheme, InputDecorationTheme)
|
|
622
|
+
- Design token mapping table showing mockup CSS property → ThemeData property
|
|
623
|
+
|
|
624
|
+
**app.dart integration:**
|
|
625
|
+
|
|
626
|
+
```dart
|
|
627
|
+
import 'package:flutter/material.dart';
|
|
628
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
629
|
+
|
|
630
|
+
import 'core/theme/app_theme.dart';
|
|
631
|
+
import 'router/app_router.dart';
|
|
632
|
+
import 'shared/providers/theme_mode_provider.dart';
|
|
633
|
+
|
|
634
|
+
class MyApp extends ConsumerWidget {
|
|
635
|
+
const MyApp({super.key});
|
|
636
|
+
|
|
637
|
+
@override
|
|
638
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
639
|
+
final themeMode = ref.watch(themeModeProvider);
|
|
640
|
+
final router = ref.watch(appRouterProvider);
|
|
641
|
+
|
|
642
|
+
return MaterialApp.router(
|
|
643
|
+
title: '{{APP_TITLE}}',
|
|
644
|
+
debugShowCheckedModeBanner: false,
|
|
645
|
+
theme: AppTheme.light(),
|
|
646
|
+
darkTheme: AppTheme.dark(),
|
|
647
|
+
themeMode: themeMode,
|
|
648
|
+
routerConfig: router,
|
|
649
|
+
// [If i18n = yes]
|
|
650
|
+
// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
651
|
+
// supportedLocales: AppLocalizations.supportedLocales,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Section 6: Authentication Configuration [CONDITIONAL — Include only if Auth != none]
|
|
660
|
+
|
|
661
|
+
**[If Auth = Keycloak]:**
|
|
662
|
+
Refer to `references/security-patterns.md` for the complete content.
|
|
663
|
+
Must cover:
|
|
664
|
+
- `lib/core/auth/auth_repository.dart` — wraps `FlutterAppAuth` for sign-in/refresh
|
|
665
|
+
- `lib/core/auth/auth_state.dart` — sealed-class auth state via freezed
|
|
666
|
+
- `lib/core/auth/auth_provider.dart` — `AuthNotifier extends AsyncNotifier<AuthState>`
|
|
667
|
+
- `lib/core/auth/roles.dart` — role constants
|
|
668
|
+
- `lib/core/api/auth_interceptor.dart` — Dio interceptor that injects Bearer token
|
|
669
|
+
- Token persistence in `flutter_secure_storage`
|
|
670
|
+
- AppAuth URL scheme registration (Android `AndroidManifest.xml` + iOS `Info.plist`)
|
|
671
|
+
- Auth callback handled natively by `flutter_appauth` (no Flutter route needed)
|
|
672
|
+
- Silent token refresh logic on app startup
|
|
673
|
+
|
|
674
|
+
**[If Auth = Local]:**
|
|
675
|
+
Refer to `references/security-patterns.md` (Local JWT section) for the complete content.
|
|
676
|
+
Must cover:
|
|
677
|
+
- `lib/features/auth/presentation/login_screen.dart` — email/password form
|
|
678
|
+
- `lib/core/auth/auth_repository.dart` — POST `/auth/login`, `/auth/refresh`
|
|
679
|
+
- `lib/core/auth/auth_provider.dart` — Riverpod auth state with login/logout actions
|
|
680
|
+
- `lib/core/api/auth_interceptor.dart` — reads token from `flutter_secure_storage`,
|
|
681
|
+
refreshes on 401
|
|
682
|
+
- go_router `redirect` guard that pushes `/login` when unauthenticated
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## Section 7: Router Configuration
|
|
687
|
+
|
|
688
|
+
Refer to `references/routing-patterns.md` for the complete route tree.
|
|
689
|
+
Must cover:
|
|
690
|
+
- `lib/router/app_router.dart` — complete `GoRouter(...)` with ALL routes from PRD.md
|
|
691
|
+
- `ShellRoute` for the bottom-navigation scaffold
|
|
692
|
+
- `redirect` callback for auth + role enforcement
|
|
693
|
+
- Deep link routes (e.g., `/notifications/:id`) for FCM payload navigation
|
|
694
|
+
- The exact module-based URL paths (e.g., `/task`, NOT `/worker/task`)
|
|
695
|
+
- 404 and unauthorized routes
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Section 8: API Client Configuration
|
|
700
|
+
|
|
701
|
+
```dart
|
|
702
|
+
// lib/core/api/dio_provider.dart
|
|
703
|
+
import 'package:dio/dio.dart';
|
|
704
|
+
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
|
705
|
+
import 'package:flutter/foundation.dart';
|
|
706
|
+
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
707
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
708
|
+
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
|
709
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
710
|
+
|
|
711
|
+
import 'auth_interceptor.dart';
|
|
712
|
+
|
|
713
|
+
part 'dio_provider.g.dart';
|
|
714
|
+
|
|
715
|
+
@Riverpod(keepAlive: true)
|
|
716
|
+
Dio dio(DioRef ref) {
|
|
717
|
+
final dio = Dio(
|
|
718
|
+
BaseOptions(
|
|
719
|
+
baseUrl: dotenv.env['API_BASE_URL']!,
|
|
720
|
+
connectTimeout: Duration(
|
|
721
|
+
milliseconds: int.parse(dotenv.env['API_TIMEOUT_MS'] ?? '30000'),
|
|
722
|
+
),
|
|
723
|
+
receiveTimeout: Duration(
|
|
724
|
+
milliseconds: int.parse(dotenv.env['API_TIMEOUT_MS'] ?? '30000'),
|
|
725
|
+
),
|
|
726
|
+
headers: {'Content-Type': 'application/json'},
|
|
727
|
+
),
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
// Auth interceptor — see Section 6
|
|
731
|
+
dio.interceptors.add(AuthInterceptor(ref));
|
|
732
|
+
|
|
733
|
+
// Smart retry — exponential backoff on 5xx and network errors,
|
|
734
|
+
// only for idempotent methods (GET, PUT, DELETE)
|
|
735
|
+
dio.interceptors.add(
|
|
736
|
+
RetryInterceptor(
|
|
737
|
+
dio: dio,
|
|
738
|
+
retries: int.parse(dotenv.env['API_RETRY_ATTEMPTS'] ?? '3'),
|
|
739
|
+
retryDelays: const [
|
|
740
|
+
Duration(seconds: 1),
|
|
741
|
+
Duration(seconds: 2),
|
|
742
|
+
Duration(seconds: 4),
|
|
743
|
+
],
|
|
744
|
+
retryableExtraStatuses: const {502, 503, 504},
|
|
745
|
+
),
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
// Verbose logging — dev only
|
|
749
|
+
if (kDebugMode) {
|
|
750
|
+
dio.interceptors.add(
|
|
751
|
+
PrettyDioLogger(
|
|
752
|
+
requestHeader: true,
|
|
753
|
+
requestBody: true,
|
|
754
|
+
responseBody: true,
|
|
755
|
+
responseHeader: false,
|
|
756
|
+
compact: false,
|
|
757
|
+
),
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return dio;
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
```dart
|
|
766
|
+
// lib/core/api/api_failure.dart
|
|
767
|
+
import 'package:dio/dio.dart';
|
|
768
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
769
|
+
|
|
770
|
+
part 'api_failure.freezed.dart';
|
|
771
|
+
|
|
772
|
+
@freezed
|
|
773
|
+
sealed class ApiFailure with _$ApiFailure {
|
|
774
|
+
const factory ApiFailure.network() = _Network;
|
|
775
|
+
const factory ApiFailure.unauthorized() = _Unauthorized;
|
|
776
|
+
const factory ApiFailure.forbidden() = _Forbidden;
|
|
777
|
+
const factory ApiFailure.notFound() = _NotFound;
|
|
778
|
+
const factory ApiFailure.server({int? status, String? message}) = _Server;
|
|
779
|
+
const factory ApiFailure.unknown(Object error) = _Unknown;
|
|
780
|
+
|
|
781
|
+
factory ApiFailure.fromDioException(DioException error) {
|
|
782
|
+
if (error.type == DioExceptionType.connectionTimeout ||
|
|
783
|
+
error.type == DioExceptionType.receiveTimeout ||
|
|
784
|
+
error.type == DioExceptionType.sendTimeout ||
|
|
785
|
+
error.type == DioExceptionType.connectionError) {
|
|
786
|
+
return const ApiFailure.network();
|
|
787
|
+
}
|
|
788
|
+
final status = error.response?.statusCode ?? 0;
|
|
789
|
+
if (status == 401) return const ApiFailure.unauthorized();
|
|
790
|
+
if (status == 403) return const ApiFailure.forbidden();
|
|
791
|
+
if (status == 404) return const ApiFailure.notFound();
|
|
792
|
+
if (status >= 500) {
|
|
793
|
+
final body = error.response?.data;
|
|
794
|
+
final message = body is Map<String, dynamic> ? body['message'] as String? : null;
|
|
795
|
+
return ApiFailure.server(status: status, message: message);
|
|
796
|
+
}
|
|
797
|
+
return ApiFailure.unknown(error);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## Section 9: Local Storage (Hive)
|
|
805
|
+
|
|
806
|
+
Refer to `references/storage-patterns.md`. Cover:
|
|
807
|
+
- `lib/core/local_storage/hive_setup.dart` — `Hive.initFlutter()`, register adapters,
|
|
808
|
+
open boxes (`settings`, `user_profile`, `<module>_cache`)
|
|
809
|
+
- `lib/core/local_storage/secure_storage_provider.dart` — `FlutterSecureStorage`
|
|
810
|
+
wrapper exposed as Riverpod provider
|
|
811
|
+
- `lib/core/local_storage/box_provider.dart` — typed `Box<T>` providers
|
|
812
|
+
- Encryption pattern using `HiveAesCipher` with key from secure storage
|
|
813
|
+
- Migration strategy on schema changes (clear-and-refetch vs. lazy migration)
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## Section 10: Riverpod Setup
|
|
818
|
+
|
|
819
|
+
Refer to `references/state-patterns.md`. Cover:
|
|
820
|
+
- `ProviderScope` wrapping `MyApp` in `main.dart`
|
|
821
|
+
- `riverpod_generator` configuration (`build.yaml` snippet)
|
|
822
|
+
- Provider naming conventions (`<Entity>RepositoryProvider`, `<Entity>ListProvider`,
|
|
823
|
+
`<Entity>ByIdProvider(id)`, `<Entity>FormControllerProvider`)
|
|
824
|
+
- `AsyncNotifier` / `AsyncNotifierProvider.family` patterns
|
|
825
|
+
- Side-effect mutation pattern (`Notifier` exposing `create()`, `update()`, `delete()`)
|
|
826
|
+
- Cache invalidation via `ref.invalidate(...)` and `ref.refresh(...)`
|
|
827
|
+
- Listening pattern in widgets via `ref.watch(...)` + `AsyncValue.when(...)`
|
|
828
|
+
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
## Section 11: Shared Layouts
|
|
832
|
+
|
|
833
|
+
For each shell, provide:
|
|
834
|
+
1. The constructor signature
|
|
835
|
+
2. The complete widget
|
|
836
|
+
|
|
837
|
+
- **AppShellScreen**: `Scaffold` with `body` from `ShellRoute` outlet and `BottomNavigationBar`
|
|
838
|
+
driven by `nav_config.dart` (role-filtered)
|
|
839
|
+
- **AuthShell**: Centered `Scaffold` for login / forgot-password / Keycloak callback
|
|
840
|
+
- **PublicShell**: Minimal scaffold for unauthenticated content (if any)
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Section 12: Shared Widgets
|
|
845
|
+
|
|
846
|
+
For each shared widget, provide the constructor signature and complete Dart code.
|
|
847
|
+
Reference `references/component-patterns.md` for implementations.
|
|
848
|
+
|
|
849
|
+
| Widget | Purpose |
|
|
850
|
+
|---|---|
|
|
851
|
+
| `AppScaffold` | Standard AppBar + body + optional FAB |
|
|
852
|
+
| `LoadingIndicator` | Full-screen CircularProgressIndicator |
|
|
853
|
+
| `EmptyState` | Empty list placeholder with icon and message |
|
|
854
|
+
| `ErrorState` | Full-screen error with retry button |
|
|
855
|
+
| `ConfirmDialog` | `showDialog`-based confirmation |
|
|
856
|
+
| `StatusChip` | Colored Chip for enum status values |
|
|
857
|
+
| `SearchBarField` | Debounced TextField for searches |
|
|
858
|
+
| `PullToRefreshList` | `SmartRefresher` + `ListView` wrapper |
|
|
859
|
+
| `InfiniteScrollList` | ScrollController-driven pagination wrapper |
|
|
860
|
+
| `AppNetworkImage` | `CachedNetworkImage` with placeholder + error fallback |
|
|
861
|
+
| `CachedSvgIcon` | `SvgPicture.asset` / `.network` with cache |
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
## Section 13: Navigation Configuration
|
|
866
|
+
|
|
867
|
+
Generate `lib/shared/navigation/nav_config.dart` with the actual navigation items
|
|
868
|
+
from the mockup nav files. For each role:
|
|
869
|
+
- List all nav items that role can see
|
|
870
|
+
- Include the icon (mix of `Icons.*`, `FontAwesomeIcons.*`, `MdiIcons.*` as appropriate)
|
|
871
|
+
- Bottom-nav routes use module-based paths only (e.g., `/task`, `/profile`)
|
|
872
|
+
|
|
873
|
+
```dart
|
|
874
|
+
// lib/shared/navigation/nav_config.dart
|
|
875
|
+
import 'package:flutter/material.dart';
|
|
876
|
+
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
877
|
+
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
|
878
|
+
|
|
879
|
+
import '../../core/auth/roles.dart';
|
|
880
|
+
|
|
881
|
+
class NavItem {
|
|
882
|
+
const NavItem({
|
|
883
|
+
required this.label,
|
|
884
|
+
required this.path,
|
|
885
|
+
required this.icon,
|
|
886
|
+
required this.activeIcon,
|
|
887
|
+
this.requiredRoles,
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
final String label;
|
|
891
|
+
final String path;
|
|
892
|
+
final IconData icon;
|
|
893
|
+
final IconData activeIcon;
|
|
894
|
+
/// If null, accessible to all authenticated users
|
|
895
|
+
final List<String>? requiredRoles;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const navigationItems = <NavItem>[
|
|
899
|
+
NavItem(
|
|
900
|
+
label: 'Dashboard',
|
|
901
|
+
path: '/dashboard',
|
|
902
|
+
icon: Icons.dashboard_outlined,
|
|
903
|
+
activeIcon: Icons.dashboard,
|
|
904
|
+
),
|
|
905
|
+
NavItem(
|
|
906
|
+
label: 'Tasks',
|
|
907
|
+
path: '/task',
|
|
908
|
+
icon: MdiIcons.formatListChecks,
|
|
909
|
+
activeIcon: MdiIcons.formatListChecks,
|
|
910
|
+
requiredRoles: [Roles.worker, Roles.admin],
|
|
911
|
+
),
|
|
912
|
+
NavItem(
|
|
913
|
+
label: 'Profile',
|
|
914
|
+
path: '/profile',
|
|
915
|
+
icon: FontAwesomeIcons.user,
|
|
916
|
+
activeIcon: FontAwesomeIcons.userLarge,
|
|
917
|
+
),
|
|
918
|
+
];
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
## Section 14: Form Infrastructure
|
|
924
|
+
|
|
925
|
+
Reference `references/component-patterns.md` for the complete reusable form-field widgets.
|
|
926
|
+
Describe:
|
|
927
|
+
- `Form` + `GlobalKey<FormState>` pattern
|
|
928
|
+
- All `Form*Field` widgets with full constructor signatures
|
|
929
|
+
- Reset pattern for edit modes (re-initialize controllers in `initState` / `didUpdateWidget`)
|
|
930
|
+
- Loading state on submit button (replace text with `CircularProgressIndicator`)
|
|
931
|
+
|
|
932
|
+
```dart
|
|
933
|
+
// lib/core/utils/validators.dart
|
|
934
|
+
class Validators {
|
|
935
|
+
Validators._();
|
|
936
|
+
|
|
937
|
+
static String? required(String? value, {String label = 'This field'}) {
|
|
938
|
+
if (value == null || value.trim().isEmpty) return '$label is required';
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
static String? minLength(String? value, int min, {String label = 'This field'}) {
|
|
943
|
+
if (value == null) return null;
|
|
944
|
+
if (value.length < min) return '$label must be at least $min characters';
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
static String? maxLength(String? value, int max, {String label = 'This field'}) {
|
|
949
|
+
if (value == null) return null;
|
|
950
|
+
if (value.length > max) return '$label must be at most $max characters';
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
static String? email(String? value) {
|
|
955
|
+
if (value == null || value.isEmpty) return null;
|
|
956
|
+
final pattern = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
|
|
957
|
+
if (!pattern.hasMatch(value)) return 'Enter a valid email address';
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
static String? url(String? value) {
|
|
962
|
+
if (value == null || value.isEmpty) return null;
|
|
963
|
+
final parsed = Uri.tryParse(value);
|
|
964
|
+
if (parsed == null || !(parsed.isScheme('http') || parsed.isScheme('https'))) {
|
|
965
|
+
return 'Enter a valid URL';
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
static FormFieldValidator<String> compose(List<FormFieldValidator<String>> validators) {
|
|
971
|
+
return (value) {
|
|
972
|
+
for (final v in validators) {
|
|
973
|
+
final result = v(value);
|
|
974
|
+
if (result != null) return result;
|
|
975
|
+
}
|
|
976
|
+
return null;
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
## Section 15: Error Handling Strategy
|
|
985
|
+
|
|
986
|
+
Describe:
|
|
987
|
+
- `FlutterError.onError` + `PlatformDispatcher.instance.onError` in `main.dart`
|
|
988
|
+
- Optional Crashlytics forwarding: `FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;`
|
|
989
|
+
- Dio response interceptor mapping `DioException` → `ApiFailure`
|
|
990
|
+
- Riverpod `AsyncValue.when(loading: ..., error: ..., data: ...)` pattern in screens
|
|
991
|
+
- SnackBar via `ScaffoldMessenger` for transient feedback
|
|
992
|
+
|
|
993
|
+
```dart
|
|
994
|
+
// lib/shared/widgets/error_state.dart
|
|
995
|
+
import 'package:flutter/material.dart';
|
|
996
|
+
|
|
997
|
+
import '../../core/api/api_failure.dart';
|
|
998
|
+
|
|
999
|
+
class ErrorState extends StatelessWidget {
|
|
1000
|
+
const ErrorState({super.key, required this.error, this.onRetry});
|
|
1001
|
+
|
|
1002
|
+
final Object error;
|
|
1003
|
+
final VoidCallback? onRetry;
|
|
1004
|
+
|
|
1005
|
+
String _messageFor(Object error) {
|
|
1006
|
+
if (error is ApiFailure) {
|
|
1007
|
+
return switch (error) {
|
|
1008
|
+
ApiFailure(:final _) when error is _Network => 'No internet connection. Please try again.',
|
|
1009
|
+
ApiFailure() when error is _Unauthorized => 'Your session has expired. Please sign in again.',
|
|
1010
|
+
ApiFailure() when error is _Forbidden => 'You do not have permission to perform this action.',
|
|
1011
|
+
ApiFailure() when error is _NotFound => 'The requested resource was not found.',
|
|
1012
|
+
ApiFailure() when error is _Server => 'A server error occurred. Please try again later.',
|
|
1013
|
+
_ => 'An unexpected error occurred.',
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
return error.toString();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
@override
|
|
1020
|
+
Widget build(BuildContext context) {
|
|
1021
|
+
return Center(
|
|
1022
|
+
child: Padding(
|
|
1023
|
+
padding: const EdgeInsets.all(24),
|
|
1024
|
+
child: Column(
|
|
1025
|
+
mainAxisSize: MainAxisSize.min,
|
|
1026
|
+
children: [
|
|
1027
|
+
Icon(
|
|
1028
|
+
Icons.error_outline,
|
|
1029
|
+
size: 56,
|
|
1030
|
+
color: Theme.of(context).colorScheme.error,
|
|
1031
|
+
),
|
|
1032
|
+
const SizedBox(height: 12),
|
|
1033
|
+
Text(
|
|
1034
|
+
_messageFor(error),
|
|
1035
|
+
style: Theme.of(context).textTheme.bodyLarge,
|
|
1036
|
+
textAlign: TextAlign.center,
|
|
1037
|
+
),
|
|
1038
|
+
if (onRetry != null) ...[
|
|
1039
|
+
const SizedBox(height: 16),
|
|
1040
|
+
FilledButton.icon(
|
|
1041
|
+
onPressed: onRetry,
|
|
1042
|
+
icon: const Icon(Icons.refresh),
|
|
1043
|
+
label: const Text('Retry'),
|
|
1044
|
+
),
|
|
1045
|
+
],
|
|
1046
|
+
],
|
|
1047
|
+
),
|
|
1048
|
+
),
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## Section 16: Notification System
|
|
1057
|
+
|
|
1058
|
+
Refer to `references/notification-patterns.md` for the full FCM + flutter_local_notifications
|
|
1059
|
+
architecture. Cover:
|
|
1060
|
+
- `NotificationService.init()` lifecycle
|
|
1061
|
+
- Permission request flow (iOS + Android 13+)
|
|
1062
|
+
- Foreground / background / terminated handlers
|
|
1063
|
+
- Local notification display for foreground FCM messages
|
|
1064
|
+
- Deep link from notification tap → `router.go('/...')`
|
|
1065
|
+
- FCM token registration with backend on first launch + refresh
|
|
1066
|
+
- Topic subscription (if PRD describes topic-based broadcasts)
|
|
1067
|
+
- Scheduled local notifications (`zonedSchedule`) for in-app reminders
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
1071
|
+
## Section 17: Theming & Dark Mode
|
|
1072
|
+
|
|
1073
|
+
```dart
|
|
1074
|
+
// lib/shared/providers/theme_mode_provider.dart
|
|
1075
|
+
import 'package:flutter/material.dart';
|
|
1076
|
+
import 'package:hive_flutter/hive_flutter.dart';
|
|
1077
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
1078
|
+
|
|
1079
|
+
part 'theme_mode_provider.g.dart';
|
|
1080
|
+
|
|
1081
|
+
const _settingsBox = 'settings';
|
|
1082
|
+
const _themeModeKey = 'themeMode';
|
|
1083
|
+
|
|
1084
|
+
@Riverpod(keepAlive: true)
|
|
1085
|
+
class ThemeModeController extends _$ThemeModeController {
|
|
1086
|
+
late Box _box;
|
|
1087
|
+
|
|
1088
|
+
@override
|
|
1089
|
+
ThemeMode build() {
|
|
1090
|
+
_box = Hive.box(_settingsBox);
|
|
1091
|
+
final stored = _box.get(_themeModeKey) as String?;
|
|
1092
|
+
return switch (stored) {
|
|
1093
|
+
'dark' => ThemeMode.dark,
|
|
1094
|
+
'light' => ThemeMode.light,
|
|
1095
|
+
_ => ThemeMode.system,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
Future<void> toggle() async {
|
|
1100
|
+
final next = switch (state) {
|
|
1101
|
+
ThemeMode.light => ThemeMode.dark,
|
|
1102
|
+
ThemeMode.dark => ThemeMode.light,
|
|
1103
|
+
ThemeMode.system => ThemeMode.dark,
|
|
1104
|
+
};
|
|
1105
|
+
await _box.put(_themeModeKey, next.name);
|
|
1106
|
+
state = next;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
---
|
|
1112
|
+
|
|
1113
|
+
## Section 18: Testing Strategy
|
|
1114
|
+
|
|
1115
|
+
Overview only — detailed per-feature test patterns in each module's SPEC.md.
|
|
1116
|
+
|
|
1117
|
+
- **Unit tests**: Pure Dart logic (validators, formatters, repository mappers)
|
|
1118
|
+
- **Provider tests**: Use `ProviderContainer` with overrides (mock repositories)
|
|
1119
|
+
- **Widget tests**: `flutter_test` + `ProviderScope` overrides
|
|
1120
|
+
- **Integration tests**: `integration_test` package for end-to-end flows
|
|
1121
|
+
- **Mocking**: `mocktail` (preferred — no codegen) for repository / Dio mocks
|
|
1122
|
+
|
|
1123
|
+
```yaml
|
|
1124
|
+
# pubspec.yaml — add under dev_dependencies if not present
|
|
1125
|
+
integration_test:
|
|
1126
|
+
sdk: flutter
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
---
|
|
1130
|
+
|
|
1131
|
+
## Section 19: Build & Distribution
|
|
1132
|
+
|
|
1133
|
+
### Android Release Build
|
|
1134
|
+
|
|
1135
|
+
```bash
|
|
1136
|
+
# Generate keystore (one-time)
|
|
1137
|
+
keytool -genkey -v -keystore upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
|
|
1138
|
+
|
|
1139
|
+
# Configure ~/.gradle/gradle.properties:
|
|
1140
|
+
# RELEASE_STORE_FILE=/path/to/upload-keystore.jks
|
|
1141
|
+
# RELEASE_STORE_PASSWORD=...
|
|
1142
|
+
# RELEASE_KEY_ALIAS=upload
|
|
1143
|
+
# RELEASE_KEY_PASSWORD=...
|
|
1144
|
+
|
|
1145
|
+
flutter build appbundle --release # Google Play
|
|
1146
|
+
flutter build apk --release --split-per-abi # Sideload
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### iOS Release Build
|
|
1150
|
+
|
|
1151
|
+
```bash
|
|
1152
|
+
flutter build ipa --release
|
|
1153
|
+
# Or open ios/Runner.xcworkspace in Xcode and Archive
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
### Native Splash & Icons
|
|
1157
|
+
|
|
1158
|
+
```bash
|
|
1159
|
+
dart run flutter_native_splash:create
|
|
1160
|
+
# Optional launcher icons (add flutter_launcher_icons dev dependency)
|
|
1161
|
+
dart run flutter_launcher_icons
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
### ProGuard Rules (Android Release)
|
|
1165
|
+
|
|
1166
|
+
```text
|
|
1167
|
+
# android/app/proguard-rules.pro
|
|
1168
|
+
|
|
1169
|
+
# Firebase
|
|
1170
|
+
-keep class com.google.firebase.** { *; }
|
|
1171
|
+
-keep class com.google.android.gms.** { *; }
|
|
1172
|
+
|
|
1173
|
+
# AppAuth (Keycloak)
|
|
1174
|
+
-keep class net.openid.appauth.** { *; }
|
|
1175
|
+
|
|
1176
|
+
# flutter_local_notifications
|
|
1177
|
+
-keep class com.dexterous.** { *; }
|
|
1178
|
+
|
|
1179
|
+
# Keep Dio generic types
|
|
1180
|
+
-keepattributes Signature
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
---
|
|
1184
|
+
|
|
1185
|
+
## Section 20: Internationalisation [CONDITIONAL — Include only if i18n = yes]
|
|
1186
|
+
|
|
1187
|
+
```yaml
|
|
1188
|
+
# pubspec.yaml
|
|
1189
|
+
flutter:
|
|
1190
|
+
generate: true
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
```yaml
|
|
1194
|
+
# l10n.yaml (project root)
|
|
1195
|
+
arb-dir: lib/l10n
|
|
1196
|
+
template-arb-file: app_en.arb
|
|
1197
|
+
output-localization-file: app_localizations.dart
|
|
1198
|
+
synthetic-package: false
|
|
1199
|
+
output-dir: lib/generated
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
```json
|
|
1203
|
+
// lib/l10n/app_en.arb
|
|
1204
|
+
{
|
|
1205
|
+
"@@locale": "en",
|
|
1206
|
+
"appTitle": "{{APP_TITLE}}",
|
|
1207
|
+
"actionRetry": "Retry",
|
|
1208
|
+
"errorGeneric": "An unexpected error occurred."
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
Wire `MaterialApp.router` localization delegates (already shown in Section 5) and add
|
|
1213
|
+
a locale-switch provider persisted in Hive `settings` box.
|
|
1214
|
+
|
|
1215
|
+
---
|
|
1216
|
+
|
|
1217
|
+
## Section 21: WebSocket Integration [CONDITIONAL — Include only if WebSocket = yes]
|
|
1218
|
+
|
|
1219
|
+
```dart
|
|
1220
|
+
// lib/core/realtime/socket_provider.dart
|
|
1221
|
+
import 'dart:async';
|
|
1222
|
+
|
|
1223
|
+
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
1224
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
1225
|
+
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
1226
|
+
|
|
1227
|
+
part 'socket_provider.g.dart';
|
|
1228
|
+
|
|
1229
|
+
@Riverpod(keepAlive: true)
|
|
1230
|
+
Stream<dynamic> socketEvents(SocketEventsRef ref) async* {
|
|
1231
|
+
final url = dotenv.env['WS_URL']!;
|
|
1232
|
+
while (true) {
|
|
1233
|
+
final channel = WebSocketChannel.connect(Uri.parse(url));
|
|
1234
|
+
try {
|
|
1235
|
+
ref.onDispose(channel.sink.close);
|
|
1236
|
+
await for (final event in channel.stream) {
|
|
1237
|
+
yield event;
|
|
1238
|
+
}
|
|
1239
|
+
} catch (_) {
|
|
1240
|
+
// Backoff before reconnect
|
|
1241
|
+
await Future<void>.delayed(const Duration(seconds: 3));
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
## Section 22: Firebase Initialization
|
|
1250
|
+
|
|
1251
|
+
```bash
|
|
1252
|
+
# One-time CLI flow during scaffolding
|
|
1253
|
+
dart pub global activate flutterfire_cli
|
|
1254
|
+
flutterfire configure --project={{FIREBASE_PROJECT_ID}}
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
This produces `lib/firebase_options.dart` and downloads `google-services.json` /
|
|
1258
|
+
`GoogleService-Info.plist` automatically. Both native files must be gitignored.
|
|
1259
|
+
|
|
1260
|
+
```dart
|
|
1261
|
+
// main.dart excerpt (already shown above)
|
|
1262
|
+
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
|
1263
|
+
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
# Part B: Per-Module SPEC.md Template
|
|
1269
|
+
|
|
1270
|
+
Each module from PRD.md and MODEL.md gets its own `<module-name>/SPEC.md` file.
|
|
1271
|
+
|
|
1272
|
+
---
|
|
1273
|
+
|
|
1274
|
+
## Module SPEC.md Structure
|
|
1275
|
+
|
|
1276
|
+
```markdown
|
|
1277
|
+
# {{ModuleName}} — Module Specification
|
|
1278
|
+
|
|
1279
|
+
> Part of [{{APPLICATION_NAME}} Technical Specification](../SPECIFICATION.md)
|
|
1280
|
+
|
|
1281
|
+
## Overview
|
|
1282
|
+
|
|
1283
|
+
**Module:** {{ModuleName}}
|
|
1284
|
+
**Feature Path:** `lib/features/{{moduleName}}/`
|
|
1285
|
+
**Type:** {{System Module | Business Module}}
|
|
1286
|
+
**Backend Endpoint Base:** `/{{api-path}}` (inferred from model/API context)
|
|
1287
|
+
**Cache Box:** `{{moduleName}}_cache` (if module is cached offline)
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
---
|
|
1291
|
+
|
|
1292
|
+
### Traceability
|
|
1293
|
+
|
|
1294
|
+
```markdown
|
|
1295
|
+
## Traceability
|
|
1296
|
+
|
|
1297
|
+
### User Stories
|
|
1298
|
+
| ID | Version | Description |
|
|
1299
|
+
|---|---|---|
|
|
1300
|
+
| USA000030 | v1.0.0 | Create/update task |
|
|
1301
|
+
| USA000033 | v1.0.0 | View list of tasks |
|
|
1302
|
+
|
|
1303
|
+
### Non-Functional Requirements
|
|
1304
|
+
| ID | Version | Description |
|
|
1305
|
+
|---|---|---|
|
|
1306
|
+
| NFRA00021 | v1.0.0 | Task list paginated with 20 items per page |
|
|
1307
|
+
| NFRA00024 | v1.0.0 | Task title max 100 characters |
|
|
1308
|
+
|
|
1309
|
+
### Constraints
|
|
1310
|
+
| ID | Version | Description |
|
|
1311
|
+
|---|---|---|
|
|
1312
|
+
| CONSA0012 | v1.0.0 | Status values: DRAFT, IN_PROGRESS, COMPLETED |
|
|
1313
|
+
|
|
1314
|
+
### Removed / Replaced
|
|
1315
|
+
| ID | Removed In | Replaced By | Reason |
|
|
1316
|
+
|---|---|---|---|
|
|
1317
|
+
| CONSA0012 (READY status) | v1.0.4 | — | Auto-computed status; READY removed |
|
|
1318
|
+
_or "None." if no removals_
|
|
1319
|
+
|
|
1320
|
+
### Data Sources
|
|
1321
|
+
| Artifact | Reference |
|
|
1322
|
+
|---|---|
|
|
1323
|
+
| DB Table | `task` |
|
|
1324
|
+
| Mockup Screen | `worker/task/task-list.html` |
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
---
|
|
1328
|
+
|
|
1329
|
+
### Freezed Model
|
|
1330
|
+
|
|
1331
|
+
```markdown
|
|
1332
|
+
## Freezed Model
|
|
1333
|
+
|
|
1334
|
+
File: `lib/features/{{moduleName}}/domain/{{moduleName}}.dart`
|
|
1335
|
+
|
|
1336
|
+
(Complete @freezed data class — field-for-field from MODEL.md, with fromJson/toJson and
|
|
1337
|
+
optional Hive adapter annotations if module is cached offline)
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
All enum values must match PRD.md constraints exactly. Include the generic
|
|
1341
|
+
`PaginatedResponse<T>` type in `lib/core/api/` (shared), not per-module.
|
|
1342
|
+
|
|
1343
|
+
```dart
|
|
1344
|
+
// Example
|
|
1345
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
1346
|
+
import 'package:hive/hive.dart';
|
|
1347
|
+
|
|
1348
|
+
part 'task.freezed.dart';
|
|
1349
|
+
part 'task.g.dart';
|
|
1350
|
+
|
|
1351
|
+
enum TaskStatus {
|
|
1352
|
+
@JsonValue('DRAFT') draft,
|
|
1353
|
+
@JsonValue('IN_PROGRESS') inProgress,
|
|
1354
|
+
@JsonValue('COMPLETED') completed,
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
@freezed
|
|
1358
|
+
@HiveType(typeId: 1)
|
|
1359
|
+
class Task with _$Task {
|
|
1360
|
+
@HiveField(0) const factory Task({
|
|
1361
|
+
@HiveField(0) required String id,
|
|
1362
|
+
@HiveField(1) required String title,
|
|
1363
|
+
@HiveField(2) String? description,
|
|
1364
|
+
@HiveField(3) required TaskStatus status,
|
|
1365
|
+
@HiveField(4) required DateTime dueDate,
|
|
1366
|
+
@HiveField(5) required DateTime createdAt,
|
|
1367
|
+
@HiveField(6) required DateTime updatedAt,
|
|
1368
|
+
}) = _Task;
|
|
1369
|
+
|
|
1370
|
+
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
|
|
1371
|
+
}
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
---
|
|
1375
|
+
|
|
1376
|
+
### Validation
|
|
1377
|
+
|
|
1378
|
+
```markdown
|
|
1379
|
+
## Validation
|
|
1380
|
+
|
|
1381
|
+
File: `lib/features/{{moduleName}}/presentation/{{moduleName}}_form_validators.dart`
|
|
1382
|
+
|
|
1383
|
+
(Validator closures derived from PRD.md NFRs and Constraints)
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
Rules must be explicitly derived from tagged NFR IDs:
|
|
1387
|
+
- Character limits → `Validators.maxLength`
|
|
1388
|
+
- URL format → `Validators.url`
|
|
1389
|
+
- Required fields → `Validators.required`
|
|
1390
|
+
- Cross-field rules → composed in the `onSubmit` callback after `formKey.currentState!.validate()`
|
|
1391
|
+
|
|
1392
|
+
---
|
|
1393
|
+
|
|
1394
|
+
### Repository
|
|
1395
|
+
|
|
1396
|
+
```markdown
|
|
1397
|
+
## Repository
|
|
1398
|
+
|
|
1399
|
+
File: `lib/features/{{moduleName}}/data/{{moduleName}}_repository.dart`
|
|
1400
|
+
|
|
1401
|
+
(Complete repository class with all methods matching user stories)
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
- `Future<PaginatedResponse<{{Entity}}>> list({{ListParams}} params)` — list/search user stories
|
|
1405
|
+
- `Future<{{Entity}}> getById(String id)` — view detail user stories
|
|
1406
|
+
- `Future<{{Entity}}> create({{EntityCreate}} payload, {File? imageFile})` — create
|
|
1407
|
+
- `Future<{{Entity}}> update(String id, {{EntityUpdate}} payload, {File? imageFile})` — update
|
|
1408
|
+
- `Future<void> delete(String id)` — delete
|
|
1409
|
+
- `String imageUrl(String id)` — if the module has image fields
|
|
1410
|
+
|
|
1411
|
+
---
|
|
1412
|
+
|
|
1413
|
+
### Riverpod Providers
|
|
1414
|
+
|
|
1415
|
+
```markdown
|
|
1416
|
+
## Riverpod Providers
|
|
1417
|
+
|
|
1418
|
+
File: `lib/features/{{moduleName}}/application/{{moduleName}}_providers.dart`
|
|
1419
|
+
|
|
1420
|
+
(Complete generated providers using @riverpod annotation)
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
- `<entity>RepositoryProvider` — exposes the repository instance
|
|
1424
|
+
- `<entity>ListProvider(<params>)` — `AsyncNotifierProvider.family` for paginated list
|
|
1425
|
+
- `<entity>ByIdProvider(String id)` — `FutureProvider.family` for single item
|
|
1426
|
+
- `<entity>FormControllerProvider` — `Notifier` exposing `create`/`update`/`delete` with
|
|
1427
|
+
cache invalidation via `ref.invalidate(<entity>ListProvider)`
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
### Screens
|
|
1432
|
+
|
|
1433
|
+
```markdown
|
|
1434
|
+
## Screens
|
|
1435
|
+
|
|
1436
|
+
Files:
|
|
1437
|
+
- `lib/features/{{moduleName}}/presentation/{{moduleName}}_list_screen.dart`
|
|
1438
|
+
- `lib/features/{{moduleName}}/presentation/{{moduleName}}_detail_screen.dart`
|
|
1439
|
+
- `lib/features/{{moduleName}}/presentation/{{moduleName}}_form_screen.dart`
|
|
1440
|
+
|
|
1441
|
+
(Complete screen widget implementations matching the mockup screens)
|
|
1442
|
+
```
|
|
1443
|
+
|
|
1444
|
+
**CRITICAL — screen widget requirements:**
|
|
1445
|
+
1. Extend `ConsumerWidget` or `HookConsumerWidget`
|
|
1446
|
+
2. Use `ref.watch(<entity>ListProvider(params))` and unwrap with `AsyncValue.when(...)`
|
|
1447
|
+
3. Use `context.go(...)` / `context.push(...)` for navigation (go_router)
|
|
1448
|
+
4. List screen: wrap with `SmartRefresher` (pull_to_refresh) and call `ref.invalidate(...)`
|
|
1449
|
+
in `onRefresh`
|
|
1450
|
+
5. Form screen: distinguish create vs edit via the `:id` path parameter
|
|
1451
|
+
6. Use `ConfirmDialog.show(context, ...)` for destructive actions
|
|
1452
|
+
7. Use `ScaffoldMessenger` for transient success/error toasts
|
|
1453
|
+
|
|
1454
|
+
Form screen pattern for create/edit:
|
|
1455
|
+
```dart
|
|
1456
|
+
class TaskFormScreen extends HookConsumerWidget {
|
|
1457
|
+
const TaskFormScreen({super.key, this.taskId});
|
|
1458
|
+
final String? taskId;
|
|
1459
|
+
bool get isEditMode => taskId != null;
|
|
1460
|
+
|
|
1461
|
+
@override
|
|
1462
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
1463
|
+
// If isEditMode, watch taskByIdProvider(taskId!) and prefill form
|
|
1464
|
+
// Use formKey + TextEditingControllers for fields
|
|
1465
|
+
// ...
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
---
|
|
1471
|
+
|
|
1472
|
+
### Form Widget
|
|
1473
|
+
|
|
1474
|
+
```markdown
|
|
1475
|
+
## Form Widget
|
|
1476
|
+
|
|
1477
|
+
File: `lib/features/{{moduleName}}/presentation/widgets/{{moduleName}}_form.dart`
|
|
1478
|
+
|
|
1479
|
+
(Complete form widget using Form + GlobalKey<FormState> + reusable field widgets)
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
---
|
|
1483
|
+
|
|
1484
|
+
### Route Definitions
|
|
1485
|
+
|
|
1486
|
+
```markdown
|
|
1487
|
+
## Route Definitions
|
|
1488
|
+
|
|
1489
|
+
Add to `lib/router/app_router.dart`:
|
|
1490
|
+
|
|
1491
|
+
(Complete GoRoute entries for this module — with redirect guards matching the mockup
|
|
1492
|
+
role folder access control)
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
Example:
|
|
1496
|
+
```dart
|
|
1497
|
+
GoRoute(
|
|
1498
|
+
path: '/task',
|
|
1499
|
+
name: 'task-list',
|
|
1500
|
+
redirect: (context, state) => _requireRoles(ref, [Roles.worker, Roles.admin]),
|
|
1501
|
+
builder: (context, state) => const TaskListScreen(),
|
|
1502
|
+
routes: [
|
|
1503
|
+
GoRoute(
|
|
1504
|
+
path: 'create',
|
|
1505
|
+
name: 'task-create',
|
|
1506
|
+
builder: (context, state) => const TaskFormScreen(),
|
|
1507
|
+
),
|
|
1508
|
+
GoRoute(
|
|
1509
|
+
path: ':id',
|
|
1510
|
+
name: 'task-detail',
|
|
1511
|
+
builder: (context, state) => TaskDetailScreen(taskId: state.pathParameters['id']!),
|
|
1512
|
+
routes: [
|
|
1513
|
+
GoRoute(
|
|
1514
|
+
path: 'edit',
|
|
1515
|
+
name: 'task-edit',
|
|
1516
|
+
builder: (context, state) => TaskFormScreen(taskId: state.pathParameters['id']),
|
|
1517
|
+
),
|
|
1518
|
+
],
|
|
1519
|
+
),
|
|
1520
|
+
],
|
|
1521
|
+
),
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
---
|
|
1525
|
+
|
|
1526
|
+
### Navigation Items
|
|
1527
|
+
|
|
1528
|
+
```markdown
|
|
1529
|
+
## Navigation Items
|
|
1530
|
+
|
|
1531
|
+
Add to `lib/shared/navigation/nav_config.dart`:
|
|
1532
|
+
|
|
1533
|
+
(NavItem entries for this module, per role)
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
### Status Chip Map (if module has status enum)
|
|
1539
|
+
|
|
1540
|
+
```markdown
|
|
1541
|
+
## Status Configuration
|
|
1542
|
+
|
|
1543
|
+
```dart
|
|
1544
|
+
// Status display map for use with <StatusChip />
|
|
1545
|
+
const taskStatusDisplay = {
|
|
1546
|
+
TaskStatus.draft: (label: 'Draft', color: Colors.grey),
|
|
1547
|
+
TaskStatus.inProgress: (label: 'In Progress', color: Colors.blue),
|
|
1548
|
+
TaskStatus.completed: (label: 'Completed', color: Colors.green),
|
|
1549
|
+
};
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
---
|
|
1553
|
+
|
|
1554
|
+
### CRITICAL Rules for Module SPEC.md
|
|
1555
|
+
|
|
1556
|
+
1. **Use real field names** from MODEL.md — not `fieldOne`/`fieldTwo`
|
|
1557
|
+
2. **Use real table/collection names** from MODEL.md
|
|
1558
|
+
3. **Map every user story** to a specific repository method or provider
|
|
1559
|
+
4. **Map every NFR constraint** to a validator rule or widget behaviour
|
|
1560
|
+
5. **Use module-based URLs** — `/task`, NOT `/worker/task`
|
|
1561
|
+
6. **Role guards** come from mockup role folder structure → `redirect` callback on routes
|
|
1562
|
+
7. **Version-tag every ID** in traceability tables
|
|
1563
|
+
8. **Use null-safe Dart** — no `dynamic` except at the JSON boundary
|
|
1564
|
+
9. **All code samples must be complete** — no `// ...` gaps
|
|
1565
|
+
10. **Form screen handles both create and edit** via the `:id` path parameter unless
|
|
1566
|
+
the mockup has clearly separate screens
|