Merge branch '312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation' into fix_staff_app_bugs
This commit is contained in:
@@ -200,6 +200,8 @@ class _BillingViewState extends State<BillingView> {
|
||||
const SpendingBreakdownCard(),
|
||||
if (state.invoiceHistory.isEmpty) _buildEmptyState(context)
|
||||
else InvoiceHistorySection(invoices: state.invoiceHistory),
|
||||
|
||||
const SizedBox(height: UiConstants.space32),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/coverage_repository.dart';
|
||||
import '../../domain/ui_entities/coverage_entities.dart';
|
||||
|
||||
/// Implementation of [CoverageRepository] in the Data layer.
|
||||
///
|
||||
@@ -25,18 +25,14 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
|
||||
final String? businessId =
|
||||
dc.ClientSessionStore.instance.session?.business?.id;
|
||||
print('Coverage: now=${DateTime.now().toIso8601String()}');
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
print('Coverage: missing businessId for date=${date.toIso8601String()}');
|
||||
return <CoverageShift>[];
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||
final DateTime end =
|
||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
||||
print(
|
||||
'Coverage: request businessId=$businessId dayStart=${start.toIso8601String()} dayEnd=${end.toIso8601String()}',
|
||||
);
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
||||
@@ -58,9 +54,6 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
dayEnd: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
print(
|
||||
'Coverage: ${date.toIso8601String()} staffsApplications=${applicationsResult.data.applications.length}',
|
||||
);
|
||||
|
||||
return _mapCoverageShifts(
|
||||
shiftRolesResult.data.shiftRoles,
|
||||
@@ -84,11 +77,16 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
final List<CoverageWorker> allWorkers =
|
||||
shifts.expand((CoverageShift shift) => shift.workers).toList();
|
||||
final int totalConfirmed = allWorkers.length;
|
||||
final int checkedIn =
|
||||
allWorkers.where((CoverageWorker w) => w.isCheckedIn).length;
|
||||
final int enRoute =
|
||||
allWorkers.where((CoverageWorker w) => w.isEnRoute).length;
|
||||
final int late = allWorkers.where((CoverageWorker w) => w.isLate).length;
|
||||
final int checkedIn = allWorkers
|
||||
.where((CoverageWorker w) => w.status == CoverageWorkerStatus.checkedIn)
|
||||
.length;
|
||||
final int enRoute = allWorkers
|
||||
.where((CoverageWorker w) =>
|
||||
w.status == CoverageWorkerStatus.confirmed && w.checkInTime == null)
|
||||
.length;
|
||||
final int late = allWorkers
|
||||
.where((CoverageWorker w) => w.status == CoverageWorkerStatus.late)
|
||||
.length;
|
||||
|
||||
return CoverageStats(
|
||||
totalNeeded: totalNeeded,
|
||||
@@ -172,25 +170,32 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
.toList();
|
||||
}
|
||||
|
||||
String _mapWorkerStatus(
|
||||
CoverageWorkerStatus _mapWorkerStatus(
|
||||
dc.EnumValue<dc.ApplicationStatus> status,
|
||||
) {
|
||||
if (status is dc.Known<dc.ApplicationStatus>) {
|
||||
switch (status.value) {
|
||||
case dc.ApplicationStatus.LATE:
|
||||
return 'late';
|
||||
case dc.ApplicationStatus.CHECKED_IN:
|
||||
case dc.ApplicationStatus.CHECKED_OUT:
|
||||
case dc.ApplicationStatus.ACCEPTED:
|
||||
case dc.ApplicationStatus.CONFIRMED:
|
||||
case dc.ApplicationStatus.PENDING:
|
||||
return CoverageWorkerStatus.pending;
|
||||
case dc.ApplicationStatus.ACCEPTED:
|
||||
return CoverageWorkerStatus.confirmed;
|
||||
case dc.ApplicationStatus.REJECTED:
|
||||
return CoverageWorkerStatus.rejected;
|
||||
case dc.ApplicationStatus.CONFIRMED:
|
||||
return CoverageWorkerStatus.confirmed;
|
||||
case dc.ApplicationStatus.CHECKED_IN:
|
||||
return CoverageWorkerStatus.checkedIn;
|
||||
case dc.ApplicationStatus.CHECKED_OUT:
|
||||
return CoverageWorkerStatus.checkedOut;
|
||||
case dc.ApplicationStatus.LATE:
|
||||
return CoverageWorkerStatus.late;
|
||||
case dc.ApplicationStatus.NO_SHOW:
|
||||
return CoverageWorkerStatus.noShow;
|
||||
case dc.ApplicationStatus.COMPLETED:
|
||||
return 'confirmed';
|
||||
return CoverageWorkerStatus.completed;
|
||||
}
|
||||
}
|
||||
return 'confirmed';
|
||||
return CoverageWorkerStatus.pending;
|
||||
}
|
||||
|
||||
String? _formatTime(fdc.Timestamp? timestamp) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '../ui_entities/coverage_entities.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for coverage-related operations.
|
||||
///
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Domain entity representing a shift in the coverage view.
|
||||
///
|
||||
/// This is a feature-specific domain entity that encapsulates shift information
|
||||
/// including scheduling details and assigned workers.
|
||||
class CoverageShift extends Equatable {
|
||||
/// Creates a [CoverageShift].
|
||||
const CoverageShift({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.location,
|
||||
required this.startTime,
|
||||
required this.workersNeeded,
|
||||
required this.date,
|
||||
required this.workers,
|
||||
});
|
||||
|
||||
/// The unique identifier for the shift.
|
||||
final String id;
|
||||
|
||||
/// The title or role of the shift.
|
||||
final String title;
|
||||
|
||||
/// The location where the shift takes place.
|
||||
final String location;
|
||||
|
||||
/// The start time of the shift (e.g., "16:00").
|
||||
final String startTime;
|
||||
|
||||
/// The number of workers needed for this shift.
|
||||
final int workersNeeded;
|
||||
|
||||
/// The date of the shift.
|
||||
final DateTime date;
|
||||
|
||||
/// The list of workers assigned to this shift.
|
||||
final List<CoverageWorker> workers;
|
||||
|
||||
/// Calculates the coverage percentage for this shift.
|
||||
int get coveragePercent {
|
||||
if (workersNeeded == 0) return 0;
|
||||
return ((workers.length / workersNeeded) * 100).round();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
title,
|
||||
location,
|
||||
startTime,
|
||||
workersNeeded,
|
||||
date,
|
||||
workers,
|
||||
];
|
||||
}
|
||||
|
||||
/// Domain entity representing a worker in the coverage view.
|
||||
///
|
||||
/// This entity tracks worker status including check-in information.
|
||||
class CoverageWorker extends Equatable {
|
||||
/// Creates a [CoverageWorker].
|
||||
const CoverageWorker({
|
||||
required this.name,
|
||||
required this.status,
|
||||
this.checkInTime,
|
||||
});
|
||||
|
||||
/// The name of the worker.
|
||||
final String name;
|
||||
|
||||
/// The status of the worker ('confirmed', 'late', etc.).
|
||||
final String status;
|
||||
|
||||
/// The time the worker checked in, if applicable.
|
||||
final String? checkInTime;
|
||||
|
||||
/// Returns true if the worker is checked in.
|
||||
bool get isCheckedIn => status == 'confirmed' && checkInTime != null;
|
||||
|
||||
/// Returns true if the worker is en route.
|
||||
bool get isEnRoute => status == 'confirmed' && checkInTime == null;
|
||||
|
||||
/// Returns true if the worker is late.
|
||||
bool get isLate => status == 'late';
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[name, status, checkInTime];
|
||||
}
|
||||
|
||||
/// Domain entity representing coverage statistics.
|
||||
///
|
||||
/// Aggregates coverage metrics for a specific date.
|
||||
class CoverageStats extends Equatable {
|
||||
/// Creates a [CoverageStats].
|
||||
const CoverageStats({
|
||||
required this.totalNeeded,
|
||||
required this.totalConfirmed,
|
||||
required this.checkedIn,
|
||||
required this.enRoute,
|
||||
required this.late,
|
||||
});
|
||||
|
||||
/// The total number of workers needed.
|
||||
final int totalNeeded;
|
||||
|
||||
/// The total number of confirmed workers.
|
||||
final int totalConfirmed;
|
||||
|
||||
/// The number of workers who have checked in.
|
||||
final int checkedIn;
|
||||
|
||||
/// The number of workers en route.
|
||||
final int enRoute;
|
||||
|
||||
/// The number of late workers.
|
||||
final int late;
|
||||
|
||||
/// Calculates the overall coverage percentage.
|
||||
int get coveragePercent {
|
||||
if (totalNeeded == 0) return 0;
|
||||
return ((totalConfirmed / totalNeeded) * 100).round();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
totalNeeded,
|
||||
totalConfirmed,
|
||||
checkedIn,
|
||||
enRoute,
|
||||
late,
|
||||
];
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../arguments/get_coverage_stats_arguments.dart';
|
||||
import '../repositories/coverage_repository.dart';
|
||||
import '../ui_entities/coverage_entities.dart';
|
||||
|
||||
/// Use case for fetching coverage statistics for a specific date.
|
||||
///
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../arguments/get_shifts_for_date_arguments.dart';
|
||||
import '../repositories/coverage_repository.dart';
|
||||
import '../ui_entities/coverage_entities.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Use case for fetching shifts for a specific date.
|
||||
///
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../domain/arguments/get_coverage_stats_arguments.dart';
|
||||
import '../../domain/arguments/get_shifts_for_date_arguments.dart';
|
||||
import '../../domain/ui_entities/coverage_entities.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/usecases/get_coverage_stats_usecase.dart';
|
||||
import '../../domain/usecases/get_shifts_for_date_usecase.dart';
|
||||
import 'coverage_event.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/ui_entities/coverage_entities.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Enum representing the status of coverage data loading.
|
||||
enum CoverageStatus {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/ui_entities/coverage_entities.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Quick statistics cards showing coverage metrics.
|
||||
///
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../domain/ui_entities/coverage_entities.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// List of shifts with their workers.
|
||||
///
|
||||
@@ -194,7 +194,8 @@ class _ShiftHeader extends StatelessWidget {
|
||||
size: UiConstants.space3,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
Expanded(child: Text(
|
||||
Expanded(
|
||||
child: Text(
|
||||
location,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -314,36 +315,92 @@ class _WorkerRow extends StatelessWidget {
|
||||
Color badgeText;
|
||||
String badgeLabel;
|
||||
|
||||
if (worker.isCheckedIn) {
|
||||
bg = UiColors.textSuccess.withOpacity(0.1);
|
||||
border = UiColors.textSuccess;
|
||||
textBg = UiColors.textSuccess.withOpacity(0.2);
|
||||
textColor = UiColors.textSuccess;
|
||||
icon = UiIcons.success;
|
||||
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
|
||||
badgeBg = UiColors.textSuccess;
|
||||
badgeText = UiColors.primaryForeground;
|
||||
badgeLabel = 'On Site';
|
||||
} else if (worker.isEnRoute) {
|
||||
bg = UiColors.textWarning.withOpacity(0.1);
|
||||
border = UiColors.textWarning;
|
||||
textBg = UiColors.textWarning.withOpacity(0.2);
|
||||
textColor = UiColors.textWarning;
|
||||
icon = UiIcons.clock;
|
||||
statusText = 'En Route - Expected $shiftStartTime';
|
||||
badgeBg = UiColors.textWarning;
|
||||
badgeText = UiColors.primaryForeground;
|
||||
badgeLabel = 'En Route';
|
||||
} else {
|
||||
bg = UiColors.destructive.withOpacity(0.1);
|
||||
border = UiColors.destructive;
|
||||
textBg = UiColors.destructive.withOpacity(0.2);
|
||||
textColor = UiColors.destructive;
|
||||
icon = UiIcons.warning;
|
||||
statusText = '⚠ Running Late';
|
||||
badgeBg = UiColors.destructive;
|
||||
badgeText = UiColors.destructiveForeground;
|
||||
badgeLabel = 'Late';
|
||||
switch (worker.status) {
|
||||
case CoverageWorkerStatus.checkedIn:
|
||||
bg = UiColors.textSuccess.withOpacity(0.1);
|
||||
border = UiColors.textSuccess;
|
||||
textBg = UiColors.textSuccess.withOpacity(0.2);
|
||||
textColor = UiColors.textSuccess;
|
||||
icon = UiIcons.success;
|
||||
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
|
||||
badgeBg = UiColors.textSuccess;
|
||||
badgeText = UiColors.primaryForeground;
|
||||
badgeLabel = 'On Site';
|
||||
case CoverageWorkerStatus.confirmed:
|
||||
if (worker.checkInTime == null) {
|
||||
bg = UiColors.textWarning.withOpacity(0.1);
|
||||
border = UiColors.textWarning;
|
||||
textBg = UiColors.textWarning.withOpacity(0.2);
|
||||
textColor = UiColors.textWarning;
|
||||
icon = UiIcons.clock;
|
||||
statusText = 'En Route - Expected $shiftStartTime';
|
||||
badgeBg = UiColors.textWarning;
|
||||
badgeText = UiColors.primaryForeground;
|
||||
badgeLabel = 'En Route';
|
||||
} else {
|
||||
bg = UiColors.muted.withOpacity(0.1);
|
||||
border = UiColors.border;
|
||||
textBg = UiColors.muted.withOpacity(0.2);
|
||||
textColor = UiColors.textSecondary;
|
||||
icon = UiIcons.success;
|
||||
statusText = 'Confirmed';
|
||||
badgeBg = UiColors.muted;
|
||||
badgeText = UiColors.textPrimary;
|
||||
badgeLabel = 'Confirmed';
|
||||
}
|
||||
case CoverageWorkerStatus.late:
|
||||
bg = UiColors.destructive.withOpacity(0.1);
|
||||
border = UiColors.destructive;
|
||||
textBg = UiColors.destructive.withOpacity(0.2);
|
||||
textColor = UiColors.destructive;
|
||||
icon = UiIcons.warning;
|
||||
statusText = '⚠ Running Late';
|
||||
badgeBg = UiColors.destructive;
|
||||
badgeText = UiColors.destructiveForeground;
|
||||
badgeLabel = 'Late';
|
||||
case CoverageWorkerStatus.checkedOut:
|
||||
bg = UiColors.muted.withOpacity(0.1);
|
||||
border = UiColors.border;
|
||||
textBg = UiColors.muted.withOpacity(0.2);
|
||||
textColor = UiColors.textSecondary;
|
||||
icon = UiIcons.success;
|
||||
statusText = 'Checked Out';
|
||||
badgeBg = UiColors.muted;
|
||||
badgeText = UiColors.textPrimary;
|
||||
badgeLabel = 'Done';
|
||||
case CoverageWorkerStatus.noShow:
|
||||
bg = UiColors.destructive.withOpacity(0.1);
|
||||
border = UiColors.destructive;
|
||||
textBg = UiColors.destructive.withOpacity(0.2);
|
||||
textColor = UiColors.destructive;
|
||||
icon = UiIcons.warning;
|
||||
statusText = 'No Show';
|
||||
badgeBg = UiColors.destructive;
|
||||
badgeText = UiColors.destructiveForeground;
|
||||
badgeLabel = 'No Show';
|
||||
case CoverageWorkerStatus.completed:
|
||||
bg = UiColors.textSuccess.withOpacity(0.1);
|
||||
border = UiColors.textSuccess;
|
||||
textBg = UiColors.textSuccess.withOpacity(0.2);
|
||||
textColor = UiColors.textSuccess;
|
||||
icon = UiIcons.success;
|
||||
statusText = 'Completed';
|
||||
badgeBg = UiColors.textSuccess;
|
||||
badgeText = UiColors.primaryForeground;
|
||||
badgeLabel = 'Completed';
|
||||
case CoverageWorkerStatus.pending:
|
||||
case CoverageWorkerStatus.accepted:
|
||||
case CoverageWorkerStatus.rejected:
|
||||
bg = UiColors.muted.withOpacity(0.1);
|
||||
border = UiColors.border;
|
||||
textBg = UiColors.muted.withOpacity(0.2);
|
||||
textColor = UiColors.textSecondary;
|
||||
icon = UiIcons.clock;
|
||||
statusText = worker.status.name.toUpperCase();
|
||||
badgeBg = UiColors.muted;
|
||||
badgeText = UiColors.textPrimary;
|
||||
badgeLabel = worker.status.name[0].toUpperCase() +
|
||||
worker.status.name.substring(1);
|
||||
}
|
||||
|
||||
return Container(
|
||||
|
||||
@@ -43,14 +43,11 @@ class ReorderWidget extends StatelessWidget {
|
||||
),
|
||||
if (subtitle != null) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
Text(subtitle!, style: UiTypography.body2r.textSecondary),
|
||||
],
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
SizedBox(
|
||||
height: 140,
|
||||
height: 164,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: recentOrders.length,
|
||||
@@ -67,13 +64,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
border: Border.all(color: UiColors.border, width: 0.6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -129,10 +120,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
Text(
|
||||
i18n.per_hr(
|
||||
amount: order.hourlyRate.toString(),
|
||||
) +
|
||||
' · ${order.hours}h',
|
||||
'${i18n.per_hr(amount: order.hourlyRate.toString())} · ${order.hours}h',
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -145,49 +133,37 @@ class ReorderWidget extends StatelessWidget {
|
||||
_Badge(
|
||||
icon: UiIcons.success,
|
||||
text: order.type,
|
||||
color: const Color(0xFF2563EB),
|
||||
bg: const Color(0xFF2563EB),
|
||||
textColor: UiColors.white,
|
||||
color: UiColors.primary,
|
||||
bg: UiColors.buttonSecondaryStill,
|
||||
textColor: UiColors.primary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_Badge(
|
||||
icon: UiIcons.building,
|
||||
text: '${order.workers}',
|
||||
color: const Color(0xFF334155),
|
||||
bg: const Color(0xFFF1F5F9),
|
||||
textColor: const Color(0xFF334155),
|
||||
color: UiColors.textSecondary,
|
||||
bg: UiColors.buttonSecondaryStill,
|
||||
textColor: UiColors.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 28,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => onReorderPressed(<String, dynamic>{
|
||||
'orderId': order.orderId,
|
||||
'title': order.title,
|
||||
'location': order.location,
|
||||
'hourlyRate': order.hourlyRate,
|
||||
'hours': order.hours,
|
||||
'workers': order.workers,
|
||||
'type': order.type,
|
||||
}),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.white,
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
icon: const Icon(UiIcons.zap, size: 12),
|
||||
label: Text(
|
||||
i18n.reorder_button,
|
||||
style: UiTypography.footnote1m,
|
||||
),
|
||||
),
|
||||
|
||||
UiButton.secondary(
|
||||
size: UiButtonSize.small,
|
||||
text: i18n.reorder_button,
|
||||
leadingIcon: UiIcons.zap,
|
||||
iconSize: 12,
|
||||
fullWidth: true,
|
||||
onPressed: () => onReorderPressed(<String, dynamic>{
|
||||
'orderId': order.orderId,
|
||||
'title': order.title,
|
||||
'location': order.location,
|
||||
'hourlyRate': order.hourlyRate,
|
||||
'hours': order.hours,
|
||||
'workers': order.workers,
|
||||
'type': order.type,
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -35,7 +35,7 @@ class SettingsProfileHeader extends StatelessWidget {
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
|
||||
margin: const EdgeInsets.only(top: UiConstants.space16),
|
||||
margin: const EdgeInsets.only(top: UiConstants.space24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:ui';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -10,6 +9,7 @@ import '../blocs/view_orders_cubit.dart';
|
||||
import '../blocs/view_orders_state.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../widgets/view_order_card.dart';
|
||||
import '../widgets/view_orders_header.dart';
|
||||
import '../navigation/view_orders_navigator.dart';
|
||||
|
||||
/// The main page for viewing client orders.
|
||||
@@ -22,6 +22,7 @@ class ViewOrdersPage extends StatelessWidget {
|
||||
/// Creates a [ViewOrdersPage].
|
||||
const ViewOrdersPage({super.key, this.initialDate});
|
||||
|
||||
/// The initial date to display orders for.
|
||||
final DateTime? initialDate;
|
||||
|
||||
@override
|
||||
@@ -37,7 +38,8 @@ class ViewOrdersPage extends StatelessWidget {
|
||||
class ViewOrdersView extends StatefulWidget {
|
||||
/// Creates a [ViewOrdersView].
|
||||
const ViewOrdersView({super.key, this.initialDate});
|
||||
|
||||
|
||||
/// The initial date to display orders for.
|
||||
final DateTime? initialDate;
|
||||
|
||||
@override
|
||||
@@ -88,376 +90,84 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
// Background Gradient
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[UiColors.bgSecondary, UiColors.white],
|
||||
stops: <double>[0.0, 0.3],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Header + Filter + Calendar (Sticky behavior)
|
||||
ViewOrdersHeader(
|
||||
state: state,
|
||||
calendarDays: calendarDays,
|
||||
),
|
||||
),
|
||||
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// Header + Filter + Calendar (Sticky behavior)
|
||||
_buildHeader(
|
||||
context: context,
|
||||
state: state,
|
||||
calendarDays: calendarDays,
|
||||
),
|
||||
|
||||
// Content List
|
||||
Expanded(
|
||||
child: filteredOrders.isEmpty
|
||||
? _buildEmptyState(context: context, state: state)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space4,
|
||||
UiConstants.space5,
|
||||
100,
|
||||
),
|
||||
children: <Widget>[
|
||||
if (filteredOrders.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space2,
|
||||
),
|
||||
Text(
|
||||
sectionTitle.toUpperCase(),
|
||||
style: UiTypography.titleUppercase2m
|
||||
.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space1,
|
||||
),
|
||||
Text(
|
||||
'(${filteredOrders.length})',
|
||||
style: UiTypography.footnote1r
|
||||
.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...filteredOrders.map(
|
||||
(OrderItem order) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: ViewOrderCard(order: order),
|
||||
),
|
||||
|
||||
// Content List
|
||||
Expanded(
|
||||
child: filteredOrders.isEmpty
|
||||
? _buildEmptyState(context: context, state: state)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space4,
|
||||
UiConstants.space5,
|
||||
100,
|
||||
),
|
||||
children: <Widget>[
|
||||
if (filteredOrders.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space2,
|
||||
),
|
||||
Text(
|
||||
sectionTitle.toUpperCase(),
|
||||
style: UiTypography.titleUppercase2m
|
||||
.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: UiConstants.space1,
|
||||
),
|
||||
Text(
|
||||
'(${filteredOrders.length})',
|
||||
style: UiTypography.footnote1r
|
||||
.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...filteredOrders.map(
|
||||
(OrderItem order) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
child: ViewOrderCard(order: order),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the sticky header section.
|
||||
Widget _buildHeader({
|
||||
required BuildContext context,
|
||||
required ViewOrdersState state,
|
||||
required List<DateTime> calendarDays,
|
||||
}) {
|
||||
return ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xCCFFFFFF), // White with 0.8 alpha
|
||||
border: Border(
|
||||
bottom: BorderSide(color: UiColors.separatorSecondary),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
// Top Bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_view_orders.title,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (state.filteredOrders.isNotEmpty)
|
||||
UiButton.primary(
|
||||
text: t.client_view_orders.post_button,
|
||||
leadingIcon: UiIcons.add,
|
||||
onPressed: () => Modular.to.navigateToCreateOrder(),
|
||||
size: UiButtonSize.small,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
maximumSize: const Size(0, 48),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Filter Tabs
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
_buildFilterTab(
|
||||
context,
|
||||
label: t.client_view_orders.tabs.up_next,
|
||||
isSelected: state.filterTab == 'all',
|
||||
tabId: 'all',
|
||||
count: state.upNextCount,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space6),
|
||||
_buildFilterTab(
|
||||
context,
|
||||
label: t.client_view_orders.tabs.active,
|
||||
isSelected: state.filterTab == 'active',
|
||||
tabId: 'active',
|
||||
count: state.activeCount,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space6),
|
||||
_buildFilterTab(
|
||||
context,
|
||||
label: t.client_view_orders.tabs.completed,
|
||||
isSelected: state.filterTab == 'completed',
|
||||
tabId: 'completed',
|
||||
count: state.completedCount,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Calendar Header controls
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
UiIcons.chevronLeft,
|
||||
size: 20,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
|
||||
context,
|
||||
).updateWeekOffset(-1),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 20,
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMMM yyyy').format(calendarDays.first),
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: 20,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
|
||||
context,
|
||||
).updateWeekOffset(1),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Calendar Grid
|
||||
SizedBox(
|
||||
height: 72,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 7,
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final DateTime date = calendarDays[index];
|
||||
final bool isSelected =
|
||||
state.selectedDate != null &&
|
||||
date.year == state.selectedDate!.year &&
|
||||
date.month == state.selectedDate!.month &&
|
||||
date.day == state.selectedDate!.day;
|
||||
|
||||
// Check if this date has any shifts
|
||||
final String dateStr = DateFormat(
|
||||
'yyyy-MM-dd',
|
||||
).format(date);
|
||||
final bool hasShifts = state.orders.any(
|
||||
(OrderItem s) => s.date == dateStr,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => BlocProvider.of<ViewOrdersCubit>(
|
||||
context,
|
||||
).selectDate(date),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? UiColors.primary : UiColors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? UiColors.primary
|
||||
: UiColors.separatorPrimary,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.primary.withValues(
|
||||
alpha: 0.25,
|
||||
),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('dd').format(date),
|
||||
style: UiTypography.title2b.copyWith(
|
||||
fontSize: 18,
|
||||
color: isSelected
|
||||
? UiColors.white
|
||||
: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: isSelected
|
||||
? UiColors.white.withValues(alpha: 0.8)
|
||||
: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (hasShifts) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? UiColors.white
|
||||
: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a single filter tab.
|
||||
Widget _buildFilterTab(
|
||||
BuildContext context, {
|
||||
required String label,
|
||||
required bool isSelected,
|
||||
required String tabId,
|
||||
int? count,
|
||||
}) {
|
||||
String text = label;
|
||||
if (count != null) {
|
||||
text = '$label ($count)';
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
BlocProvider.of<ViewOrdersCubit>(context).selectFilterTab(tabId),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||
child: Text(
|
||||
text,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: isSelected ? UiColors.primary : UiColors.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: 2,
|
||||
width: isSelected ? 40 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
if (!isSelected) const SizedBox(height: 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the empty state view.
|
||||
Widget _buildEmptyState({
|
||||
required BuildContext context,
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../blocs/view_orders_cubit.dart';
|
||||
|
||||
/// A rich card displaying details of a client order/shift.
|
||||
@@ -325,15 +326,23 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (order.workersNeeded != 0)
|
||||
if (coveragePercent != 100)
|
||||
const Icon(
|
||||
UiIcons.error,
|
||||
size: 16,
|
||||
color: UiColors.textError,
|
||||
),
|
||||
if (coveragePercent == 100)
|
||||
const Icon(
|
||||
UiIcons.checkCircle,
|
||||
size: 16,
|
||||
color: UiColors.textSuccess,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${order.workersNeeded} Workers Needed',
|
||||
coveragePercent == 100
|
||||
? 'All Workers Confirmed'
|
||||
: '${order.workersNeeded} Workers Needed',
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../blocs/view_orders_cubit.dart';
|
||||
|
||||
/// A single filter tab for the View Orders page.
|
||||
///
|
||||
/// Displays a label with an optional count and shows a selection indicator
|
||||
/// when the tab is active.
|
||||
class ViewOrdersFilterTab extends StatelessWidget {
|
||||
/// Creates a [ViewOrdersFilterTab].
|
||||
const ViewOrdersFilterTab({
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.tabId,
|
||||
this.count,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The label text to display.
|
||||
final String label;
|
||||
|
||||
/// Whether this tab is currently selected.
|
||||
final bool isSelected;
|
||||
|
||||
/// The unique identifier for this tab.
|
||||
final String tabId;
|
||||
|
||||
/// Optional count to display next to the label.
|
||||
final int? count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String text = label;
|
||||
if (count != null) {
|
||||
text = '$label ($count)';
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
BlocProvider.of<ViewOrdersCubit>(context).selectFilterTab(tabId),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||
child: Text(
|
||||
text,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: isSelected ? UiColors.primary : UiColors.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: 2,
|
||||
width: isSelected ? 40 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
if (!isSelected) const SizedBox(height: 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import 'dart:ui';
|
||||
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';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/view_orders_cubit.dart';
|
||||
import '../blocs/view_orders_state.dart';
|
||||
import '../navigation/view_orders_navigator.dart';
|
||||
import 'view_orders_filter_tab.dart';
|
||||
|
||||
/// The sticky header section for the View Orders page.
|
||||
///
|
||||
/// This widget contains:
|
||||
/// - Top bar with title and post button
|
||||
/// - Filter tabs (Up Next, Active, Completed)
|
||||
/// - Calendar navigation controls
|
||||
/// - Horizontal calendar grid
|
||||
class ViewOrdersHeader extends StatelessWidget {
|
||||
/// Creates a [ViewOrdersHeader].
|
||||
const ViewOrdersHeader({
|
||||
required this.state,
|
||||
required this.calendarDays,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The current state of the view orders feature.
|
||||
final ViewOrdersState state;
|
||||
|
||||
/// The list of calendar days to display.
|
||||
final List<DateTime> calendarDays;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xCCFFFFFF), // White with 0.8 alpha
|
||||
border: Border(
|
||||
bottom: BorderSide(color: UiColors.separatorSecondary),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
// Top Bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_view_orders.title,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (state.filteredOrders.isNotEmpty)
|
||||
UiButton.primary(
|
||||
text: t.client_view_orders.post_button,
|
||||
leadingIcon: UiIcons.add,
|
||||
onPressed: () => Modular.to.navigateToCreateOrder(),
|
||||
size: UiButtonSize.small,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
maximumSize: const Size(0, 48),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Filter Tabs
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ViewOrdersFilterTab(
|
||||
label: t.client_view_orders.tabs.up_next,
|
||||
isSelected: state.filterTab == 'all',
|
||||
tabId: 'all',
|
||||
count: state.upNextCount,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space6),
|
||||
ViewOrdersFilterTab(
|
||||
label: t.client_view_orders.tabs.active,
|
||||
isSelected: state.filterTab == 'active',
|
||||
tabId: 'active',
|
||||
count: state.activeCount,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space6),
|
||||
ViewOrdersFilterTab(
|
||||
label: t.client_view_orders.tabs.completed,
|
||||
isSelected: state.filterTab == 'completed',
|
||||
tabId: 'completed',
|
||||
count: state.completedCount,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Calendar Header controls
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
UiIcons.chevronLeft,
|
||||
size: 20,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
|
||||
context,
|
||||
).updateWeekOffset(-1),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 20,
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMMM yyyy').format(calendarDays.first),
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: 20,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
|
||||
context,
|
||||
).updateWeekOffset(1),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Calendar Grid
|
||||
SizedBox(
|
||||
height: 72,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 7,
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final DateTime date = calendarDays[index];
|
||||
final bool isSelected =
|
||||
state.selectedDate != null &&
|
||||
date.year == state.selectedDate!.year &&
|
||||
date.month == state.selectedDate!.month &&
|
||||
date.day == state.selectedDate!.day;
|
||||
|
||||
// Check if this date has any shifts
|
||||
final String dateStr = DateFormat(
|
||||
'yyyy-MM-dd',
|
||||
).format(date);
|
||||
final bool hasShifts = state.orders.any(
|
||||
(OrderItem s) => s.date == dateStr,
|
||||
);
|
||||
|
||||
// Check if date is in the past
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime today = DateTime(now.year, now.month, now.day);
|
||||
final DateTime checkDate = DateTime(date.year, date.month, date.day);
|
||||
final bool isPast = checkDate.isBefore(today);
|
||||
|
||||
return Opacity(
|
||||
opacity: isPast && !isSelected ? 0.5 : 1.0,
|
||||
child: GestureDetector(
|
||||
onTap: () => BlocProvider.of<ViewOrdersCubit>(
|
||||
context,
|
||||
).selectDate(date),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? UiColors.primary : UiColors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? UiColors.primary
|
||||
: UiColors.separatorPrimary,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.primary.withValues(
|
||||
alpha: 0.25,
|
||||
),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('dd').format(date),
|
||||
style: UiTypography.title2b.copyWith(
|
||||
fontSize: 18,
|
||||
color: isSelected
|
||||
? UiColors.white
|
||||
: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: isSelected
|
||||
? UiColors.white.withValues(alpha: 0.8)
|
||||
: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (hasShifts) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? UiColors.white
|
||||
: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,14 @@ dependencies:
|
||||
path: ../../../domain
|
||||
krow_core:
|
||||
path: ../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
# UI
|
||||
lucide_icons: ^0.257.0
|
||||
intl: ^0.20.1
|
||||
url_launcher: ^6.3.1
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user