diff --git a/.claude/agents/mobile-feature-builder.md b/.claude/agents/mobile-feature-builder.md index 02364f33..2923b110 100644 --- a/.claude/agents/mobile-feature-builder.md +++ b/.claude/agents/mobile-feature-builder.md @@ -54,9 +54,7 @@ If any of these files are missing or unreadable, notify the user before proceedi - Use `BlocProvider.value()` for singleton BLoCs - Use `UiColors`, `UiTypography`, `UiIcons`, `UiConstants` for all design values - Use `core_localization` for user-facing strings -- Write unit tests for use cases and repositories -- Mock dependencies with `mocktail` -- Test BLoCs with `bloc_test` +- Add human readable doc comments for `dartdoc` for all classes and methods. ## Standard Workflow @@ -109,7 +107,6 @@ Follow these steps in order for every feature implementation: ### 8. Self-Review - Run `melos analyze` and fix all issues -- Run `melos test` and ensure all pass - Manually verify no architectural violations exist - Check all barrel files are complete - Verify no hardcoded design values @@ -150,9 +147,7 @@ Before declaring work complete, verify: - [ ] BLoCs only depend on use cases - [ ] Use cases only depend on repository interfaces - [ ] All barrel files are complete and up to date -- [ ] Tests exist for use cases, repositories, and BLoCs - [ ] `melos analyze` passes -- [ ] `melos test` passes ## Escalation Criteria @@ -168,7 +163,6 @@ Stop and escalate to the human when you encounter: After completing implementation, prepare a handoff summary including: - Feature name and target app - List of all changed/created files -- Test coverage percentage - Any concerns, trade-offs, or technical debt introduced - Recommendation for Architecture Review Agent review @@ -179,9 +173,8 @@ As you work on features, update your agent memory with discoveries about: - Session store usage patterns and available stores - DataConnect query/mutation names and their locations - Design token values and component patterns actually in use -- Common test setup patterns and shared test utilities - Module registration patterns and route conventions -- Recurring issues found during `melos analyze` or `melos test` +- Recurring issues found during `melos analyze` - Codebase-specific naming conventions that differ from general Flutter conventions This builds institutional knowledge that improves your effectiveness across conversations. diff --git a/.claude/skills/krow-mobile-development-rules/SKILL.md b/.claude/skills/krow-mobile-development-rules/SKILL.md index a15331f5..4f4adc0f 100644 --- a/.claude/skills/krow-mobile-development-rules/SKILL.md +++ b/.claude/skills/krow-mobile-development-rules/SKILL.md @@ -585,7 +585,7 @@ testWidgets('shows loading indicator when logging in', (tester) async { ## 11. Clean Code Principles ### Documentation -- ✅ Add doc comments to all public classes and methods +- ✅ Add human readable doc comments for `dartdoc` for all classes and methods. ```dart /// Authenticates user with email and password. /// diff --git a/CLAUDE.md b/CLAUDE.md index fc5dda87..86facd07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,6 +133,30 @@ lib/src/ - `docs/MOBILE/05-release-process.md` — Release quick reference - `docs/RELEASE/mobile-releases.md` — Complete release guide +## Skills & Sub-Agents + +The project has 4 specialized skills in `.claude/skills/` that provide deep domain knowledge. **Invoke them when working in their domains** — they contain detailed rules, patterns, and code examples beyond what's in this file. + +### krow-mobile-architecture +**When to use:** Architecting new mobile features, debugging state management or BLoC lifecycle issues, preventing prop drilling, managing session state, implementing Data Connect connector repositories, setting up feature modules and DI, refactoring to Clean Architecture. + +**What it covers:** Full Clean Architecture implementation, package dependency graph, Data Connect service & session management (SessionHandlerMixin, SessionListener), connector pattern for reusable backend queries, BLoC lifecycle safety (singleton registration, BlocProvider.value(), BlocErrorHandler mixin with _safeEmit()), feature isolation rules, typed navigation with safe extensions, session store pattern. + +### krow-mobile-development-rules +**When to use:** Creating new mobile features/packages, implementing BLoCs/Use Cases/Repositories, integrating with Firebase Data Connect, migrating from prototypes, reviewing code compliance, setting up navigation flows. + +**What it covers:** Non-negotiable enforcement rules — file creation & package structure with exact path conventions, naming conventions, zero-tolerance logic placement boundaries (business rules → Use Cases only, state → BLoCs only, data transformation → Repositories), localization integration (all strings via core_localization, BLoCs emit failures not strings), Data Connect repository pattern with `_service.run()`, prototype migration rules, error handling pattern (domain failures → ErrorTranslator), enforcement checklist. + +### krow-mobile-design-system +**When to use:** Implementing any UI in mobile features, migrating POC/prototype designs to production, creating themed widgets, reviewing UI code for design system compliance, matching colors/typography from designs, adding icons/spacing/layout. + +**What it covers:** Immutable design token rules — all colors from `UiColors` (zero hex codes), all typography from `UiTypography` (zero custom TextStyle), all spacing/radius/elevation from `UiConstants` (zero magic numbers), all icons from `UiIcons` (zero direct library imports). POC → Production workflow (structure → architecture → design system integration), color/typography matching tables, extension policy for adding new tokens, review checklist. + +### krow-mobile-release +**When to use:** Preparing mobile releases, updating CHANGELOGs, triggering GitHub Actions release workflows, creating hotfix branches, understanding versioning strategy, setting up APK signing, troubleshooting release failures. + +**What it covers:** Versioning strategy (`v{major}.{minor}.{patch}-{milestone}`), CHANGELOG management (Keep a Changelog format, writing guidelines), Git tagging (`krow-withus--mobile/-vX.Y.Z`), GitHub Actions workflows (Product Release, Product Hotfix), APK signing setup (24 GitHub Secrets), step-by-step release process for dev/stage/prod, hotfix procedures, release cadence, troubleshooting guide, helper scripts. + ## CI/CD - `.github/workflows/mobile-ci.yml` — Mobile build & test on PR diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 52fbdc50..8b597294 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -1249,7 +1249,7 @@ "clock_in": "CLOCK IN", "decline": "DECLINE", "accept_shift": "ACCEPT SHIFT", - "apply_now": "APPLY NOW", + "apply_now": "BOOK SHIFT", "book_dialog": { "title": "Book Shift", "message": "Do you want to instantly book this shift?" diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 3e057580..cb5f4477 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -1244,7 +1244,7 @@ "clock_in": "ENTRADA", "decline": "RECHAZAR", "accept_shift": "ACEPTAR TURNO", - "apply_now": "SOLICITAR AHORA", + "apply_now": "RESERVAR TURNO", "book_dialog": { "title": "Reservar turno", "message": "\u00bfDesea reservar este turno al instante?" diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart index 989033ab..afcc60f4 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_page.dart @@ -8,13 +8,23 @@ import 'package:krow_domain/krow_domain.dart'; import 'package:staff_attire/src/presentation/blocs/attire/attire_cubit.dart'; import 'package:staff_attire/src/presentation/blocs/attire/attire_state.dart'; -import '../widgets/attire_filter_chips.dart'; +import '../widgets/attire_empty_section.dart'; import '../widgets/attire_info_card.dart'; import '../widgets/attire_item_card.dart'; +import '../widgets/attire_section_header.dart'; +import '../widgets/attire_section_tab.dart'; -class AttirePage extends StatelessWidget { +class AttirePage extends StatefulWidget { const AttirePage({super.key}); + @override + State createState() => _AttirePageState(); +} + +class _AttirePageState extends State { + bool _showRequired = true; + bool _showNonEssential = true; + @override Widget build(BuildContext context) { final AttireCubit cubit = Modular.get(); @@ -42,7 +52,12 @@ class AttirePage extends StatelessWidget { return const Center(child: CircularProgressIndicator()); } - final List filteredOptions = state.filteredOptions; + final List requiredItems = state.options + .where((AttireItem item) => item.isMandatory) + .toList(); + final List nonEssentialItems = state.options + .where((AttireItem item) => !item.isMandatory) + .toList(); return Column( children: [ @@ -55,55 +70,110 @@ class AttirePage extends StatelessWidget { const AttireInfoCard(), const SizedBox(height: UiConstants.space6), - // Filter Chips - AttireFilterChips( - selectedFilter: state.filter, - onFilterChanged: cubit.updateFilter, + // Section toggle chips + Row( + children: [ + AttireSectionTab( + label: 'Required', + isSelected: _showRequired, + onTap: () => setState( + () => _showRequired = !_showRequired, + ), + ), + const SizedBox(width: UiConstants.space3), + AttireSectionTab( + label: 'Non-Essential', + isSelected: _showNonEssential, + onTap: () => setState( + () => _showNonEssential = !_showNonEssential, + ), + ), + ], ), const SizedBox(height: UiConstants.space6), - // Item List - if (filteredOptions.isEmpty) - Padding( - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space10, - ), - child: Center( - child: Column( - children: [ - const Icon( - UiIcons.shirt, - size: 48, - color: UiColors.iconInactive, - ), - const SizedBox(height: UiConstants.space4), - Text( - context.t.staff_profile_attire.capture.no_items_filter, - style: UiTypography.body1m.textSecondary, - ), - ], - ), + // Required section + if (_showRequired) ...[ + AttireSectionHeader( + title: 'Required', + count: requiredItems.length, + ), + const SizedBox(height: UiConstants.space3), + if (requiredItems.isEmpty) + AttireEmptySection( + message: context + .t + .staff_profile_attire + .capture + .no_items_filter, + ) + else + ...requiredItems.map((AttireItem item) { + return Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), + child: AttireItemCard( + item: item, + isUploading: false, + uploadedPhotoUrl: state.photoUrls[item.id], + onTap: () { + Modular.to.toAttireCapture( + item: item, + initialPhotoUrl: state.photoUrls[item.id], + ); + }, + ), + ); + }), + ], + + // Divider between sections + if (_showRequired && _showNonEssential) + const Padding( + padding: EdgeInsets.symmetric( + vertical: UiConstants.space8, ), + child: Divider(), ) else - ...filteredOptions.map((AttireItem item) { - return Padding( - padding: const EdgeInsets.only( - bottom: UiConstants.space3, - ), - child: AttireItemCard( - item: item, - isUploading: false, - uploadedPhotoUrl: state.photoUrls[item.id], - onTap: () { - Modular.to.toAttireCapture( - item: item, - initialPhotoUrl: state.photoUrls[item.id], - ); - }, - ), - ); - }), + const SizedBox(height: UiConstants.space6), + + // Non-Essential section + if (_showNonEssential) ...[ + AttireSectionHeader( + title: 'Non-Essential', + count: nonEssentialItems.length, + ), + const SizedBox(height: UiConstants.space3), + if (nonEssentialItems.isEmpty) + AttireEmptySection( + message: context + .t + .staff_profile_attire + .capture + .no_items_filter, + ) + else + ...nonEssentialItems.map((AttireItem item) { + return Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), + child: AttireItemCard( + item: item, + isUploading: false, + uploadedPhotoUrl: state.photoUrls[item.id], + onTap: () { + Modular.to.toAttireCapture( + item: item, + initialPhotoUrl: state.photoUrls[item.id], + ); + }, + ), + ); + }), + ], const SizedBox(height: UiConstants.space20), ], ), @@ -117,3 +187,4 @@ class AttirePage extends StatelessWidget { ); } } + diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_empty_section.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_empty_section.dart new file mode 100644 index 00000000..07afd35f --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_empty_section.dart @@ -0,0 +1,24 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class AttireEmptySection extends StatelessWidget { + const AttireEmptySection({super.key, required this.message}); + + final String message; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space6), + child: Center( + child: Column( + children: [ + const Icon(UiIcons.shirt, size: 48, color: UiColors.iconInactive), + const SizedBox(height: UiConstants.space4), + Text(message, style: UiTypography.body1m.textSecondary), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_section_header.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_section_header.dart new file mode 100644 index 00000000..b39ef5bb --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_section_header.dart @@ -0,0 +1,24 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class AttireSectionHeader extends StatelessWidget { + const AttireSectionHeader({ + super.key, + required this.title, + required this.count, + }); + + final String title; + final int count; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text(title, style: UiTypography.headline4b), + const SizedBox(width: UiConstants.space2), + Text('($count)', style: UiTypography.body1m.textSecondary), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_section_tab.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_section_tab.dart new file mode 100644 index 00000000..365b80b4 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_section_tab.dart @@ -0,0 +1,41 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class AttireSectionTab extends StatelessWidget { + const AttireSectionTab({ + super.key, + required this.label, + required this.isSelected, + required this.onTap, + }); + + final String label; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space2, + ), + decoration: BoxDecoration( + color: isSelected ? UiColors.primary : UiColors.white, + borderRadius: UiConstants.radiusFull, + border: Border.all( + color: isSelected ? UiColors.primary : UiColors.border, + ), + ), + child: Text( + label, + style: isSelected + ? UiTypography.footnote2m.white + : UiTypography.footnote2m.textSecondary, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index 06fd236f..11caa4ac 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -328,9 +328,9 @@ class _ShiftDetailsPageState extends State { backgroundColor: UiColors.bgPopup, shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg), title: Row( + spacing: UiConstants.space2, children: [ const Icon(UiIcons.warning, color: UiColors.error), - const SizedBox(width: UiConstants.space2), Expanded( child: Text( context.t.staff_shifts.shift_details.eligibility_requirements, @@ -350,7 +350,7 @@ class _ShiftDetailsPageState extends State { UiButton.primary( text: "Go to Certificates", onPressed: () { - Navigator.of(ctx).pop(); + Modular.to.popSafe(); Modular.to.toCertificates(); }, ), diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 7fd533da..3b76b755 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -853,14 +853,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: transitive description: @@ -929,18 +921,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" melos: dependency: "direct dev" description: @@ -1516,26 +1508,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.15" typed_data: dependency: transitive description: diff --git a/makefiles/dataconnect.mk b/makefiles/dataconnect.mk index 9006a982..cd4423ff 100644 --- a/makefiles/dataconnect.mk +++ b/makefiles/dataconnect.mk @@ -3,11 +3,11 @@ # Usage examples: # make dataconnect-sync DC_ENV=dev # make dataconnect-sync-full DC_ENV=dev -# make dataconnect-seed DC_ENV=validation -# make dataconnect-clean DC_ENV=validation +# make dataconnect-seed DC_ENV=dev +# make dataconnect-clean DC_ENV=dev # make dataconnect-generate-sdk DC_ENV=dev # -DC_ENV ?= validation +DC_ENV ?= dev DC_LOCATION ?= us-central1 DC_CONNECTOR_ID ?= example