Merge branch 'dev' into feature/centralized-data-error-handling and resolve conflicts

This commit is contained in:
2026-02-11 12:34:29 +05:30
158 changed files with 10945 additions and 5478 deletions

View File

@@ -2,14 +2,14 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension;
import 'package:flutter_modular/flutter_modular.dart'
hide ModularWatchExtension;
import 'package:intl/intl.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/availability_bloc.dart';
import '../blocs/availability_event.dart';
import '../blocs/availability_state.dart';
import 'package:krow_domain/krow_domain.dart';
class AvailabilityPage extends StatefulWidget {
const AvailabilityPage({super.key});
@@ -46,7 +46,6 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
return BlocProvider.value(
value: _bloc,
child: Scaffold(
backgroundColor: AppColors.krowBackground,
appBar: UiAppBar(
title: 'My Availability',
centerTitle: false,
@@ -55,13 +54,18 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
body: BlocListener<AvailabilityBloc, AvailabilityState>(
listener: (context, state) {
if (state is AvailabilityLoaded && state.successMessage != null) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.successMessage!),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
UiSnackbar.show(
context,
message: state.successMessage!,
type: UiSnackbarType.success,
);
}
if (state is AvailabilityError) {
UiSnackbar.show(
context,
message: state.message,
type: UiSnackbarType.error,
);
} else if (state is AvailabilityError) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
@@ -85,19 +89,19 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: UiConstants.space6,
children: [
_buildQuickSet(context),
const SizedBox(height: 24),
_buildWeekNavigation(context, state),
const SizedBox(height: 24),
_buildSelectedDayAvailability(
context,
state.selectedDayAvailability,
),
const SizedBox(height: 24),
_buildInfoCard(),
],
),
@@ -106,12 +110,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
),
),
if (state.isActionInProgress)
Container(
color: Colors.black.withOpacity(0.3),
child: const Center(
child: CircularProgressIndicator(),
),
),
const UiLoadingPage(), // Show loading overlay during actions
],
);
} else if (state is AvailabilityError) {
@@ -124,14 +123,12 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
Text(
translateErrorKey(state.message),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: AppColors.krowMuted,
),
style: UiTypography.body2r.textSecondary,
),
],
),
),
),
);
}
return const SizedBox.shrink();
@@ -144,49 +141,31 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
Widget _buildQuickSet(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
color: UiColors.primary.withValues(alpha: 0.1),
borderRadius: UiConstants.radiusLg,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'Quick Set Availability',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF333F48),
),
style: UiTypography.body2b,
),
const SizedBox(height: 12),
const SizedBox(height: UiConstants.space3),
Row(
children: [
Expanded(child: _buildQuickSetButton(context, 'All Week', 'all')),
const SizedBox(width: UiConstants.space2),
Expanded(
child: _buildQuickSetButton(
context,
'All Week',
'all',
),
child: _buildQuickSetButton(context, 'Weekdays', 'weekdays'),
),
const SizedBox(width: 8),
const SizedBox(width: UiConstants.space2),
Expanded(
child: _buildQuickSetButton(
context,
'Weekdays',
'weekdays',
),
child: _buildQuickSetButton(context, 'Weekends', 'weekends'),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
context,
'Weekends',
'weekends',
),
),
const SizedBox(width: 8),
const SizedBox(width: UiConstants.space2),
Expanded(
child: _buildQuickSetButton(
context,
@@ -211,23 +190,26 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
return SizedBox(
height: 32,
child: OutlinedButton(
onPressed: () => context.read<AvailabilityBloc>().add(PerformQuickSet(type)),
onPressed: () =>
context.read<AvailabilityBloc>().add(PerformQuickSet(type)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.zero,
side: BorderSide(
color: isDestructive
? Colors.red.withOpacity(0.2)
: AppColors.krowBlue.withOpacity(0.2),
? UiColors.destructive.withValues(alpha: 0.2)
: UiColors.primary.withValues(alpha: 0.2),
),
backgroundColor: Colors.transparent,
backgroundColor: UiColors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: UiConstants.radiusLg,
),
foregroundColor: isDestructive ? Colors.red : AppColors.krowBlue,
foregroundColor: isDestructive
? UiColors.destructive
: UiColors.primary,
),
child: Text(
label,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500),
style: UiTypography.body4r,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -241,42 +223,35 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
final monthYear = DateFormat('MMMM yyyy').format(middleDate);
return Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
color: UiColors.cardViewBackground,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: Column(
children: [
// Nav Header
Padding(
padding: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildNavButton(
LucideIcons.chevronLeft,
() => context.read<AvailabilityBloc>().add(const NavigateWeek(-1)),
UiIcons.chevronLeft,
() => context.read<AvailabilityBloc>().add(
const NavigateWeek(-1),
),
),
Text(
monthYear,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
style: UiTypography.title2b,
),
_buildNavButton(
LucideIcons.chevronRight,
() => context.read<AvailabilityBloc>().add(const NavigateWeek(1)),
UiIcons.chevronRight,
() => context.read<AvailabilityBloc>().add(
const NavigateWeek(1),
),
),
],
),
@@ -284,7 +259,9 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
// Days Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: state.days.map((day) => _buildDayItem(context, day, state.selectedDate)).toList(),
children: state.days
.map((day) => _buildDayItem(context, day, state.selectedDate))
.toList(),
),
],
),
@@ -298,15 +275,19 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
width: 32,
height: 32,
decoration: const BoxDecoration(
color: Color(0xFFF1F5F9), // slate-100
color: UiColors.separatorSecondary,
shape: BoxShape.circle,
),
child: Icon(icon, size: 20, color: AppColors.krowMuted),
child: Icon(icon, size: 20, color: UiColors.iconSecondary),
),
);
}
Widget _buildDayItem(BuildContext context, DayAvailability day, DateTime selectedDate) {
Widget _buildDayItem(
BuildContext context,
DayAvailability day,
DateTime selectedDate,
) {
final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate);
final isAvailable = day.isAvailable;
final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now());
@@ -316,30 +297,19 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
onTap: () => context.read<AvailabilityBloc>().add(SelectDate(day.date)),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
decoration: BoxDecoration(
color: isSelected
? AppColors.krowBlue
: (isAvailable
? const Color(0xFFECFDF5)
: const Color(0xFFF8FAFC)), // emerald-50 or slate-50
borderRadius: BorderRadius.circular(16),
? UiColors.primary
: (isAvailable ? UiColors.tagSuccess : UiColors.bgSecondary),
borderRadius: UiConstants.radiusLg,
border: Border.all(
color: isSelected
? AppColors.krowBlue
? UiColors.primary
: (isAvailable
? const Color(0xFFA7F3D0)
: Colors.transparent), // emerald-200
? UiColors.success.withValues(alpha: 0.3)
: UiColors.transparent),
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColors.krowBlue.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
]
: null,
),
child: Stack(
clipBehavior: Clip.none,
@@ -349,26 +319,24 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
children: [
Text(
day.date.day.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 18,
style: UiTypography.title1m.copyWith(
fontWeight: FontWeight.bold,
color: isSelected
? Colors.white
? UiColors.white
: (isAvailable
? const Color(0xFF047857)
: AppColors.krowMuted), // emerald-700
? UiColors.textSuccess
: UiColors.textSecondary),
),
),
const SizedBox(height: 2),
Text(
DateFormat('EEE').format(day.date),
style: TextStyle(
fontSize: 10,
DateFormat('EEE').format(day.date),
style: UiTypography.footnote2r.copyWith(
color: isSelected
? Colors.white.withOpacity(0.8)
? UiColors.white.withValues(alpha: 0.8)
: (isAvailable
? const Color(0xFF047857)
: AppColors.krowMuted),
? UiColors.textSuccess
: UiColors.textSecondary),
),
),
],
@@ -380,7 +348,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
width: 6,
height: 6,
decoration: const BoxDecoration(
color: AppColors.krowBlue,
color: UiColors.primary,
shape: BoxShape.circle,
),
),
@@ -400,18 +368,11 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
final isAvailable = day.isAvailable;
return Container(
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(UiConstants.space5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
color: UiColors.cardViewBackground,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: Column(
children: [
@@ -424,114 +385,112 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
children: [
Text(
dateStr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
style: UiTypography.title2b,
),
Text(
isAvailable ? 'You are available' : 'Not available',
style: const TextStyle(
fontSize: 14,
color: AppColors.krowMuted,
),
style: UiTypography.body2r.textSecondary,
),
],
),
Switch(
value: isAvailable,
onChanged: (val) => context.read<AvailabilityBloc>().add(ToggleDayStatus(day)),
activeColor: AppColors.krowBlue,
onChanged: (val) =>
context.read<AvailabilityBloc>().add(ToggleDayStatus(day)),
activeColor: UiColors.primary,
),
],
),
const SizedBox(height: 16),
const SizedBox(height: UiConstants.space4),
// Time Slots (only from Domain)
...day.slots.map((slot) {
// Get UI config for this slot ID
final uiConfig = _getSlotUiConfig(slot.id);
return _buildTimeSlotItem(context, day, slot, uiConfig);
}).toList(),
],
),
);
}
Map<String, dynamic> _getSlotUiConfig(String slotId) {
switch (slotId) {
case 'morning':
return {
'icon': LucideIcons.sunrise,
'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10
'iconColor': const Color(0xFF0032A0),
'icon': UiIcons.sunrise,
'bg': UiColors.primary.withValues(alpha: 0.1),
'iconColor': UiColors.primary,
};
case 'afternoon':
return {
'icon': LucideIcons.sun,
'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20
'iconColor': const Color(0xFF0032A0),
'icon': UiIcons.sun,
'bg': UiColors.primary.withValues(alpha: 0.2),
'iconColor': UiColors.primary,
};
case 'evening':
return {
'icon': LucideIcons.moon,
'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10
'iconColor': const Color(0xFF333F48),
'icon': UiIcons.moon,
'bg': UiColors.bgSecondary,
'iconColor': UiColors.foreground,
};
default:
return {
'icon': LucideIcons.clock,
'bg': Colors.grey.shade100,
'iconColor': Colors.grey,
'icon': UiIcons.clock,
'bg': UiColors.bgSecondary,
'iconColor': UiColors.iconSecondary,
};
}
}
Widget _buildTimeSlotItem(
BuildContext context,
DayAvailability day,
AvailabilitySlot slot,
Map<String, dynamic> uiConfig
BuildContext context,
DayAvailability day,
AvailabilitySlot slot,
Map<String, dynamic> uiConfig,
) {
// Determine styles based on state
final isEnabled = day.isAvailable;
final isEnabled = day.isAvailable;
final isActive = slot.isAvailable;
// Container style
Color bgColor;
Color borderColor;
if (!isEnabled) {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFF1F5F9); // slate-100
bgColor = UiColors.bgSecondary;
borderColor = UiColors.borderInactive;
} else if (isActive) {
bgColor = AppColors.krowBlue.withOpacity(0.05);
borderColor = AppColors.krowBlue.withOpacity(0.2);
bgColor = UiColors.primary.withValues(alpha: 0.05);
borderColor = UiColors.primary.withValues(alpha: 0.2);
} else {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFE2E8F0); // slate-200
bgColor = UiColors.bgSecondary;
borderColor = UiColors.borderPrimary;
}
// Text colors
final titleColor = (isEnabled && isActive)
? AppColors.krowCharcoal
: AppColors.krowMuted;
? UiColors.foreground
: UiColors.mutedForeground;
final subtitleColor = (isEnabled && isActive)
? AppColors.krowMuted
: Colors.grey.shade400;
? UiColors.mutedForeground
: UiColors.textInactive;
return GestureDetector(
onTap: isEnabled ? () => context.read<AvailabilityBloc>().add(ToggleSlotStatus(day, slot.id)) : null,
onTap: isEnabled
? () => context.read<AvailabilityBloc>().add(
ToggleSlotStatus(day, slot.id),
)
: null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: UiConstants.space3),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: borderColor, width: 2),
),
child: Row(
@@ -542,7 +501,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
height: 40,
decoration: BoxDecoration(
color: uiConfig['bg'],
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Icon(
uiConfig['icon'],
@@ -550,7 +509,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
size: 20,
),
),
const SizedBox(width: 12),
const SizedBox(width: UiConstants.space3),
// Text
Expanded(
child: Column(
@@ -558,18 +517,11 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
children: [
Text(
slot.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: titleColor,
),
style: UiTypography.body2m.copyWith(color: titleColor),
),
Text(
slot.timeRange,
style: TextStyle(
fontSize: 12,
color: subtitleColor,
),
style: UiTypography.body3r.copyWith(color: subtitleColor),
),
],
),
@@ -580,13 +532,13 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
width: 24,
height: 24,
decoration: const BoxDecoration(
color: AppColors.krowBlue,
color: UiColors.primary,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.check,
UiIcons.check,
size: 16,
color: Colors.white,
color: UiColors.white,
),
)
else if (isEnabled && !isActive)
@@ -596,9 +548,9 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFCBD5E1),
color: UiColors.borderStill,
width: 2,
), // slate-300
),
),
),
],
@@ -609,32 +561,28 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
Widget _buildInfoCard() {
return Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
color: UiColors.primary.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: const Row(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space3,
children: [
Icon(LucideIcons.clock, size: 20, color: AppColors.krowBlue),
SizedBox(width: 12),
const Icon(UiIcons.clock, size: 20, color: UiColors.primary),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space1,
children: [
Text(
'Auto-Match uses your availability',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.krowCharcoal,
),
style: UiTypography.body2m,
),
SizedBox(height: 2),
Text(
"When enabled, you'll only be matched with shifts during your available times.",
style: TextStyle(fontSize: 12, color: AppColors.krowMuted),
style: UiTypography.body3r.textSecondary,
),
],
),
@@ -644,15 +592,3 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
);
}
}
class AppColors {
static const Color krowBlue = Color(0xFF0A39DF);
static const Color krowYellow = Color(0xFFFFED4A);
static const Color krowCharcoal = Color(0xFF121826);
static const Color krowMuted = Color(0xFF6A7382);
static const Color krowBorder = Color(0xFFE3E6E9);
static const Color krowBackground = Color(0xFFFAFBFC);
static const Color white = Colors.white;
static const Color black = Colors.black;
}

View File

@@ -1,22 +1,17 @@
name: staff_availability
description: Staff Availability Feature
version: 0.0.1
publish_to: 'none'
publish_to: "none"
resolution: workspace
environment:
sdk: '>=3.10.0 <4.0.0'
sdk: ">=3.10.0 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.3
equatable: ^2.0.5
intl: ^0.20.0
lucide_icons: ^0.257.0
flutter_modular: ^6.3.2
# Internal packages
core_localization:
path: ../../../core_localization
@@ -28,6 +23,11 @@ dependencies:
path: ../../../data_connect
krow_core:
path: ../../../core
flutter_bloc: ^8.1.3
equatable: ^2.0.5
intl: ^0.20.0
flutter_modular: ^6.3.2
firebase_data_connect: ^0.2.2+2
firebase_auth: ^6.1.4