diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart new file mode 100644 index 00000000..fd68a50e --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart @@ -0,0 +1,245 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:core_localization/core_localization.dart'; + +import '../blocs/attire_cubit.dart'; +import '../blocs/attire_state.dart'; +import '../widgets/attestation_checkbox.dart'; + +class AttireCapturePage extends StatefulWidget { + const AttireCapturePage({super.key, required this.item}); + + final AttireItem item; + + @override + State createState() => _AttireCapturePageState(); +} + +class _AttireCapturePageState extends State { + bool _isAttested = false; + + void _onUpload(BuildContext context) { + if (!_isAttested) { + UiSnackbar.show( + context, + message: 'Please attest that you own this item.', + type: UiSnackbarType.error, + margin: const EdgeInsets.all(UiConstants.space4), + ); + return; + } + // Call the upload via cubit + final AttireCubit cubit = Modular.get(); + cubit.uploadPhoto(widget.item.id); + } + + void _viewEnlargedImage(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + constraints: const BoxConstraints(maxHeight: 500, maxWidth: 500), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + image: DecorationImage( + image: NetworkImage( + widget.item.imageUrl ?? + 'https://images.unsplash.com/photo-1549298916-b41d501d3772?auto=format&fit=crop&q=80&w=400&h=400', + ), + fit: BoxFit.contain, + ), + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final AttireCubit cubit = Modular.get(); + + return Scaffold( + backgroundColor: UiColors.background, + appBar: UiAppBar(title: widget.item.label, showBackButton: true), + body: BlocConsumer( + bloc: cubit, + listener: (BuildContext context, AttireState state) { + if (state.status == AttireStatus.failure) { + UiSnackbar.show( + context, + message: translateErrorKey(state.errorMessage ?? 'Error'), + type: UiSnackbarType.error, + ); + } + }, + builder: (BuildContext context, AttireState state) { + final bool isUploading = + state.uploadingStatus[widget.item.id] ?? false; + final bool hasPhoto = state.photoUrls.containsKey(widget.item.id); + final String statusText = hasPhoto + ? 'Pending Verification' + : 'Not Uploaded'; + final Color statusColor = hasPhoto + ? UiColors.textWarning + : UiColors.textInactive; + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + children: [ + // Image Preview + GestureDetector( + onTap: () => _viewEnlargedImage(context), + child: Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x19000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + image: DecorationImage( + image: NetworkImage( + widget.item.imageUrl ?? + 'https://images.unsplash.com/photo-1549298916-b41d501d3772?auto=format&fit=crop&q=80&w=400&h=400', + ), + fit: BoxFit.cover, + ), + ), + child: const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + UiIcons.search, + color: UiColors.white, + shadows: [ + Shadow(color: Colors.black, blurRadius: 4), + ], + ), + ), + ), + ), + ), + const SizedBox(height: UiConstants.space6), + + Text( + widget.item.description ?? '', + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + const SizedBox(height: UiConstants.space6), + + // Verification info + Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.bgPopup, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + border: Border.all(color: UiColors.border), + ), + child: Row( + children: [ + const Icon( + UiIcons.info, + color: UiColors.primary, + size: 24, + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Verification Status', + style: UiTypography.footnote2m.textPrimary, + ), + Text( + statusText, + style: UiTypography.body2m.copyWith( + color: statusColor, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: UiConstants.space6), + + AttestationCheckbox( + isChecked: _isAttested, + onChanged: (bool? val) { + setState(() { + _isAttested = val ?? false; + }); + }, + ), + const SizedBox(height: UiConstants.space6), + + if (isUploading) + const Center(child: CircularProgressIndicator()) + else if (!hasPhoto || + true) // Show options even if has photo (allows re-upload) + Row( + children: [ + Expanded( + child: UiButton.secondary( + text: 'Gallery', + onPressed: () => _onUpload(context), + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: UiButton.primary( + text: 'Camera', + onPressed: () => _onUpload(context), + ), + ), + ], + ), + ], + ), + ), + ), + if (hasPhoto) + SafeArea( + child: Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: SizedBox( + width: double.infinity, + child: UiButton.primary( + text: 'Submit Image', + onPressed: () { + Modular.to.pop(); + }, + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} 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 862397c6..7e17a08b 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 @@ -6,89 +6,142 @@ import 'package:core_localization/core_localization.dart'; import '../blocs/attire_cubit.dart'; import '../blocs/attire_state.dart'; -import '../widgets/attestation_checkbox.dart'; -import '../widgets/attire_bottom_bar.dart'; -import '../widgets/attire_grid.dart'; import '../widgets/attire_info_card.dart'; +import '../widgets/attire_item_card.dart'; +import 'attire_capture_page.dart'; +import 'package:krow_domain/krow_domain.dart'; -class AttirePage extends StatelessWidget { +class AttirePage extends StatefulWidget { const AttirePage({super.key}); @override - Widget build(BuildContext context) { - // Note: t.staff_profile_attire is available via re-export of core_localization - final AttireCubit cubit = Modular.get(); + State createState() => _AttirePageState(); +} - return BlocProvider.value( - value: cubit, - child: Scaffold( - backgroundColor: UiColors.background, // FAFBFC - appBar: UiAppBar( - title: t.staff_profile_attire.title, - showBackButton: true, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), +class _AttirePageState extends State { + String _filter = 'All'; + + Widget _buildFilterChip(String label) { + final bool isSelected = _filter == label; + return GestureDetector( + onTap: () => setState(() => _filter = label), + 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, ), ), - body: BlocConsumer( + child: Text( + label, + textAlign: TextAlign.center, + style: (isSelected + ? UiTypography.footnote2m.white + : UiTypography.footnote2m.textSecondary), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final AttireCubit cubit = Modular.get(); + + return Scaffold( + backgroundColor: UiColors.background, + appBar: UiAppBar( + title: t.staff_profile_attire.title, + showBackButton: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container(color: UiColors.border, height: 1.0), + ), + ), + body: BlocProvider.value( + value: cubit, + child: BlocConsumer( listener: (BuildContext context, AttireState state) { if (state.status == AttireStatus.failure) { UiSnackbar.show( context, message: translateErrorKey(state.errorMessage ?? 'Error'), type: UiSnackbarType.error, - margin: const EdgeInsets.only( - bottom: 150, - left: UiConstants.space4, - right: UiConstants.space4, - ), ); } - if (state.status == AttireStatus.saved) { - Modular.to.pop(); - } }, builder: (BuildContext context, AttireState state) { if (state.status == AttireStatus.loading && state.options.isEmpty) { return const Center(child: CircularProgressIndicator()); } + final List options = state.options; + final List filteredOptions = options.where(( + AttireItem item, + ) { + if (_filter == 'Required') return item.isMandatory; + if (_filter == 'Non-Essential') return !item.isMandatory; + return true; + }).toList(); + return Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(UiConstants.space5), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ const AttireInfoCard(), const SizedBox(height: UiConstants.space6), - AttireGrid( - items: state.options, - selectedIds: state.selectedIds, - photoUrls: state.photoUrls, - uploadingStatus: state.uploadingStatus, - onToggle: cubit.toggleSelection, - onUpload: cubit.uploadPhoto, + + // Filter Chips + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildFilterChip('All'), + const SizedBox(width: UiConstants.space2), + _buildFilterChip('Required'), + const SizedBox(width: UiConstants.space2), + _buildFilterChip('Non-Essential'), + ], + ), ), const SizedBox(height: UiConstants.space6), - AttestationCheckbox( - isChecked: state.attestationChecked, - onChanged: (bool? val) => - cubit.toggleAttestation(val ?? false), - ), + + // Item List + ...filteredOptions.map((AttireItem item) { + return Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), + child: AttireItemCard( + item: item, + isUploading: + state.uploadingStatus[item.id] ?? false, + uploadedPhotoUrl: state.photoUrls[item.id], + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext ctx) => + AttireCapturePage(item: item), + ), + ); + }, + ), + ); + }).toList(), const SizedBox(height: UiConstants.space20), ], ), ), ), - AttireBottomBar( - canSave: state.canSave, - allMandatorySelected: state.allMandatorySelected, - allMandatoryHavePhotos: state.allMandatoryHavePhotos, - attestationChecked: state.attestationChecked, - onSave: cubit.save, - ), ], ); }, diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_item_card.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_item_card.dart new file mode 100644 index 00000000..61124f83 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_item_card.dart @@ -0,0 +1,141 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +class AttireItemCard extends StatelessWidget { + final AttireItem item; + final String? uploadedPhotoUrl; + final bool isUploading; + final VoidCallback onTap; + + const AttireItemCard({ + super.key, + required this.item, + this.uploadedPhotoUrl, + this.isUploading = false, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final bool hasPhoto = uploadedPhotoUrl != null; + + final String statusText = hasPhoto ? 'Pending' : 'Not Uploaded'; + final Color statusColor = hasPhoto + ? UiColors.textWarning + : UiColors.textInactive; + + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: Border.all(color: UiColors.border), + boxShadow: const [ + BoxShadow( + color: Color(0x19000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + image: DecorationImage( + image: NetworkImage( + item.imageUrl ?? + 'https://images.unsplash.com/photo-1549298916-b41d501d3772?auto=format&fit=crop&q=80&w=400&h=400', + ), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: UiConstants.space4), + // details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.label, style: UiTypography.body1m.textPrimary), + if (item.description != null) ...[ + const SizedBox(height: UiConstants.space1), + Text( + item.description!, + style: UiTypography.body2r.textSecondary, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: UiConstants.space2), + Row( + children: [ + if (item.isMandatory) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: UiColors.error.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Required', + style: UiTypography.footnote2m.textError, + ), + ), + const Spacer(), + if (isUploading) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else if (hasPhoto) + Text( + statusText, + style: UiTypography.footnote2m.copyWith( + color: statusColor, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(width: UiConstants.space2), + // Chevron or status + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20), + if (!hasPhoto && !isUploading) + const Icon( + UiIcons.chevronRight, + color: UiColors.textInactive, + size: 24, + ) + else if (hasPhoto && !isUploading) + const Icon( + UiIcons.check, + color: UiColors.textWarning, + size: 24, + ), + ], + ), + ], + ), + ), + ); + } +}