Merge branch 'dev' into feature/centralized-data-error-handling and resolve conflicts
This commit is contained in:
@@ -5,16 +5,14 @@
|
||||
* Break down large widgets into **smaller, reusable widgets**
|
||||
* Add **doc comments** where necessary to improve readability and maintainability
|
||||
* **Remove overly complicated or unnecessary logic** introduced by AI and simplify where possible
|
||||
* **Adhere to the design system** and remove all **hard-coded colors and typography**, using shared tokens instead
|
||||
|
||||
* Improvement points
|
||||
- apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart
|
||||
- Fix the location field in CoverageShiftRole to use the correct fallback logic.
|
||||
- line 125 remove redundant location values.
|
||||
|
||||
- Need to clarify the difference b/w `case dc.ApplicationStatus.ACCEPTED` and `case dc.ApplicationStatus.CONFIRMED`.
|
||||
- Update the dataconnect docs.
|
||||
- Track `lat` and `lng` in the staff preferred work locations (for now we are only storing the name).
|
||||
- Change the name of the dataconnect connector replacing the "ExampleConnecter" with "KrowConnecter"
|
||||
|
||||
- ` final String status;` in `OrderItem` make it an enum.
|
||||
- /// Date of the shift (ISO format).
|
||||
@@ -23,20 +21,20 @@
|
||||
- in `view_orders_cubit.dart` combine the logic of `_calculateUpNextCount ` and `_calculateTodayCount` into a single function that calculates both counts together to avoid redundant filtering of orders.
|
||||
- In places api call in the when the api's not working we need to show a proper error message instead of just an empty list.
|
||||
- pending should come first in the view order list.
|
||||
- rename connect name to 'krow-connect' in the project.
|
||||
|
||||
- fix "dartdepend_on_referenced_packages" warnings.
|
||||
- fix "dartunnecessary_library_name" warnings.
|
||||
- fix lint issues.
|
||||
- fix localization.
|
||||
- centralise way to handle errors.
|
||||
|
||||
|
||||
|
||||
|
||||
- track minimum shift hours in the staff profile and show a warning if they try to apply for shifts that are below their minimum hours.
|
||||
- this need to be added in the BE and also a FE validation (5 hrs).
|
||||
|
||||
- Cannot cancel before 24 hours of the shift start time. If do we should charge for 4 hours of work for each shifts.
|
||||
|
||||
- verify the order creation process in the client app.
|
||||
- Vendor don't need to verify the order, when the order is created it should be automatically published.
|
||||
- rethink the order status, we need to simplify it.
|
||||
|
||||
- Validation layer
|
||||
- Profile info
|
||||
- emergency contact
|
||||
@@ -48,4 +46,9 @@
|
||||
- there should be manual verification by the client even if the ai verification is passed.
|
||||
- to track false positives and false negatives.
|
||||
- documents
|
||||
- tax forms
|
||||
- tax forms
|
||||
|
||||
- How do we handle the current bank account verifcaiton in the current legacy application.
|
||||
- We need have a show a list of clothing items in the staff app -> shift page.
|
||||
- Template models for the pdf reports in the client and web apps.
|
||||
- remove `any` type and replace it with the correct types in the codebase.
|
||||
@@ -1,7 +1,7 @@
|
||||
name: krowwithus_client
|
||||
description: "Krow Client Application"
|
||||
publish_to: "none"
|
||||
version: 0.0.1-M3+5
|
||||
version: 0.0.1-IlianaClientM3
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: krowwithus_staff
|
||||
description: "Krow Staff Application"
|
||||
publish_to: 'none'
|
||||
version: 0.0.1-M3+3
|
||||
version: 0.0.1-IlianaStaffM3
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
|
||||
@@ -10,5 +10,5 @@ export 'src/widgets/ui_step_indicator.dart';
|
||||
export 'src/widgets/ui_icon_button.dart';
|
||||
export 'src/widgets/ui_button.dart';
|
||||
export 'src/widgets/ui_chip.dart';
|
||||
export 'src/widgets/ui_error_snackbar.dart';
|
||||
export 'src/widgets/ui_success_snackbar.dart';
|
||||
export 'src/widgets/ui_loading_page.dart';
|
||||
export 'src/widgets/ui_snackbar.dart';
|
||||
|
||||
@@ -120,7 +120,7 @@ class UiColors {
|
||||
static const Color textDescription = mutedForeground;
|
||||
|
||||
/// Success text (#10B981)
|
||||
static const Color textSuccess = Color(0xFF10B981);
|
||||
static const Color textSuccess = Color(0xFF0A8159);
|
||||
|
||||
/// Error text (#F04444)
|
||||
static const Color textError = destructive;
|
||||
|
||||
@@ -225,4 +225,28 @@ class UiIcons {
|
||||
|
||||
/// Globe icon
|
||||
static const IconData globe = _IconLib.globe;
|
||||
|
||||
/// Sunrise icon
|
||||
static const IconData sunrise = _IconLib.sunrise;
|
||||
|
||||
/// Sun icon
|
||||
static const IconData sun = _IconLib.sun;
|
||||
|
||||
/// Moon icon
|
||||
static const IconData moon = _IconLib.moon;
|
||||
|
||||
/// Timer icon
|
||||
static const IconData timer = _IconLib.timer;
|
||||
|
||||
/// Coffee icon for breaks
|
||||
static const IconData coffee = _IconLib.coffee;
|
||||
|
||||
/// Wifi icon for NFC check-in
|
||||
static const IconData wifi = _IconLib.wifi;
|
||||
|
||||
/// X Circle icon for no-shows
|
||||
static const IconData xCircle = _IconLib.xCircle;
|
||||
|
||||
/// Ban icon for cancellations
|
||||
static const IconData ban = _IconLib.ban;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ class UiTheme {
|
||||
}
|
||||
return UiColors.switchInactive;
|
||||
}),
|
||||
thumbColor: const WidgetStatePropertyAll(UiColors.white),
|
||||
thumbColor: const WidgetStatePropertyAll<Color>(UiColors.white),
|
||||
),
|
||||
|
||||
// Checkbox Theme
|
||||
|
||||
@@ -579,4 +579,10 @@ extension TypographyColors on TextStyle {
|
||||
|
||||
/// Active content color
|
||||
TextStyle get activeContentColor => copyWith(color: UiColors.textPrimary);
|
||||
|
||||
/// Primary color
|
||||
TextStyle get primary => copyWith(color: UiColors.primary);
|
||||
|
||||
/// Accent color
|
||||
TextStyle get accent => copyWith(color: UiColors.accent);
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import '../ui_colors.dart';
|
||||
import '../ui_typography.dart';
|
||||
|
||||
/// Centralized error snackbar for consistent error presentation across the app.
|
||||
///
|
||||
/// This widget automatically resolves localization keys and displays
|
||||
/// user-friendly error messages with optional error codes for support.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UiErrorSnackbar.show(
|
||||
/// context,
|
||||
/// messageKey: 'errors.auth.invalid_credentials',
|
||||
/// errorCode: 'AUTH_001',
|
||||
/// );
|
||||
/// ```
|
||||
class UiErrorSnackbar {
|
||||
/// Shows an error snackbar with a localized message.
|
||||
///
|
||||
/// [messageKey] should be a dot-separated path like 'errors.auth.invalid_credentials'
|
||||
/// [errorCode] is optional and will be shown in smaller text for support reference
|
||||
/// [duration] controls how long the snackbar is visible
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String messageKey,
|
||||
String? errorCode,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
// 1. Added explicit type 'Translations' to satisfy the lint
|
||||
final Translations texts = Translations.of(context);
|
||||
final String message = _getMessageFromKey(texts, messageKey);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: UiColors.white),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
message,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.white),
|
||||
),
|
||||
if (errorCode != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Error Code: $errorCode',
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
// 3. Fixed deprecated member use
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: UiColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: duration,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolves a localization key path to the actual translated message.
|
||||
///
|
||||
/// Supports keys like:
|
||||
/// - errors.auth.invalid_credentials
|
||||
/// - errors.hub.has_orders
|
||||
/// - errors.generic.unknown
|
||||
static String _getMessageFromKey(Translations texts, String key) {
|
||||
// Parse key like "errors.auth.invalid_credentials"
|
||||
final parts = key.split('.');
|
||||
if (parts.length < 2) return texts.errors.generic.unknown;
|
||||
|
||||
try {
|
||||
switch (parts[1]) {
|
||||
case 'auth':
|
||||
return _getAuthError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'hub':
|
||||
return _getHubError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'order':
|
||||
return _getOrderError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'profile':
|
||||
return _getProfileError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'shift':
|
||||
return _getShiftError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'generic':
|
||||
return _getGenericError(texts, parts.length > 2 ? parts[2] : '');
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
} catch (_) {
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getAuthError(Translations texts, String key) {
|
||||
switch (key) {
|
||||
case 'invalid_credentials':
|
||||
return texts.errors.auth.invalid_credentials;
|
||||
case 'account_exists':
|
||||
return texts.errors.auth.account_exists;
|
||||
case 'session_expired':
|
||||
return texts.errors.auth.session_expired;
|
||||
case 'user_not_found':
|
||||
return texts.errors.auth.user_not_found;
|
||||
case 'unauthorized_app':
|
||||
return texts.errors.auth.unauthorized_app;
|
||||
case 'weak_password':
|
||||
return texts.errors.auth.weak_password;
|
||||
case 'sign_up_failed':
|
||||
return texts.errors.auth.sign_up_failed;
|
||||
case 'sign_in_failed':
|
||||
return texts.errors.auth.sign_in_failed;
|
||||
case 'not_authenticated':
|
||||
return texts.errors.auth.not_authenticated;
|
||||
case 'password_mismatch':
|
||||
return texts.errors.auth.password_mismatch;
|
||||
case 'google_only_account':
|
||||
return texts.errors.auth.google_only_account;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getHubError(Translations texts, String key) {
|
||||
switch (key) {
|
||||
case 'has_orders':
|
||||
return texts.errors.hub.has_orders;
|
||||
case 'not_found':
|
||||
return texts.errors.hub.not_found;
|
||||
case 'creation_failed':
|
||||
return texts.errors.hub.creation_failed;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getOrderError(Translations texts, String key) {
|
||||
switch (key) {
|
||||
case 'missing_hub':
|
||||
return texts.errors.order.missing_hub;
|
||||
case 'missing_vendor':
|
||||
return texts.errors.order.missing_vendor;
|
||||
case 'creation_failed':
|
||||
return texts.errors.order.creation_failed;
|
||||
case 'shift_creation_failed':
|
||||
return texts.errors.order.shift_creation_failed;
|
||||
case 'missing_business':
|
||||
return texts.errors.order.missing_business;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getProfileError(Translations texts, String key) {
|
||||
switch (key) {
|
||||
case 'staff_not_found':
|
||||
return texts.errors.profile.staff_not_found;
|
||||
case 'business_not_found':
|
||||
return texts.errors.profile.business_not_found;
|
||||
case 'update_failed':
|
||||
return texts.errors.profile.update_failed;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getShiftError(Translations texts, String key) {
|
||||
switch (key) {
|
||||
case 'no_open_roles':
|
||||
return texts.errors.shift.no_open_roles;
|
||||
case 'application_not_found':
|
||||
return texts.errors.shift.application_not_found;
|
||||
case 'no_active_shift':
|
||||
return texts.errors.shift.no_active_shift;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getGenericError(Translations texts, String key) {
|
||||
switch (key) {
|
||||
case 'unknown':
|
||||
return texts.errors.generic.unknown;
|
||||
case 'no_connection':
|
||||
return texts.errors.generic.no_connection;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../ui_colors.dart';
|
||||
|
||||
/// A common loading page that adheres to the design system.
|
||||
/// It features a blurred background using [UiColors.popupShadow] and a [CircularProgressIndicator].
|
||||
class UiLoadingPage extends StatelessWidget {
|
||||
/// Creates a [UiLoadingPage].
|
||||
const UiLoadingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.transparent,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
// Background blur and color
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Container(color: UiColors.popupShadow),
|
||||
),
|
||||
),
|
||||
// Center loading indicator
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../ui_colors.dart';
|
||||
import '../ui_icons.dart';
|
||||
import '../ui_typography.dart';
|
||||
|
||||
/// Types of snackbars available in the design system.
|
||||
enum UiSnackbarType {
|
||||
/// Success state - green text and light blurred green background.
|
||||
success,
|
||||
|
||||
/// Message state - blue text and light blurred blue background.
|
||||
message,
|
||||
|
||||
/// Warning state - dark yellow text and light blurred yellow background.
|
||||
warning,
|
||||
|
||||
/// Error state - red text and light blurred red background.
|
||||
error,
|
||||
}
|
||||
|
||||
/// A centralized snackbar widget that adheres to the design system with glassmorphism effects.
|
||||
class UiSnackbar {
|
||||
UiSnackbar._();
|
||||
|
||||
/// Shows a snackbar with the specified [message] and [type].
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
required UiSnackbarType type,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
final Color textColor;
|
||||
final Color backgroundColor;
|
||||
final IconData icon;
|
||||
|
||||
switch (type) {
|
||||
case UiSnackbarType.success:
|
||||
textColor = UiColors.textSuccess;
|
||||
backgroundColor = UiColors.tagSuccess.withValues(alpha: 0.7);
|
||||
icon = UiIcons.success;
|
||||
break;
|
||||
case UiSnackbarType.message:
|
||||
textColor = UiColors.primary;
|
||||
backgroundColor = UiColors.tagInProgress.withValues(alpha: 0.7);
|
||||
icon = UiIcons.info;
|
||||
break;
|
||||
case UiSnackbarType.warning:
|
||||
textColor = UiColors.textWarning;
|
||||
backgroundColor = UiColors.tagPending.withValues(alpha: 0.7);
|
||||
icon = UiIcons.warning;
|
||||
break;
|
||||
case UiSnackbarType.error:
|
||||
textColor = UiColors.textError;
|
||||
backgroundColor = UiColors.tagError.withValues(alpha: 0.7);
|
||||
icon = UiIcons.error;
|
||||
break;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: duration,
|
||||
backgroundColor: UiColors.transparent,
|
||||
elevation: 0,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: textColor.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: <Widget>[
|
||||
Icon(icon, color: textColor, size: 20),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2b.copyWith(color: textColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,6 @@ import '../ui_icons.dart';
|
||||
/// This widget shows a series of circular step indicators connected by lines,
|
||||
/// with different visual states for completed, active, and inactive steps.
|
||||
class UiStepIndicator extends StatelessWidget {
|
||||
/// The list of icons to display for each step.
|
||||
final List<IconData> stepIcons;
|
||||
|
||||
/// The index of the currently active step (0-based).
|
||||
final int currentStep;
|
||||
|
||||
/// Creates a [UiStepIndicator].
|
||||
const UiStepIndicator({
|
||||
super.key,
|
||||
@@ -21,6 +15,12 @@ class UiStepIndicator extends StatelessWidget {
|
||||
required this.currentStep,
|
||||
});
|
||||
|
||||
/// The list of icons to display for each step.
|
||||
final List<IconData> stepIcons;
|
||||
|
||||
/// The index of the currently active step (0-based).
|
||||
final int currentStep;
|
||||
|
||||
@override
|
||||
/// Builds the step indicator UI.
|
||||
Widget build(BuildContext context) {
|
||||
@@ -35,7 +35,7 @@ class UiStepIndicator extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(stepIcons.length, (int index) {
|
||||
children: List<Widget>.generate(stepIcons.length, (int index) {
|
||||
final bool isActive = index == currentStep;
|
||||
final bool isCompleted = index < currentStep;
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../ui_colors.dart';
|
||||
import '../ui_typography.dart';
|
||||
|
||||
/// Centralized success snackbar for consistent success message presentation.
|
||||
///
|
||||
/// This widget provides a unified way to show success feedback across the app
|
||||
/// with consistent styling and behavior.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UiSuccessSnackbar.show(
|
||||
/// context,
|
||||
/// message: 'Profile updated successfully!',
|
||||
/// );
|
||||
/// ```
|
||||
class UiSuccessSnackbar {
|
||||
/// Shows a success snackbar with a custom message.
|
||||
///
|
||||
/// [message] is the success message to display
|
||||
/// [duration] controls how long the snackbar is visible
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle_outline, color: UiColors.white),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: UiColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: duration,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ class TimeCardAdapter {
|
||||
case 'DISPUTED':
|
||||
return TimeCardStatus.disputed;
|
||||
case 'CHECKED_IN':
|
||||
case 'ACCEPTED':
|
||||
case 'CONFIRMED':
|
||||
default:
|
||||
return TimeCardStatus.pending;
|
||||
|
||||
@@ -12,8 +12,7 @@ import 'package:krow_domain/krow_domain.dart'
|
||||
AccountExistsException,
|
||||
UserNotFoundException,
|
||||
UnauthorizedAppException,
|
||||
PasswordMismatchException,
|
||||
GoogleOnlyAccountException;
|
||||
PasswordMismatchException;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
|
||||
import '../../domain/repositories/auth_repository_interface.dart';
|
||||
|
||||
@@ -177,8 +177,6 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
switch (status.value) {
|
||||
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:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Feature Manifest: Staff Authentication
|
||||
|
||||
## Overview
|
||||
**Feature Name:** Staff Authentication & Onboarding
|
||||
**Package Path:** `packages/features/staff/authentication`
|
||||
|
||||
## Responsibilities
|
||||
* Handle user sign-up and log-in via Phone Auth.
|
||||
* Verify OTP codes.
|
||||
* Manage the Onboarding Wizard for new staff.
|
||||
* Persist onboarding progress.
|
||||
|
||||
## Architecture
|
||||
* **Domain**:
|
||||
* `AuthRepositoryInterface`
|
||||
* `SignInWithPhoneUseCase`
|
||||
* `VerifyOtpUseCase`
|
||||
* **Data**:
|
||||
* `AuthRepositoryImpl` (uses `AuthRepositoryMock` from `krow_data_connect`)
|
||||
* **Presentation**:
|
||||
* `AuthBloc`: Manages auth state (phone, otp, user status).
|
||||
* `OnboardingBloc`: Manages wizard steps.
|
||||
* Pages: `GetStartedPage`, `PhoneVerificationPage`, `ProfileSetupPage`.
|
||||
|
||||
## Dependencies
|
||||
* `krow_domain`: User entities.
|
||||
* `krow_data_connect`: Auth mocks.
|
||||
* `design_system`: UI components.
|
||||
|
||||
## Routes
|
||||
* `/`: Get Started (Welcome)
|
||||
* `/phone-verification`: OTP Entry
|
||||
* `/profile-setup`: Onboarding Wizard
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
@@ -36,7 +37,7 @@ class GetStartedPage extends StatelessWidget {
|
||||
|
||||
// Content Overlay
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -44,7 +45,7 @@ class GetStartedPage extends StatelessWidget {
|
||||
// Main text and actions
|
||||
const GetStartedHeader(),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
const SizedBox(height: UiConstants.space10),
|
||||
|
||||
// Actions
|
||||
GetStartedActions(
|
||||
@@ -52,7 +53,7 @@ class GetStartedPage extends StatelessWidget {
|
||||
onLoginPressed: onLoginPressed,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
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:krow_core/core.dart';
|
||||
|
||||
import '../blocs/profile_setup/profile_setup_bloc.dart';
|
||||
import '../widgets/profile_setup_page/profile_setup_basic_info.dart';
|
||||
import '../widgets/profile_setup_page/profile_setup_location.dart';
|
||||
import '../widgets/profile_setup_page/profile_setup_experience.dart';
|
||||
import '../widgets/profile_setup_page/profile_setup_header.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../widgets/profile_setup_page/profile_setup_location.dart';
|
||||
|
||||
/// Page for setting up the user profile after authentication.
|
||||
class ProfileSetupPage extends StatefulWidget {
|
||||
@@ -106,7 +107,8 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, ProfileSetupState state) {
|
||||
final bool isCreatingProfile = state.status == ProfileSetupStatus.loading;
|
||||
final bool isCreatingProfile =
|
||||
state.status == ProfileSetupStatus.loading;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
@@ -125,7 +127,10 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||
// Step Indicators
|
||||
UiStepIndicator(
|
||||
stepIcons: steps
|
||||
.map((Map<String, dynamic> step) => step['icon'] as IconData)
|
||||
.map(
|
||||
(Map<String, dynamic> step) =>
|
||||
step['icon'] as IconData,
|
||||
)
|
||||
.toList(),
|
||||
currentStep: _currentStep,
|
||||
),
|
||||
@@ -211,9 +216,10 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||
return ProfileSetupLocation(
|
||||
preferredLocations: state.preferredLocations,
|
||||
maxDistanceMiles: state.maxDistanceMiles,
|
||||
onLocationsChanged: (List<String> val) => BlocProvider.of<ProfileSetupBloc>(
|
||||
context,
|
||||
).add(ProfileSetupLocationsChanged(val)),
|
||||
onLocationsChanged: (List<String> val) =>
|
||||
BlocProvider.of<ProfileSetupBloc>(
|
||||
context,
|
||||
).add(ProfileSetupLocationsChanged(val)),
|
||||
onDistanceChanged: (double val) => BlocProvider.of<ProfileSetupBloc>(
|
||||
context,
|
||||
).add(ProfileSetupDistanceChanged(val)),
|
||||
@@ -222,12 +228,14 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||
return ProfileSetupExperience(
|
||||
skills: state.skills,
|
||||
industries: state.industries,
|
||||
onSkillsChanged: (List<String> val) => BlocProvider.of<ProfileSetupBloc>(
|
||||
context,
|
||||
).add(ProfileSetupSkillsChanged(val)),
|
||||
onIndustriesChanged: (List<String> val) => BlocProvider.of<ProfileSetupBloc>(
|
||||
context,
|
||||
).add(ProfileSetupIndustriesChanged(val)),
|
||||
onSkillsChanged: (List<String> val) =>
|
||||
BlocProvider.of<ProfileSetupBloc>(
|
||||
context,
|
||||
).add(ProfileSetupSkillsChanged(val)),
|
||||
onIndustriesChanged: (List<String> val) =>
|
||||
BlocProvider.of<ProfileSetupBloc>(
|
||||
context,
|
||||
).add(ProfileSetupIndustriesChanged(val)),
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A common widget that displays a "Having trouble? Contact Support" link.
|
||||
class AuthTroubleLink extends StatelessWidget {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that displays the welcome text and description on the Get Started page.
|
||||
class GetStartedHeader extends StatelessWidget {
|
||||
@@ -20,9 +20,7 @@ class GetStartedHeader extends StatelessWidget {
|
||||
text: TextSpan(
|
||||
style: UiTypography.displayM,
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: i18n.title_part1,
|
||||
),
|
||||
TextSpan(text: i18n.title_part1),
|
||||
TextSpan(
|
||||
text: i18n.title_part2,
|
||||
style: UiTypography.displayMb.textLink,
|
||||
@@ -39,4 +37,4 @@ class GetStartedHeader extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget that handles the OTP resend logic and countdown timer.
|
||||
class OtpResendSection extends StatefulWidget {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
import '../../common/auth_trouble_link.dart';
|
||||
|
||||
/// A widget that displays the primary action button and trouble link for OTP verification.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget that displays the title and subtitle for the OTP Verification page.
|
||||
class OtpVerificationHeader extends StatelessWidget {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/src/presentation/widgets/common/auth_trouble_link.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget that displays the primary action button and trouble link for Phone Input.
|
||||
class PhoneInputActions extends StatelessWidget {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget that displays the title and subtitle for the Phone Input page.
|
||||
class PhoneInputHeader extends StatelessWidget {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/src/presentation/widgets/common/section_title_subtitle.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget for setting up basic profile information (photo, name, bio).
|
||||
class ProfileSetupBasicInfo extends StatelessWidget {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_authentication/src/presentation/widgets/common/section_title_subtitle.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget for setting up skills and preferred industries.
|
||||
class ProfileSetupExperience extends StatelessWidget {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A header widget for the profile setup page showing back button and step count.
|
||||
class ProfileSetupHeader extends StatelessWidget {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
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:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
|
||||
import 'package:staff_authentication/src/presentation/widgets/common/section_title_subtitle.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget for setting up preferred work locations and distance.
|
||||
class ProfileSetupLocation extends StatefulWidget {
|
||||
@@ -47,22 +48,23 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
||||
void _onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 300), () {
|
||||
context
|
||||
.read<ProfileSetupBloc>()
|
||||
.add(ProfileSetupLocationQueryChanged(query));
|
||||
context.read<ProfileSetupBloc>().add(
|
||||
ProfileSetupLocationQueryChanged(query),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds the selected location.
|
||||
void _addLocation(String location) {
|
||||
if (location.isNotEmpty && !widget.preferredLocations.contains(location)) {
|
||||
final List<String> updatedList =
|
||||
List<String>.from(widget.preferredLocations)..add(location);
|
||||
final List<String> updatedList = List<String>.from(
|
||||
widget.preferredLocations,
|
||||
)..add(location);
|
||||
widget.onLocationsChanged(updatedList);
|
||||
_locationController.clear();
|
||||
context
|
||||
.read<ProfileSetupBloc>()
|
||||
.add(const ProfileSetupClearLocationSuggestions());
|
||||
context.read<ProfileSetupBloc>().add(
|
||||
const ProfileSetupClearLocationSuggestions(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +81,16 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
||||
|
||||
// Search Input
|
||||
UiTextField(
|
||||
label: t.staff_authentication.profile_setup_page.location
|
||||
label: t
|
||||
.staff_authentication
|
||||
.profile_setup_page
|
||||
.location
|
||||
.add_location_label,
|
||||
controller: _locationController,
|
||||
hintText: t.staff_authentication.profile_setup_page.location
|
||||
hintText: t
|
||||
.staff_authentication
|
||||
.profile_setup_page
|
||||
.location
|
||||
.add_location_hint,
|
||||
onChanged: _onSearchChanged,
|
||||
),
|
||||
@@ -99,15 +107,8 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
margin: const EdgeInsets.only(top: UiConstants.space2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
color: UiColors.cardViewBackground,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
@@ -167,12 +168,18 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_authentication.profile_setup_page.location
|
||||
t
|
||||
.staff_authentication
|
||||
.profile_setup_page
|
||||
.location
|
||||
.min_dist_label,
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
t.staff_authentication.profile_setup_page.location
|
||||
t
|
||||
.staff_authentication
|
||||
.profile_setup_page
|
||||
.location
|
||||
.max_dist_label,
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
@@ -185,8 +192,9 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
||||
|
||||
/// Removes the specified [location] from the list.
|
||||
void _removeLocation({required String location}) {
|
||||
final List<String> updatedList =
|
||||
List<String>.from(widget.preferredLocations)..remove(location);
|
||||
final List<String> updatedList = List<String>.from(
|
||||
widget.preferredLocations,
|
||||
)..remove(location);
|
||||
widget.onLocationsChanged(updatedList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
import 'package:staff_authentication/src/data/repositories_impl/auth_repository_impl.dart';
|
||||
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/verify_otp_usecase.dart';
|
||||
import 'package:staff_authentication/src/domain/repositories/profile_setup_repository.dart';
|
||||
import 'package:staff_authentication/src/data/repositories_impl/profile_setup_repository_impl.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/submit_profile_setup_usecase.dart';
|
||||
import 'package:staff_authentication/src/domain/repositories/place_repository.dart';
|
||||
import 'package:staff_authentication/src/data/repositories_impl/place_repository_impl.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/search_cities_usecase.dart';
|
||||
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
|
||||
import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
|
||||
import 'package:staff_authentication/src/presentation/pages/get_started_page.dart';
|
||||
import 'package:staff_authentication/src/presentation/pages/phone_verification_page.dart';
|
||||
import 'package:staff_authentication/src/presentation/pages/profile_setup_page.dart';
|
||||
import 'package:staff_authentication/src/domain/ui_entities/auth_mode.dart';
|
||||
|
||||
/// A [Module] for the staff authentication feature.
|
||||
class StaffAuthenticationModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[DataConnectModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<AuthRepositoryInterface>(
|
||||
() => AuthRepositoryImpl(
|
||||
firebaseAuth: firebase.FirebaseAuth.instance,
|
||||
dataConnect: ExampleConnector.instance,
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<ProfileSetupRepository>(
|
||||
() => ProfileSetupRepositoryImpl(
|
||||
firebaseAuth: firebase.FirebaseAuth.instance,
|
||||
dataConnect: ExampleConnector.instance,
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<PlaceRepository>(PlaceRepositoryImpl.new);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(SignInWithPhoneUseCase.new);
|
||||
i.addLazySingleton(VerifyOtpUseCase.new);
|
||||
i.addLazySingleton(SubmitProfileSetup.new);
|
||||
i.addLazySingleton(SearchCitiesUseCase.new);
|
||||
|
||||
// BLoCs
|
||||
i.addLazySingleton<AuthBloc>(
|
||||
() => AuthBloc(
|
||||
signInUseCase: i.get<SignInWithPhoneUseCase>(),
|
||||
verifyOtpUseCase: i.get<VerifyOtpUseCase>(),
|
||||
),
|
||||
);
|
||||
i.add<ProfileSetupBloc>(
|
||||
() => ProfileSetupBloc(
|
||||
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
||||
searchCities: i.get<SearchCitiesUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child(StaffPaths.root, child: (_) => const GetStartedPage());
|
||||
r.child(
|
||||
StaffPaths.phoneVerification,
|
||||
child: (BuildContext context) {
|
||||
final Map<String, dynamic>? data = r.args.data;
|
||||
final String? modeName = data?['mode'];
|
||||
final AuthMode mode = AuthMode.values.firstWhere(
|
||||
(AuthMode e) => e.name == modeName,
|
||||
orElse: () => AuthMode.login,
|
||||
);
|
||||
return PhoneVerificationPage(mode: mode);
|
||||
},
|
||||
);
|
||||
r.child(StaffPaths.profileSetup, child: (_) => const ProfileSetupPage());
|
||||
}
|
||||
}
|
||||
@@ -1,91 +1,5 @@
|
||||
library staff_authentication;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
import 'package:staff_authentication/src/data/repositories_impl/auth_repository_impl.dart';
|
||||
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/verify_otp_usecase.dart';
|
||||
import 'package:staff_authentication/src/domain/repositories/profile_setup_repository.dart';
|
||||
import 'package:staff_authentication/src/data/repositories_impl/profile_setup_repository_impl.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/submit_profile_setup_usecase.dart';
|
||||
import 'package:staff_authentication/src/domain/repositories/place_repository.dart';
|
||||
import 'package:staff_authentication/src/data/repositories_impl/place_repository_impl.dart';
|
||||
import 'package:staff_authentication/src/domain/usecases/search_cities_usecase.dart';
|
||||
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
|
||||
import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
|
||||
import 'package:staff_authentication/src/presentation/pages/get_started_page.dart';
|
||||
import 'package:staff_authentication/src/presentation/pages/phone_verification_page.dart';
|
||||
import 'package:staff_authentication/src/presentation/pages/profile_setup_page.dart';
|
||||
import 'package:staff_authentication/src/domain/ui_entities/auth_mode.dart';
|
||||
|
||||
export 'src/domain/ui_entities/auth_mode.dart';
|
||||
export 'src/presentation/pages/get_started_page.dart';
|
||||
export 'src/presentation/pages/phone_verification_page.dart';
|
||||
export 'src/presentation/pages/profile_setup_page.dart';
|
||||
export 'package:core_localization/core_localization.dart';
|
||||
|
||||
/// A [Module] for the staff authentication feature.
|
||||
class StaffAuthenticationModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[DataConnectModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<AuthRepositoryInterface>(
|
||||
() => AuthRepositoryImpl(
|
||||
firebaseAuth: firebase.FirebaseAuth.instance,
|
||||
dataConnect: ExampleConnector.instance,
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<ProfileSetupRepository>(
|
||||
() => ProfileSetupRepositoryImpl(
|
||||
firebaseAuth: firebase.FirebaseAuth.instance,
|
||||
dataConnect: ExampleConnector.instance,
|
||||
),
|
||||
);
|
||||
i.addLazySingleton<PlaceRepository>(PlaceRepositoryImpl.new);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(SignInWithPhoneUseCase.new);
|
||||
i.addLazySingleton(VerifyOtpUseCase.new);
|
||||
i.addLazySingleton(SubmitProfileSetup.new);
|
||||
i.addLazySingleton(SearchCitiesUseCase.new);
|
||||
|
||||
// BLoCs
|
||||
i.addLazySingleton<AuthBloc>(
|
||||
() => AuthBloc(
|
||||
signInUseCase: i.get<SignInWithPhoneUseCase>(),
|
||||
verifyOtpUseCase: i.get<VerifyOtpUseCase>(),
|
||||
),
|
||||
);
|
||||
i.add<ProfileSetupBloc>(
|
||||
() => ProfileSetupBloc(
|
||||
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
||||
searchCities: i.get<SearchCitiesUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child(StaffPaths.root, child: (_) => const GetStartedPage());
|
||||
r.child(
|
||||
StaffPaths.phoneVerification,
|
||||
child: (BuildContext context) {
|
||||
final Map<String, dynamic>? data = r.args.data;
|
||||
final String? modeName = data?['mode'];
|
||||
final AuthMode mode = AuthMode.values.firstWhere(
|
||||
(AuthMode e) => e.name == modeName,
|
||||
orElse: () => AuthMode.login,
|
||||
);
|
||||
return PhoneVerificationPage(mode: mode);
|
||||
},
|
||||
);
|
||||
r.child(StaffPaths.profileSetup, child: (_) => const ProfileSetupPage());
|
||||
}
|
||||
}
|
||||
export 'src/staff_authentication_module.dart';
|
||||
|
||||
@@ -14,9 +14,8 @@ dependencies:
|
||||
flutter_bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
lucide_icons: ^0.257.0
|
||||
firebase_core: ^4.2.1
|
||||
firebase_auth: ^6.1.2 # Updated for compatibility
|
||||
firebase_auth: ^6.1.2
|
||||
firebase_data_connect: ^0.2.2+1
|
||||
http: ^1.2.0
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
import '../bloc/clock_in_bloc.dart';
|
||||
import '../bloc/clock_in_event.dart';
|
||||
import '../bloc/clock_in_state.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../widgets/commute_tracker.dart';
|
||||
import '../widgets/date_selector.dart';
|
||||
import '../widgets/lunch_break_modal.dart';
|
||||
@@ -40,10 +38,10 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
listener: (BuildContext context, ClockInState state) {
|
||||
if (state.status == ClockInStatus.failure &&
|
||||
state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(content: Text(translateErrorKey(state.errorMessage!))),
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -67,14 +65,6 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
final bool isCheckedIn =
|
||||
state.attendance.isCheckedIn && isActiveSelected;
|
||||
|
||||
// Format times for display
|
||||
final String checkInStr = checkInTime != null
|
||||
? DateFormat('h:mm a').format(checkInTime)
|
||||
: '--:-- --';
|
||||
final String checkOutStr = checkOutTime != null
|
||||
? DateFormat('h:mm a').format(checkOutTime)
|
||||
: '--:-- --';
|
||||
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
titleWidget: Text(
|
||||
@@ -94,7 +84,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -113,26 +105,22 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
// Date Selector
|
||||
DateSelector(
|
||||
selectedDate: state.selectedDate,
|
||||
onSelect: (DateTime date) => _bloc.add(DateSelected(date)),
|
||||
onSelect: (DateTime date) =>
|
||||
_bloc.add(DateSelected(date)),
|
||||
shiftDates: <String>[
|
||||
DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
|
||||
// Your Activity Header
|
||||
const Text(
|
||||
Text(
|
||||
"Your Activity",
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Selected Shift Info Card
|
||||
if (todayShifts.isNotEmpty)
|
||||
@@ -143,19 +131,21 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
onTap: () =>
|
||||
_bloc.add(ShiftSelected(shift)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin:
|
||||
const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(
|
||||
UiConstants.space3,
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
color: UiColors.white,
|
||||
borderRadius:
|
||||
UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: shift.id ==
|
||||
selectedShift?.id
|
||||
? AppColors.krowBlue
|
||||
: const Color(0xFFE2E8F0),
|
||||
? UiColors.primary
|
||||
: UiColors.border,
|
||||
width:
|
||||
shift.id == selectedShift?.id
|
||||
? 2
|
||||
@@ -176,34 +166,25 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
selectedShift?.id
|
||||
? "SELECTED SHIFT"
|
||||
: "TODAY'S SHIFT",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
style: UiTypography
|
||||
.titleUppercase4b
|
||||
.copyWith(
|
||||
color: shift.id ==
|
||||
selectedShift?.id
|
||||
? AppColors.krowBlue
|
||||
: AppColors
|
||||
.krowCharcoal,
|
||||
letterSpacing: 0.5,
|
||||
? UiColors.primary
|
||||
: UiColors
|
||||
.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
shift.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: Color(0xFF1E293B),
|
||||
),
|
||||
style: UiTypography.body2b,
|
||||
),
|
||||
Text(
|
||||
"${shift.clientName} • ${shift.location}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
),
|
||||
style: UiTypography.body3r
|
||||
.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -214,18 +195,14 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
style: UiTypography.body3m
|
||||
.textSecondary,
|
||||
),
|
||||
Text(
|
||||
"\$${shift.hourlyRate}/hr",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowBlue,
|
||||
style: UiTypography.body3m
|
||||
.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -239,46 +216,41 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
|
||||
// Swipe To Check In / Checked Out State / No Shift State
|
||||
if (selectedShift != null && checkOutTime == null) ...<Widget>[
|
||||
if (!isCheckedIn &&
|
||||
!_isCheckInAllowed(selectedShift))
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 48,
|
||||
color: Color(0xFF94A3B8), // slate-400
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"You're early!",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Check-in available at ${_getCheckInAvailabilityTime(selectedShift)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
SwipeToCheckIn(
|
||||
if (selectedShift != null &&
|
||||
checkOutTime == null) ...<Widget>[
|
||||
if (!isCheckedIn &&
|
||||
!_isCheckInAllowed(selectedShift))
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding:
|
||||
const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.clock,
|
||||
size: 48,
|
||||
color: UiColors.iconThird,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
"You're early!",
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"Check-in available at ${_getCheckInAvailabilityTime(selectedShift)}",
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
SwipeToCheckIn(
|
||||
isCheckedIn: isCheckedIn,
|
||||
mode: state.checkInMode,
|
||||
isLoading:
|
||||
@@ -299,14 +271,17 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
onCheckOut: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => LunchBreakDialog(
|
||||
onComplete: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(); // Close dialog first
|
||||
_bloc.add(const CheckOutRequested());
|
||||
},
|
||||
),
|
||||
builder: (BuildContext context) =>
|
||||
LunchBreakDialog(
|
||||
onComplete: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(); // Close dialog first
|
||||
_bloc.add(
|
||||
const CheckOutRequested(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -314,13 +289,15 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
checkOutTime != null) ...<Widget>[
|
||||
// Shift Completed State
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFECFDF5), // emerald-50
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.tagSuccess,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFA7F3D0),
|
||||
), // emerald-200
|
||||
color: UiColors.success.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
@@ -328,31 +305,24 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFD1FAE5), // emerald-100
|
||||
color: UiColors.tagActive,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.check,
|
||||
color: Color(0xFF059669), // emerald-600
|
||||
UiIcons.check,
|
||||
color: UiColors.textSuccess,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Text(
|
||||
"Shift Completed!",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF065F46), // emerald-800
|
||||
),
|
||||
style: UiTypography.body1b.textSuccess,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"Great work today",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF059669), // emerald-600
|
||||
),
|
||||
style: UiTypography.body2r.textSuccess,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -361,29 +331,22 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
// No Shift State
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Column(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"No confirmed shifts for today",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"Accept a shift to clock in",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -393,15 +356,17 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
|
||||
// Checked In Banner
|
||||
if (isCheckedIn && checkInTime != null) ...<Widget>[
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFECFDF5), // emerald-50
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.tagSuccess,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFA7F3D0),
|
||||
), // emerald-200
|
||||
color: UiColors.success.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
@@ -411,23 +376,15 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
Text(
|
||||
"Checked in at",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF059669),
|
||||
),
|
||||
style: UiTypography.body3m.textSuccess,
|
||||
),
|
||||
Text(
|
||||
DateFormat(
|
||||
'h:mm a',
|
||||
).format(checkInTime),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF065F46),
|
||||
),
|
||||
style: UiTypography.body1b.textSuccess,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -435,12 +392,12 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFD1FAE5),
|
||||
color: UiColors.tagActive,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.check,
|
||||
color: Color(0xFF059669),
|
||||
UiIcons.check,
|
||||
color: UiColors.textSuccess,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -476,14 +433,14 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
child: GestureDetector(
|
||||
onTap: () => _bloc.add(CheckInModeChanged(value)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isSelected ? UiColors.white : UiColors.transparent,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
boxShadow: isSelected
|
||||
? <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -496,15 +453,15 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: isSelected ? Colors.black : Colors.grey,
|
||||
color: isSelected ? UiColors.foreground : UiColors.iconThird,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected ? Colors.black : Colors.grey,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: isSelected
|
||||
? UiColors.foreground
|
||||
: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -534,36 +491,31 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: scanned
|
||||
? Colors.green.shade50
|
||||
: Colors.blue.shade50,
|
||||
? UiColors.tagSuccess
|
||||
: UiColors.tagInProgress,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
scanned ? LucideIcons.check : LucideIcons.nfc,
|
||||
scanned ? UiIcons.check : UiIcons.nfc,
|
||||
size: 48,
|
||||
color: scanned
|
||||
? Colors.green.shade600
|
||||
: Colors.blue.shade600,
|
||||
color: scanned ? UiColors.textSuccess : UiColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
scanned ? 'Processing check-in...' : 'Ready to scan',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
scanned
|
||||
? 'Please wait...'
|
||||
: 'Hold your phone near the NFC tag at the clock-in station',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
if (!scanned) ...<Widget>[
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
@@ -584,19 +536,16 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
// But this dialog is just a function call.
|
||||
// It's safer to just return a result
|
||||
},
|
||||
icon: const Icon(LucideIcons.nfc, size: 24),
|
||||
label: const Text(
|
||||
icon: const Icon(UiIcons.nfc, size: 24),
|
||||
label: Text(
|
||||
'Tap to Scan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0047FF),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
enum AttendanceType { checkin, checkout, breaks, days }
|
||||
|
||||
class AttendanceCard extends StatelessWidget {
|
||||
|
||||
const AttendanceCard({
|
||||
super.key,
|
||||
required this.type,
|
||||
@@ -24,14 +23,14 @@ class AttendanceCard extends StatelessWidget {
|
||||
final _AttendanceStyle styles = _getStyles(type);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade100),
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.bgSecondary),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -46,17 +45,14 @@ class AttendanceCard extends StatelessWidget {
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: styles.bgColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: Icon(styles.icon, size: 16, color: styles.iconColor),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
style: UiTypography.titleUppercase4m.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -65,27 +61,20 @@ class AttendanceCard extends StatelessWidget {
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
),
|
||||
if (scheduledTime != null) ...<Widget>[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"Scheduled: $scheduledTime",
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF94A3B8), // slate-400
|
||||
),
|
||||
style: UiTypography.footnote2r.textInactive,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF0032A0)),
|
||||
style: UiTypography.footnote1r.copyWith(color: UiColors.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -96,27 +85,27 @@ class AttendanceCard extends StatelessWidget {
|
||||
switch (type) {
|
||||
case AttendanceType.checkin:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.logIn,
|
||||
bgColor: const Color(0xFF0032A0).withOpacity(0.1),
|
||||
iconColor: const Color(0xFF0032A0),
|
||||
icon: UiIcons.logIn,
|
||||
bgColor: UiColors.primary.withValues(alpha: 0.1),
|
||||
iconColor: UiColors.primary,
|
||||
);
|
||||
case AttendanceType.checkout:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.logOut,
|
||||
bgColor: const Color(0xFF333F48).withOpacity(0.1),
|
||||
iconColor: const Color(0xFF333F48),
|
||||
icon: UiIcons.logOut,
|
||||
bgColor: UiColors.foreground.withValues(alpha: 0.1),
|
||||
iconColor: UiColors.foreground,
|
||||
);
|
||||
case AttendanceType.breaks:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.coffee,
|
||||
bgColor: const Color(0xFFF9E547).withOpacity(0.2),
|
||||
iconColor: const Color(0xFF4C460D),
|
||||
icon: UiIcons.coffee,
|
||||
bgColor: UiColors.accent.withValues(alpha: 0.2),
|
||||
iconColor: UiColors.accentForeground,
|
||||
);
|
||||
case AttendanceType.days:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.calendar,
|
||||
bgColor: Colors.green.withOpacity(0.1),
|
||||
iconColor: Colors.green,
|
||||
icon: UiIcons.calendar,
|
||||
bgColor: UiColors.success.withValues(alpha: 0.1),
|
||||
iconColor: UiColors.textSuccess,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
enum CommuteMode {
|
||||
@@ -158,21 +157,21 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
|
||||
Widget _buildConsentCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[
|
||||
Color(0xFFEFF6FF), // blue-50
|
||||
Color(0xFFECFEFF), // cyan-50
|
||||
UiColors.primary.withValues(alpha: 0.05),
|
||||
UiColors.primary.withValues(alpha: 0.1),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -188,42 +187,35 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF2563EB), // blue-600
|
||||
color: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.mapPin,
|
||||
UiIcons.mapPin,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Enable Commute Tracking?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'Share location 1hr before shift so your manager can see you\'re on the way.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
style: UiTypography.body4r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
@@ -232,25 +224,29 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
setState(() => _localHasConsent = false);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
side: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
child: const Text('Not Now', style: TextStyle(fontSize: 12)),
|
||||
child: Text('Not Now', style: UiTypography.footnote1m),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _localHasConsent = true);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2563EB), // blue-600
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
backgroundColor: UiColors.primary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Enable',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
style: UiTypography.footnote1m.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -263,14 +259,14 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
|
||||
Widget _buildPreShiftCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -282,56 +278,46 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF1F5F9), // slate-100
|
||||
color: UiColors.bgSecondary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.navigation,
|
||||
UiIcons.navigation,
|
||||
size: 16,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
Text(
|
||||
'On My Way',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
UiIcons.clock,
|
||||
size: 12,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
color: UiColors.textInactive,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'Shift starts in ${_getMinutesUntilShift()} min',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
style: UiTypography.titleUppercase4m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
Text(
|
||||
'Track arrival',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
style: UiTypography.titleUppercase4m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -342,7 +328,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
setState(() => _localIsCommuteOn = value);
|
||||
widget.onCommuteToggled?.call(value);
|
||||
},
|
||||
activeThumbColor: AppColors.krowBlue,
|
||||
activeThumbColor: UiColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -357,8 +343,8 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[
|
||||
Color(0xFF2563EB), // blue-600
|
||||
Color(0xFF0891B2), // cyan-600
|
||||
UiColors.primary,
|
||||
UiColors.iconActive,
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -368,7 +354,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
@@ -383,13 +369,13 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.navigation,
|
||||
UiIcons.navigation,
|
||||
size: 48,
|
||||
color: Colors.white,
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -399,100 +385,84 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'On My Way',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: UiTypography.displayMb.white,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your manager can see you\'re heading to the site',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade100,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
if (widget.distanceMeters != null) ...<Widget>[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Distance to Site',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade100,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
_formatDistance(widget.distanceMeters!),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: UiTypography.displayM.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.etaMinutes != null) ...<Widget>[
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Estimated Arrival',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade100,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'${widget.etaMinutes} min',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: UiTypography.headline1m.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Text(
|
||||
'Most app features are locked while commute mode is on. You\'ll be able to clock in once you arrive.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue.shade100,
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -502,18 +472,20 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() => _localIsCommuteOn = false);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white.withOpacity(0.3)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
foregroundColor: UiColors.white,
|
||||
side: BorderSide(color: UiColors.white.withValues(alpha: 0.3)),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text('Turn Off Commute Mode'),
|
||||
child: Text('Turn Off Commute Mode', style: UiTypography.buttonL),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -524,21 +496,21 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
|
||||
Widget _buildArrivedCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[
|
||||
Color(0xFFECFDF5), // emerald-50
|
||||
Color(0xFFD1FAE5), // green-50
|
||||
UiColors.tagSuccess,
|
||||
UiColors.tagActive,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: UiColors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -550,31 +522,24 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF10B981), // emerald-500
|
||||
color: UiColors.success,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.checkCircle,
|
||||
UiIcons.check,
|
||||
size: 32,
|
||||
color: Colors.white,
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'You\'ve Arrived! 🎉',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'You\'re at the shift location. Ready to clock in?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateSelector extends StatelessWidget {
|
||||
|
||||
const DateSelector({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
@@ -34,14 +34,16 @@ class DateSelector extends StatelessWidget {
|
||||
onTap: () => onSelect(date),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF0032A0) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: isSelected ? UiColors.primary : UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: isSelected
|
||||
? <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: const Color(0xFF0032A0).withOpacity(0.3),
|
||||
color: UiColors.primary.withValues(alpha: 0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -53,33 +55,28 @@ class DateSelector extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
style: UiTypography.title1m.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: const Color(0xFF0F172A),
|
||||
color:
|
||||
isSelected ? UiColors.white : UiColors.foreground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: isSelected
|
||||
? Colors.white.withOpacity(0.8)
|
||||
: const Color(0xFF94A3B8),
|
||||
? UiColors.white.withValues(alpha: 0.8)
|
||||
: UiColors.textInactive,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
if (hasShift)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: const Color(0xFF0032A0),
|
||||
color: isSelected ? UiColors.white : UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
@@ -87,8 +84,8 @@ class DateSelector extends StatelessWidget {
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.border,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LocationMapPlaceholder extends StatelessWidget {
|
||||
|
||||
@@ -18,8 +17,8 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.border,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
image: DecorationImage(
|
||||
image: const NetworkImage(
|
||||
'https://maps.googleapis.com/maps/api/staticmap?center=40.7128,-74.0060&zoom=15&size=600x300&maptype=roadmap&markers=color:red%7C40.7128,-74.0060&key=YOUR_API_KEY',
|
||||
@@ -33,30 +32,37 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
// Fallback UI if image fails (which it will without key)
|
||||
const Center(
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(LucideIcons.mapPin, size: 48, color: UiColors.iconSecondary),
|
||||
SizedBox(height: 8),
|
||||
Text('Map View (GPS)', style: TextStyle(color: UiColors.textSecondary)),
|
||||
const Icon(
|
||||
UiIcons.mapPin,
|
||||
size: 48,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text('Map View (GPS)', style: UiTypography.body2r.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Status Overlay
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: UiConstants.space4,
|
||||
left: UiConstants.space4,
|
||||
right: UiConstants.space4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: UiColors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -65,23 +71,25 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
isVerified ? LucideIcons.checkCircle : LucideIcons.alertCircle,
|
||||
color: isVerified ? UiColors.textSuccess : UiColors.destructive,
|
||||
isVerified ? UiIcons.checkCircle : UiIcons.warning,
|
||||
color: isVerified
|
||||
? UiColors.textSuccess
|
||||
: UiColors.destructive,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
isVerified ? 'Location Verified' : 'Location Check',
|
||||
style: UiTypography.body1b.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
if (distance != null)
|
||||
Text(
|
||||
'${distance!.toStringAsFixed(0)}m from venue',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class LunchBreakDialog extends StatefulWidget {
|
||||
|
||||
const LunchBreakDialog({super.key, required this.onComplete});
|
||||
final VoidCallback onComplete;
|
||||
|
||||
@@ -47,8 +46,10 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
backgroundColor: UiColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.space6),
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _buildCurrentStep(),
|
||||
@@ -75,34 +76,30 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
|
||||
Widget _buildStep1() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.coffee,
|
||||
UiIcons.coffee,
|
||||
size: 40,
|
||||
color: Color(0xFF6A7382),
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
"Did You Take\na Lunch?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF121826),
|
||||
),
|
||||
style: UiTypography.headline1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
@@ -114,25 +111,23 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.transparent,
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.transparent,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"No",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF121826),
|
||||
),
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
@@ -142,19 +137,17 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
});
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: UiColors.primary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"Yes",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: UiTypography.body1m.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -168,155 +161,183 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
Widget _buildStep2() {
|
||||
// Time input
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"When did you take lunch?",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Mock Inputs
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
initialValue: _breakStart,
|
||||
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (String? v) => setState(() => _breakStart = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Start',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
initialValue: _breakEnd,
|
||||
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (String? v) => setState(() => _breakEnd = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'End',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 3);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"When did you take lunch?",
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Mock Inputs
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
value: _breakStart,
|
||||
items: _timeOptions
|
||||
.map(
|
||||
(String t) => DropdownMenuItem(
|
||||
value: t,
|
||||
child: Text(t, style: UiTypography.body3r),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (String? v) => setState(() => _breakStart = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Start',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text("Next", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
value: _breakEnd,
|
||||
items: _timeOptions
|
||||
.map(
|
||||
(String t) => DropdownMenuItem(
|
||||
value: t,
|
||||
child: Text(t, style: UiTypography.body3r),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (String? v) => setState(() => _breakEnd = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'End',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 3);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
],
|
||||
));
|
||||
child: Text("Next", style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2b() {
|
||||
Widget _buildStep2b() {
|
||||
// No lunch reason
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Why didn't you take lunch?",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Why didn't you take lunch?",
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
..._noLunchReasons.map(
|
||||
(String reason) => RadioListTile<String>(
|
||||
title: Text(reason, style: UiTypography.body2r),
|
||||
value: reason,
|
||||
groupValue: _noLunchReason,
|
||||
onChanged: (String? val) => setState(() => _noLunchReason = val),
|
||||
activeColor: UiColors.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._noLunchReasons.map((String reason) => RadioListTile<String>(
|
||||
title: Text(reason),
|
||||
value: reason,
|
||||
groupValue: _noLunchReason,
|
||||
onChanged: (String? val) => setState(() => _noLunchReason = val),
|
||||
)),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 3);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text("Next", style: TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 3);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
],
|
||||
));
|
||||
child: Text("Next", style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep3() {
|
||||
// Additional Notes
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Additional Notes",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Additional Notes",
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
TextField(
|
||||
onChanged: (String v) => _additionalNotes = v,
|
||||
style: UiTypography.body2r,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Add any details...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
onChanged: (String v) => _additionalNotes = v,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Add any details...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 4);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text("Submit", style: TextStyle(color: Colors.white)),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 4);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
],
|
||||
));
|
||||
child: Text("Submit", style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildStep4() {
|
||||
// Success
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(LucideIcons.checkCircle, size: 64, color: Colors.green),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
"Break Logged!",
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.checkCircle, size: 64, color: UiColors.success),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
"Break Logged!",
|
||||
style: UiTypography.headline1m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onComplete,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onComplete,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text("Close", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
));
|
||||
child: Text("Close", style: UiTypography.body1m.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class SwipeToCheckIn extends StatefulWidget {
|
||||
|
||||
const SwipeToCheckIn({
|
||||
super.key,
|
||||
this.onCheckIn,
|
||||
@@ -73,8 +72,8 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color baseColor = widget.isCheckedIn
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF0032A0);
|
||||
? UiColors.success
|
||||
: UiColors.primary;
|
||||
|
||||
if (widget.mode == 'nfc') {
|
||||
return GestureDetector(
|
||||
@@ -93,10 +92,10 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: baseColor.withOpacity(0.4),
|
||||
color: baseColor.withValues(alpha: 0.4),
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: -5,
|
||||
@@ -106,19 +105,15 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(LucideIcons.wifi, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(UiIcons.wifi, color: UiColors.white),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Text(
|
||||
widget.isLoading
|
||||
? (widget.isCheckedIn
|
||||
? "Checking out..."
|
||||
: "Checking in...")
|
||||
: (widget.isCheckedIn ? "NFC Check Out" : "NFC Check In"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
style: UiTypography.body1b.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -134,11 +129,11 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
// Calculate background color based on drag
|
||||
final double progress = _dragValue / maxDrag;
|
||||
final Color startColor = widget.isCheckedIn
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF0032A0);
|
||||
? UiColors.success
|
||||
: UiColors.primary;
|
||||
final Color endColor = widget.isCheckedIn
|
||||
? const Color(0xFF0032A0)
|
||||
: const Color(0xFF10B981);
|
||||
? UiColors.primary
|
||||
: UiColors.success;
|
||||
final Color currentColor =
|
||||
Color.lerp(startColor, endColor, progress) ?? startColor;
|
||||
|
||||
@@ -146,10 +141,10 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: UiColors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -164,11 +159,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
widget.isCheckedIn
|
||||
? "Swipe to Check Out"
|
||||
: "Swipe to Check In",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -176,28 +167,26 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
Center(
|
||||
child: Text(
|
||||
widget.isCheckedIn ? "Check Out!" : "Check In!",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 4 + _dragValue,
|
||||
top: 4,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: (DragUpdateDetails d) => _onDragUpdate(d, maxWidth),
|
||||
onHorizontalDragEnd: (DragEndDetails d) => _onDragEnd(d, maxWidth),
|
||||
onHorizontalDragUpdate: (DragUpdateDetails d) =>
|
||||
_onDragUpdate(d, maxWidth),
|
||||
onHorizontalDragEnd: (DragEndDetails d) =>
|
||||
_onDragEnd(d, maxWidth),
|
||||
child: Container(
|
||||
width: _handleSize,
|
||||
height: _handleSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: UiColors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -205,9 +194,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_isComplete
|
||||
? LucideIcons.check
|
||||
: LucideIcons.arrowRight,
|
||||
_isComplete ? UiIcons.check : UiIcons.arrowRight,
|
||||
color: startColor,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -46,10 +46,8 @@ class HomeRepositoryImpl
|
||||
.dayEnd(_toTimestamp(end))
|
||||
.execute());
|
||||
|
||||
// Filter for ACCEPTED applications (same logic as shifts_repository_impl)
|
||||
// Filter for CONFIRMED applications (same logic as shifts_repository_impl)
|
||||
final apps = response.data.applications.where((app) =>
|
||||
(app.status is Known &&
|
||||
(app.status as Known).value == ApplicationStatus.ACCEPTED) ||
|
||||
(app.status is Known &&
|
||||
(app.status as Known).value == ApplicationStatus.CONFIRMED));
|
||||
|
||||
@@ -145,4 +143,3 @@ class HomeRepositoryImpl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ 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:lucide_icons/lucide_icons.dart';
|
||||
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
|
||||
@@ -84,28 +84,28 @@ class WorkerHomePage extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: QuickActionItem(
|
||||
icon: LucideIcons.search,
|
||||
icon: UiIcons.search,
|
||||
label: quickI18n.find_shifts,
|
||||
onTap: () => Modular.to.toShifts(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: QuickActionItem(
|
||||
icon: LucideIcons.calendar,
|
||||
icon: UiIcons.calendar,
|
||||
label: quickI18n.availability,
|
||||
onTap: () => Modular.to.toAvailability(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: QuickActionItem(
|
||||
icon: LucideIcons.dollarSign,
|
||||
icon: UiIcons.dollar,
|
||||
label: quickI18n.earnings,
|
||||
onTap: () => Modular.to.toPayments(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Today's Shifts
|
||||
BlocBuilder<HomeCubit, HomeState>(
|
||||
@@ -124,9 +124,11 @@ class WorkerHomePage extends StatelessWidget {
|
||||
if (state.status == HomeStatus.loading)
|
||||
const Center(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: CircularProgressIndicator(),
|
||||
height: UiConstants.space10,
|
||||
width: UiConstants.space10,
|
||||
child: CircularProgressIndicator(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (shifts.isEmpty)
|
||||
@@ -150,7 +152,7 @@ class WorkerHomePage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Tomorrow's Shifts
|
||||
BlocBuilder<HomeCubit, HomeState>(
|
||||
@@ -178,7 +180,7 @@ class WorkerHomePage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Recommended Shifts
|
||||
SectionHeader(
|
||||
@@ -198,7 +200,8 @@ class WorkerHomePage extends StatelessWidget {
|
||||
itemCount: state.recommendedShifts.length,
|
||||
clipBehavior: Clip.none,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.only(
|
||||
right: UiConstants.space3),
|
||||
child: RecommendedShiftCard(
|
||||
shift: state.recommendedShifts[index],
|
||||
),
|
||||
@@ -207,7 +210,7 @@ class WorkerHomePage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -28,23 +28,20 @@ class HomeHeader extends StatelessWidget {
|
||||
spacing: UiConstants.space3,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: UiConstants.space12,
|
||||
height: UiConstants.space12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: UiColors.primary.withOpacity(0.2),
|
||||
color: UiColors.primary.withValues(alpha: 0.2),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: UiColors.primary.withOpacity(0.1),
|
||||
backgroundColor: UiColors.primary.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
initial,
|
||||
style: const TextStyle(
|
||||
color: UiColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
@@ -42,7 +42,7 @@ class PendingPaymentCard extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.dollarSign,
|
||||
UiIcons.dollar,
|
||||
color: UiColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
@@ -76,7 +76,7 @@ class PendingPaymentCard extends StatelessWidget {
|
||||
),
|
||||
SizedBox(width: UiConstants.space2),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
UiIcons.chevronRight,
|
||||
color: UiColors.mutedForeground,
|
||||
size: 20,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
@@ -29,19 +29,19 @@ class PlaceholderBanner extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: accent.withOpacity(0.3)),
|
||||
border: Border.all(color: accent.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
width: UiConstants.space10,
|
||||
height: UiConstants.space10,
|
||||
padding: const EdgeInsets.all(UiConstants.space2),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.bgBanner,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(LucideIcons.sparkles, color: accent, size: 20),
|
||||
child: Icon(UiIcons.sparkles, color: accent, size: UiConstants.space5),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
@@ -59,7 +59,7 @@ class PlaceholderBanner extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(LucideIcons.chevronRight, color: accent),
|
||||
Icon(UiIcons.chevronRight, color: accent),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,8 +22,8 @@ class QuickActionItem extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
width: UiConstants.space16,
|
||||
height: UiConstants.space16,
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgBanner,
|
||||
@@ -31,13 +31,13 @@ class QuickActionItem extends StatelessWidget {
|
||||
border: Border.all(color: UiColors.bgSecondary),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.foreground.withOpacity(0.05),
|
||||
color: UiColors.foreground.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: UiColors.primary, size: 24),
|
||||
child: Icon(icon, color: UiColors.primary, size: UiConstants.space6),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
class RecommendedShiftCard extends StatelessWidget {
|
||||
@@ -14,8 +14,6 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final recI18n = t.staff.home.recommended_card;
|
||||
final duration = 8;
|
||||
final totalPay = duration * shift.hourlyRate;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -23,14 +21,14 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
},
|
||||
child: Container(
|
||||
width: 300,
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
color: UiColors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -45,51 +43,43 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
recI18n.act_now,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFDC2626),
|
||||
),
|
||||
style: UiTypography.body3m.copyWith(color: UiColors.textError),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE8F0FF),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
color: UiColors.tagInProgress,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
recI18n.one_day,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF0047FF),
|
||||
),
|
||||
style: UiTypography.body3m.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: UiConstants.space10,
|
||||
height: UiConstants.space10,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE8F0FF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.tagInProgress,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.calendar,
|
||||
color: Color(0xFF0047FF),
|
||||
size: 20,
|
||||
UiIcons.calendar,
|
||||
color: UiColors.primary,
|
||||
size: UiConstants.space5,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -97,25 +87,16 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
Flexible(
|
||||
child: Text(
|
||||
shift.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
maxLines: 1,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'\$${totalPay.round()}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
'\$${shift.hourlyRate}/h',
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -125,17 +106,11 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
shift.clientName,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr • ${duration}h',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -144,57 +119,48 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.calendar,
|
||||
size: 14,
|
||||
UiIcons.calendar,
|
||||
size: UiConstants.space3,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
recI18n.today,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 14,
|
||||
UiIcons.clock,
|
||||
size: UiConstants.space3,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
recI18n.time_range(
|
||||
start: shift.startTime,
|
||||
end: shift.endTime,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.mapPin,
|
||||
size: 14,
|
||||
UiIcons.mapPin,
|
||||
size: UiConstants.space3,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Expanded(
|
||||
child: Text(
|
||||
shift.locationAddress,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
|
||||
/// Section header widget for home page sections, using design system tokens.
|
||||
class SectionHeader extends StatelessWidget {
|
||||
/// Section title
|
||||
@@ -23,43 +22,58 @@ class SectionHeader extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.headline4m,
|
||||
),
|
||||
if (action != null)
|
||||
if (onAction != null)
|
||||
GestureDetector(
|
||||
onTap: onAction,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
action!,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.primary),
|
||||
),
|
||||
const Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 16,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: UiColors.primary.withOpacity(0.2),
|
||||
Expanded(
|
||||
child: action != null
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
if (onAction != null)
|
||||
GestureDetector(
|
||||
onTap: onAction,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
action ?? '',
|
||||
style: UiTypography.body3r.textPrimary,
|
||||
),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: UiConstants.space4,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(alpha: 0.08),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
color: UiColors.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
action!,
|
||||
style: UiTypography.body3r.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
title,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
action!,
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
@@ -77,15 +77,15 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
Modular.to.pushShiftDetails(widget.shift);
|
||||
},
|
||||
child: Container(
|
||||
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: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -94,27 +94,28 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: UiConstants.space12,
|
||||
height: UiConstants.space12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: widget.shift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
child: Image.network(
|
||||
widget.shift.logoUrl!,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
LucideIcons.building2,
|
||||
: Icon(
|
||||
UiIcons.building,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -125,28 +126,18 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.shift.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '\$${widget.shift.hourlyRate}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
children: const [
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '/h',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 12,
|
||||
),
|
||||
style: UiTypography.body3r,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -155,19 +146,13 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
),
|
||||
Text(
|
||||
widget.shift.clientName,
|
||||
style: const TextStyle(
|
||||
color: UiColors.mutedForeground,
|
||||
fontSize: 13,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'${_formatTime(widget.shift.startTime)} • ${widget.shift.location}',
|
||||
style: const TextStyle(
|
||||
color: UiColors.mutedForeground,
|
||||
fontSize: 12,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -179,14 +164,14 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -195,7 +180,7 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
@@ -203,48 +188,45 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
width: UiConstants.space14,
|
||||
height: UiConstants.space14,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: widget.shift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
child: Image.network(
|
||||
widget.shift.logoUrl!,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
LucideIcons.building2,
|
||||
: Icon(
|
||||
UiIcons.building,
|
||||
size: 28,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
'Assigned ${_getTimeAgo(widget.shift.createdDate).replaceAll('Pending ', '')}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: UiTypography.body3m.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Title and Rate
|
||||
Row(
|
||||
@@ -257,18 +239,11 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
children: [
|
||||
Text(
|
||||
widget.shift.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
widget.shift.clientName,
|
||||
style: const TextStyle(
|
||||
color: UiColors.mutedForeground,
|
||||
fontSize: 14,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -276,31 +251,24 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '\$${widget.shift.hourlyRate}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
children: const [
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '/h',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 16,
|
||||
),
|
||||
style: UiTypography.body1r,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Location and Date
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.mapPin,
|
||||
Icon(
|
||||
UiIcons.mapPin,
|
||||
size: 16,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
@@ -308,30 +276,24 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.shift.location,
|
||||
style: const TextStyle(
|
||||
color: UiColors.mutedForeground,
|
||||
fontSize: 14,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Icon(
|
||||
LucideIcons.calendar,
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Icon(
|
||||
UiIcons.calendar,
|
||||
size: 16,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)}',
|
||||
style: const TextStyle(
|
||||
color: UiColors.mutedForeground,
|
||||
fontSize: 14,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Tags
|
||||
Wrap(
|
||||
@@ -339,21 +301,21 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildTag(
|
||||
LucideIcons.zap,
|
||||
UiIcons.zap,
|
||||
'Immediate start',
|
||||
UiColors.accent.withValues(alpha: 0.3),
|
||||
UiColors.foreground,
|
||||
),
|
||||
_buildTag(
|
||||
LucideIcons.timer,
|
||||
UiIcons.timer,
|
||||
'No experience',
|
||||
const Color(0xFFFEE2E2),
|
||||
const Color(0xFFDC2626),
|
||||
UiColors.tagError,
|
||||
UiColors.textError,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -361,46 +323,48 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
// Actions
|
||||
if (!widget.compact)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
height: UiConstants.space12,
|
||||
child: ElevatedButton(
|
||||
onPressed: widget.onApply,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.foreground,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Accept shift',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
child: const Text('Accept shift'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
height: UiConstants.space12,
|
||||
child: OutlinedButton(
|
||||
onPressed: widget.onDecline,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFEF4444),
|
||||
side: const BorderSide(color: Color(0xFFFCA5A5)),
|
||||
foregroundColor: UiColors.destructive,
|
||||
side: BorderSide(
|
||||
color: UiColors.destructive.withValues(alpha: 0.3),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Decline shift',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
child: const Text('Decline shift'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -414,7 +378,7 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -424,11 +388,7 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: text,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: UiTypography.body3m.copyWith(color: text),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
@@ -17,29 +18,33 @@ class AutoMatchToggle extends StatefulWidget {
|
||||
State<AutoMatchToggle> createState() => _AutoMatchToggleState();
|
||||
}
|
||||
|
||||
class _AutoMatchToggleState extends State<AutoMatchToggle> with SingleTickerProviderStateMixin {
|
||||
class _AutoMatchToggleState extends State<AutoMatchToggle>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.home.auto_match;
|
||||
final Color primary = Theme.of(context).colorScheme.primary;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
gradient: widget.enabled
|
||||
? LinearGradient(
|
||||
colors: [primary, primary.withOpacity(0.8)],
|
||||
colors: [
|
||||
UiColors.primary,
|
||||
UiColors.primary.withValues(alpha: 0.8),
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
)
|
||||
: null,
|
||||
color: widget.enabled ? null : Colors.white,
|
||||
border: widget.enabled ? null : Border.all(color: Colors.grey.shade200),
|
||||
color: widget.enabled ? null : UiColors.white,
|
||||
border:
|
||||
widget.enabled ? null : Border.all(color: UiColors.border),
|
||||
boxShadow: widget.enabled
|
||||
? [
|
||||
BoxShadow(
|
||||
color: primary.withOpacity(0.3),
|
||||
color: UiColors.primary.withValues(alpha: 0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -54,36 +59,39 @@ class _AutoMatchToggleState extends State<AutoMatchToggle> with SingleTickerProv
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
width: UiConstants.space10,
|
||||
height: UiConstants.space10,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.enabled
|
||||
? Colors.white.withOpacity(0.2)
|
||||
: primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
? UiColors.white.withValues(alpha: 0.2)
|
||||
: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.zap,
|
||||
color: widget.enabled ? Colors.white : primary,
|
||||
UiIcons.zap,
|
||||
color: widget.enabled ? UiColors.white : UiColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.enabled ? Colors.white : const Color(0xFF0F172A),
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: widget.enabled
|
||||
? UiColors.white
|
||||
: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.enabled ? i18n.finding_shifts : i18n.get_matched,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: widget.enabled ? const Color(0xFFF8E08E) : Colors.grey.shade500,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: widget.enabled
|
||||
? UiColors.accent
|
||||
: UiColors.textInactive,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -93,10 +101,10 @@ class _AutoMatchToggleState extends State<AutoMatchToggle> with SingleTickerProv
|
||||
Switch(
|
||||
value: widget.enabled,
|
||||
onChanged: widget.onToggle,
|
||||
activeThumbColor: Colors.white,
|
||||
activeTrackColor: Colors.white.withValues(alpha: 0.3),
|
||||
inactiveThumbColor: Colors.white,
|
||||
inactiveTrackColor: Colors.grey.shade300,
|
||||
activeThumbColor: UiColors.white,
|
||||
activeTrackColor: UiColors.white.withValues(alpha: 0.3),
|
||||
inactiveThumbColor: UiColors.white,
|
||||
inactiveTrackColor: UiColors.border,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -106,29 +114,28 @@ class _AutoMatchToggleState extends State<AutoMatchToggle> with SingleTickerProv
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Container(
|
||||
height: 1,
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
i18n.matching_based_on,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFF8E08E),
|
||||
fontSize: 12,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.accent,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
_buildChip(LucideIcons.mapPin, i18n.chips.location),
|
||||
_buildChip(UiIcons.mapPin, i18n.chips.location),
|
||||
_buildChip(
|
||||
LucideIcons.clock,
|
||||
UiIcons.clock,
|
||||
i18n.chips.availability,
|
||||
),
|
||||
_buildChip(LucideIcons.briefcase, i18n.chips.skills),
|
||||
_buildChip(UiIcons.briefcase, i18n.chips.skills),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -144,17 +151,17 @@ class _AutoMatchToggleState extends State<AutoMatchToggle> with SingleTickerProv
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 12, color: Colors.white),
|
||||
Icon(icon, size: 12, color: UiColors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
style: UiTypography.body3r.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
@@ -15,14 +16,14 @@ class BenefitsWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.home.benefits;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -35,7 +36,7 @@ class BenefitsWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => Modular.to.pushNamed('/benefits'),
|
||||
@@ -43,19 +44,19 @@ class BenefitsWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
i18n.view_all,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(color: Theme.of(context).colorScheme.primary),
|
||||
style: UiTypography.buttonL.textPrimary,
|
||||
),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
UiIcons.chevronRight,
|
||||
size: UiConstants.space4,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -63,19 +64,19 @@ class BenefitsWidget extends StatelessWidget {
|
||||
label: i18n.items.sick_days,
|
||||
current: 10,
|
||||
total: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
_BenefitItem(
|
||||
label: i18n.items.vacation,
|
||||
current: 40,
|
||||
total: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
_BenefitItem(
|
||||
label: i18n.items.holidays,
|
||||
current: 24,
|
||||
total: 24,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -104,13 +105,13 @@ class _BenefitItem extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
width: UiConstants.space14,
|
||||
height: UiConstants.space14,
|
||||
child: CustomPaint(
|
||||
painter: _CircularProgressPainter(
|
||||
progress: current / total,
|
||||
color: color,
|
||||
backgroundColor: const Color(0xFFE5E7EB),
|
||||
backgroundColor: UiColors.border,
|
||||
strokeWidth: 4,
|
||||
),
|
||||
child: Center(
|
||||
@@ -119,32 +120,21 @@ class _BenefitItem extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'${current.toInt()}/${total.toInt()}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1E293B),
|
||||
),
|
||||
style: UiTypography.body3m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
i18n.hours_label,
|
||||
style: const TextStyle(
|
||||
fontSize: 8,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
style: UiTypography.footnote1r.textTertiary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
@@ -32,9 +33,9 @@ class ImproveYourselfWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
clipBehavior: Clip.none,
|
||||
@@ -51,14 +52,14 @@ class ImproveYourselfWidget extends StatelessWidget {
|
||||
onTap: () => Modular.to.pushNamed(item['page']!),
|
||||
child: Container(
|
||||
width: 160,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
margin: const EdgeInsets.only(right: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -69,36 +70,33 @@ class ImproveYourselfWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 96,
|
||||
height: UiConstants.space24,
|
||||
width: double.infinity,
|
||||
child: Image.network(
|
||||
item['image']!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
color: Colors.grey,
|
||||
color: UiColors.background,
|
||||
child: Icon(
|
||||
UiIcons.zap,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item['title']!,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item['description']!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
|
||||
/// Widget for displaying more ways to use Krow, using design system tokens.
|
||||
class MoreWaysToUseKrowWidget extends StatelessWidget {
|
||||
/// Creates a [MoreWaysToUseKrowWidget].
|
||||
@@ -31,9 +31,9 @@ class MoreWaysToUseKrowWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
clipBehavior: Clip.none,
|
||||
@@ -50,14 +50,14 @@ class MoreWaysToUseKrowWidget extends StatelessWidget {
|
||||
onTap: () => Modular.to.pushNamed(item['page']!),
|
||||
child: Container(
|
||||
width: 160,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
margin: const EdgeInsets.only(right: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -68,25 +68,25 @@ class MoreWaysToUseKrowWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 96,
|
||||
height: UiConstants.space24,
|
||||
width: double.infinity,
|
||||
child: Image.network(
|
||||
item['image']!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
color: Colors.grey,
|
||||
color: UiColors.background,
|
||||
child: Icon(
|
||||
UiIcons.zap,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
child: Text(
|
||||
item['title']!,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -5,20 +5,10 @@ publish_to: none
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
sdk: '>=3.10.0 <4.0.0'
|
||||
sdk: ">=3.10.0 <4.0.0"
|
||||
flutter: ">=3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.0
|
||||
bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
lucide_icons: ^0.257.0
|
||||
intl: ^0.20.0
|
||||
google_fonts: ^7.0.0
|
||||
|
||||
# Architecture Packages
|
||||
design_system:
|
||||
path: ../../../design_system
|
||||
@@ -32,6 +22,15 @@ dependencies:
|
||||
path: ../shifts
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.0
|
||||
bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
intl: ^0.20.0
|
||||
google_fonts: ^7.0.0
|
||||
firebase_data_connect:
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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:lucide_icons/lucide_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
@@ -35,7 +35,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
return BlocProvider<PaymentsBloc>.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF8FAFC),
|
||||
backgroundColor: UiColors.background,
|
||||
body: BlocConsumer<PaymentsBloc, PaymentsState>(
|
||||
listener: (context, state) {
|
||||
if (state is PaymentsError) {
|
||||
@@ -58,7 +58,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
child: Text(
|
||||
translateErrorKey(state.message),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF64748B)), // TextSecondary
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -67,6 +67,14 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is PaymentsLoaded) {
|
||||
return _buildContent(context, state);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -78,63 +86,57 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
children: <Widget>[
|
||||
// Header Section with Gradient
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: <Color>[Color(0xFF0032A0), Color(0xFF333F48)],
|
||||
colors: <Color>[
|
||||
UiColors.primary,
|
||||
UiColors.primary.withValues(alpha: 0.8),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20,
|
||||
MediaQuery.of(context).padding.top + 24,
|
||||
20,
|
||||
32,
|
||||
UiConstants.space5,
|
||||
MediaQuery.of(context).padding.top + UiConstants.space6,
|
||||
UiConstants.space5,
|
||||
UiConstants.space8,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
Text(
|
||||
"Earnings",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: UiTypography.displayMb.white,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Main Balance
|
||||
Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
Text(
|
||||
"Total Earnings",
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF8E08E),
|
||||
fontSize: 14,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.accent,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"\$${state.summary.totalEarnings.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}",
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: UiTypography.displayL.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Period Tabs
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.all(UiConstants.space1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@@ -150,9 +152,9 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
|
||||
// Main Content - Offset upwards
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -16),
|
||||
offset: const Offset(0, -UiConstants.space4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -161,76 +163,75 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
payments: state.history,
|
||||
period: state.activePeriod,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
// Quick Stats
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: PaymentStatsCard(
|
||||
icon: LucideIcons.trendingUp,
|
||||
iconColor: const Color(0xFF059669),
|
||||
icon: UiIcons.chart,
|
||||
iconColor: UiColors.success,
|
||||
label: "This Week",
|
||||
amount: "\$${state.summary.weeklyEarnings}",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: PaymentStatsCard(
|
||||
icon: LucideIcons.calendar,
|
||||
iconColor: const Color(0xFF2563EB),
|
||||
icon: UiIcons.calendar,
|
||||
iconColor: UiColors.primary,
|
||||
label: "This Month",
|
||||
amount: "\$${state.summary.monthlyEarnings.toStringAsFixed(0)}",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Pending Pay
|
||||
if(state.summary.pendingEarnings > 0) PendingPayCard(
|
||||
amount: state.summary.pendingEarnings,
|
||||
onCashOut: () {
|
||||
Modular.to.pushNamed('/early-pay');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
if (state.summary.pendingEarnings > 0)
|
||||
PendingPayCard(
|
||||
amount: state.summary.pendingEarnings,
|
||||
onCashOut: () {
|
||||
Modular.to.pushNamed('/early-pay');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Recent Payments
|
||||
if (state.history.isNotEmpty) Column(
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Recent Payments",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A),
|
||||
if (state.history.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Recent Payments",
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Column(
|
||||
children: state.history.map((StaffPayment payment) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: PaymentHistoryItem(
|
||||
amount: payment.amount,
|
||||
title: "Shift Payment",
|
||||
location: "Varies",
|
||||
address: "Payment ID: ${payment.id}",
|
||||
date: payment.paidAt != null
|
||||
? DateFormat('E, MMM d').format(payment.paidAt!)
|
||||
: 'Pending',
|
||||
workedTime: "Completed",
|
||||
hours: 0,
|
||||
rate: 0.0,
|
||||
status: payment.status.name.toUpperCase(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Column(
|
||||
children: state.history.map((StaffPayment payment) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space2),
|
||||
child: PaymentHistoryItem(
|
||||
amount: payment.amount,
|
||||
title: "Shift Payment",
|
||||
location: "Varies",
|
||||
address: "Payment ID: ${payment.id}",
|
||||
date: payment.paidAt != null
|
||||
? DateFormat('E, MMM d')
|
||||
.format(payment.paidAt!)
|
||||
: 'Pending',
|
||||
workedTime: "Completed",
|
||||
hours: 0,
|
||||
rate: 0.0,
|
||||
status: payment.status.name.toUpperCase(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
@@ -248,19 +249,17 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
child: GestureDetector(
|
||||
onTap: () => _bloc.add(ChangePeriodEvent(value)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isSelected ? UiColors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected ? const Color(0xFF0032A0) : Colors.white,
|
||||
),
|
||||
style: isSelected
|
||||
? UiTypography.body2m.copyWith(color: UiColors.primary)
|
||||
: UiTypography.body2m.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -25,26 +26,30 @@ class EarningsGraph extends StatelessWidget {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No sufficient data for graph",
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
child: const Center(child: Text("No sufficient data for graph")),
|
||||
);
|
||||
}
|
||||
|
||||
final List<FlSpot> spots = _generateSpots(validPayments);
|
||||
final double maxX = spots.isNotEmpty ? spots.last.x : 0.0;
|
||||
final double maxY = spots.isNotEmpty ? spots.map((FlSpot s) => s.y).reduce((double a, double b) => a > b ? a : b) : 0.0;
|
||||
|
||||
return Container(
|
||||
height: 220,
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
offset: const Offset(0, 4),
|
||||
blurRadius: 12,
|
||||
),
|
||||
@@ -53,15 +58,11 @@ class EarningsGraph extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
Text(
|
||||
"Earnings Trend",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A),
|
||||
),
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
@@ -79,7 +80,7 @@ class EarningsGraph extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
DateFormat('d').format(validPayments[index].paidAt!),
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -96,13 +97,13 @@ class EarningsGraph extends StatelessWidget {
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: true,
|
||||
color: const Color(0xFF0032A0),
|
||||
color: UiColors.primary,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: const Color(0xFF0032A0).withOpacity(0.1),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -121,7 +122,7 @@ class EarningsGraph extends StatelessWidget {
|
||||
List<FlSpot> _generateSpots(List<StaffPayment> data) {
|
||||
// Generate spots based on index in the list for simplicity in this demo
|
||||
// Real implementation would map to actual dates on X-axis
|
||||
return List.generate(data.length, (int index) {
|
||||
return List<FlSpot>.generate(data.length, (int index) {
|
||||
return FlSpot(index.toDouble(), data[index].amount);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class PaymentHistoryItem extends StatelessWidget {
|
||||
|
||||
@@ -28,13 +28,13 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -50,23 +50,20 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF3B82F6), // blue-500
|
||||
color: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
Text(
|
||||
"PAID",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF2563EB), // blue-600
|
||||
letterSpacing: 0.5,
|
||||
style: UiTypography.titleUppercase4b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -76,16 +73,16 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.secondary,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.dollarSign,
|
||||
color: Color(0xFF334155), // slate-700
|
||||
UiIcons.chart,
|
||||
color: UiColors.mutedForeground,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
@@ -101,18 +98,11 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
Text(
|
||||
location,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -122,75 +112,59 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"\$${amount.toStringAsFixed(0)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
"\$${rate.toStringAsFixed(0)}/hr · ${hours}h",
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
|
||||
// Date and Time
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.calendar,
|
||||
UiIcons.calendar,
|
||||
size: 12,
|
||||
color: Color(0xFF64748B),
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
date,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
UiIcons.clock,
|
||||
size: 12,
|
||||
color: Color(0xFF64748B),
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
workedTime,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 1),
|
||||
|
||||
// Address
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.mapPin,
|
||||
UiIcons.mapPin,
|
||||
size: 12,
|
||||
color: Color(0xFF64748B),
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
address,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PaymentStatsCard extends StatelessWidget {
|
||||
|
||||
const PaymentStatsCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
@@ -9,6 +9,7 @@ class PaymentStatsCard extends StatelessWidget {
|
||||
required this.label,
|
||||
required this.amount,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String label;
|
||||
@@ -17,13 +18,13 @@ class PaymentStatsCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -35,24 +36,17 @@ class PaymentStatsCard extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
amount,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
style: UiTypography.headline1m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class PendingPayCard extends StatelessWidget {
|
||||
|
||||
@@ -14,17 +14,13 @@ class PendingPayCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: <Color>[Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.tagInProgress,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -36,63 +32,34 @@ class PendingPayCard extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
width: UiConstants.space10,
|
||||
height: UiConstants.space10,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE8F0FF),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.dollarSign,
|
||||
color: Color(0xFF0047FF),
|
||||
UiIcons.chart,
|
||||
color: UiColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
Text(
|
||||
"Pending",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
fontSize: 14,
|
||||
),
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
Text(
|
||||
"\$${amount.toStringAsFixed(0)} available",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
/*
|
||||
ElevatedButton.icon(
|
||||
onPressed: onCashOut,
|
||||
icon: const Icon(LucideIcons.zap, size: 14),
|
||||
label: const Text("Early Pay"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0047FF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.2),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
name: staff_payments
|
||||
description: Staff Payments 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: ">=3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
flutter_modular: ^6.3.2
|
||||
lucide_icons: ^0.257.0
|
||||
intl: ^0.20.0
|
||||
fl_chart: ^0.66.0
|
||||
|
||||
# Internal packages
|
||||
design_system:
|
||||
path: ../../../design_system
|
||||
@@ -30,10 +21,17 @@ dependencies:
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
flutter_modular: ^6.3.2
|
||||
lucide_icons: ^0.257.0
|
||||
intl: ^0.20.0
|
||||
fl_chart: ^0.66.0
|
||||
flutter_bloc: any
|
||||
equatable: any
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// The sign-out button widget.
|
||||
///
|
||||
@@ -14,31 +13,33 @@ class LogoutButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.profile.header;
|
||||
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Material(
|
||||
color: const Color(0x00000000),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(LucideIcons.logOut, color: UiColors.destructive, size: 20),
|
||||
SizedBox(width: UiConstants.space2),
|
||||
const Icon(
|
||||
UiIcons.logOut,
|
||||
color: UiColors.destructive,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
i18n.sign_out,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.destructive,
|
||||
),
|
||||
style: UiTypography.body1m.textError,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -34,13 +34,13 @@ class ProfileHeader extends StatelessWidget {
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(UiConstants.space6),
|
||||
@@ -56,22 +56,20 @@ class ProfileHeader extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.primaryForeground,
|
||||
),
|
||||
style: UiTypography.headline4m.textSecondary,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: onSignOutTap,
|
||||
child: Text(
|
||||
i18n.sign_out,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.primaryForeground.withOpacity(0.8),
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: UiConstants.space8),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
// Avatar Section
|
||||
Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
@@ -79,7 +77,7 @@ class ProfileHeader extends StatelessWidget {
|
||||
Container(
|
||||
width: 112,
|
||||
height: 112,
|
||||
padding: EdgeInsets.all(UiConstants.space1),
|
||||
padding: const EdgeInsets.all(UiConstants.space1),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
@@ -87,13 +85,13 @@ class ProfileHeader extends StatelessWidget {
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
UiColors.accent,
|
||||
UiColors.accent.withOpacity(0.5),
|
||||
UiColors.accent.withValues(alpha: 0.5),
|
||||
UiColors.primaryForeground,
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.foreground.withOpacity(0.2),
|
||||
color: UiColors.foreground.withValues(alpha: 0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -103,7 +101,7 @@ class ProfileHeader extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: UiColors.primaryForeground.withOpacity(0.2),
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.2),
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
@@ -123,16 +121,16 @@ class ProfileHeader extends StatelessWidget {
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
UiColors.accent,
|
||||
UiColors.accent.withOpacity(0.7),
|
||||
UiColors.accent.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
fullName.isNotEmpty ? fullName[0].toUpperCase() : 'K',
|
||||
style: UiTypography.displayM.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
fullName.isNotEmpty
|
||||
? fullName[0].toUpperCase()
|
||||
: 'K',
|
||||
style: UiTypography.displayM.primary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -148,7 +146,7 @@ class ProfileHeader extends StatelessWidget {
|
||||
border: Border.all(color: UiColors.primary, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.foreground.withOpacity(0.1),
|
||||
color: UiColors.foreground.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
@@ -161,28 +159,24 @@ class ProfileHeader extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
fullName,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.primaryForeground,
|
||||
),
|
||||
style: UiTypography.headline3m.textPlaceholder,
|
||||
),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space3,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.accent.withOpacity(0.2),
|
||||
color: UiColors.accent.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.space5),
|
||||
),
|
||||
child: Text(
|
||||
level,
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: UiColors.accent,
|
||||
),
|
||||
style: UiTypography.footnote1b.accent,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -25,10 +25,10 @@ class ProfileMenuItem extends StatelessWidget {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
padding: EdgeInsets.all(UiConstants.space2),
|
||||
padding: const EdgeInsets.all(UiConstants.space2),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Stack(
|
||||
@@ -42,24 +42,23 @@ class ProfileMenuItem extends StatelessWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
color: UiColors.primary.withValues(alpha: 0.08),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, color: UiColors.primary, size: 20),
|
||||
),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: UiConstants.space1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space1,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: UiTypography.footnote1m.copyWith(
|
||||
color: UiColors.foreground,
|
||||
height: 1.2,
|
||||
),
|
||||
style: UiTypography.footnote1m.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -76,16 +75,18 @@ class ProfileMenuItem extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
color: completed!
|
||||
? UiColors.primary
|
||||
: UiColors.primary.withOpacity(0.1),
|
||||
: UiColors.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: completed!
|
||||
? const Icon(Icons.check, size: 10, color: UiColors.primaryForeground)
|
||||
? const Icon(
|
||||
UiIcons.check,
|
||||
size: 10,
|
||||
color: UiColors.primaryForeground,
|
||||
)
|
||||
: Text(
|
||||
"!",
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.footnote2b.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -19,10 +19,10 @@ class ReliabilityScoreBar extends StatelessWidget {
|
||||
final score = (reliabilityScore ?? 0) / 100;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -32,19 +32,15 @@ class ReliabilityScoreBar extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
i18n.title,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.body2m.primary,
|
||||
),
|
||||
Text(
|
||||
"${reliabilityScore ?? 0}%",
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.headline4m.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(UiConstants.space1),
|
||||
child: LinearProgressIndicator(
|
||||
@@ -55,12 +51,10 @@ class ReliabilityScoreBar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: UiConstants.space2),
|
||||
padding: const EdgeInsets.only(top: UiConstants.space2),
|
||||
child: Text(
|
||||
i18n.description,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Displays the staff member's reliability statistics (Shifts, Rating, On Time, etc.).
|
||||
///
|
||||
@@ -24,14 +23,14 @@ class ReliabilityStatsCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.foreground.withOpacity(0.05),
|
||||
color: UiColors.foreground.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -42,31 +41,31 @@ class ReliabilityStatsCard extends StatelessWidget {
|
||||
children: [
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.briefcase,
|
||||
UiIcons.briefcase,
|
||||
"${totalShifts ?? 0}",
|
||||
"Shifts",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.star,
|
||||
UiIcons.star,
|
||||
(averageRating ?? 0.0).toStringAsFixed(1),
|
||||
"Rating",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.clock,
|
||||
UiIcons.clock,
|
||||
"${onTimeRate ?? 0}%",
|
||||
"On Time",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.xCircle,
|
||||
UiIcons.xCircle,
|
||||
"${noShowCount ?? 0}",
|
||||
"No Shows",
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
LucideIcons.ban,
|
||||
UiIcons.ban,
|
||||
"${cancellationCount ?? 0}",
|
||||
"Cancel.",
|
||||
),
|
||||
@@ -88,26 +87,22 @@ class ReliabilityStatsCard extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(icon, size: 20, color: UiColors.primary),
|
||||
),
|
||||
SizedBox(height: UiConstants.space1),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
style: UiTypography.body1b.textSecondary,
|
||||
),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -13,14 +13,11 @@ class SectionTitle extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(left: UiConstants.space1),
|
||||
margin: EdgeInsets.only(bottom: UiConstants.space3),
|
||||
padding: const EdgeInsets.only(left: UiConstants.space1),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: UiColors.mutedForeground,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
style: UiTypography.footnote1b.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class CertificatesPage extends StatelessWidget {
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -48),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
...documents.map((StaffDocument doc) => CertificateCard(
|
||||
@@ -82,11 +82,11 @@ class CertificatesPage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
AddCertificateCard(
|
||||
onTap: () => _showUploadModal(context, null),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,37 +12,36 @@ class AddCertificateCard extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[Colors.grey[50]!, Colors.grey[100]!], // Keep prototype style
|
||||
colors: <Color>[
|
||||
UiColors.bgSecondary.withValues(alpha: 0.5),
|
||||
UiColors.bgSecondary,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
color: UiColors.border,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.add, color: UiColors.primary, size: 24),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.add_more.title,
|
||||
style: UiTypography.body1b.copyWith( // 16px Bold
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
Text(
|
||||
t.staff_certificates.add_more.subtitle,
|
||||
style: UiTypography.body3r.copyWith( // 12px Regular
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class CertificateCard extends StatelessWidget {
|
||||
final StaffDocument document;
|
||||
@@ -39,13 +39,13 @@ class CertificateCard extends StatelessWidget {
|
||||
final _CertificateUiProps uiProps = _getUiProps(document.documentId);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.space4),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -57,11 +57,14 @@ class CertificateCard extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
if (isExpiring || isExpired)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF9E547).withOpacity(0.2), // Yellow tint
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Color(0x66F9E547)),
|
||||
color: UiColors.accent.withValues(alpha: 0.2), // Yellow tint
|
||||
border: Border(
|
||||
bottom: BorderSide(color: UiColors.accent.withValues(alpha: 0.4)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -76,16 +79,14 @@ class CertificateCard extends StatelessWidget {
|
||||
isExpired
|
||||
? t.staff_certificates.card.expired
|
||||
: t.staff_certificates.card.expires_in_days(days: _daysUntilExpiry(document.expiryDate)),
|
||||
style: UiTypography.body3m.copyWith( // 12px Medium
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body3m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -96,8 +97,8 @@ class CertificateCard extends StatelessWidget {
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: uiProps.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: uiProps.color.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
@@ -137,7 +138,7 @@ class CertificateCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -151,16 +152,12 @@ class CertificateCard extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
document.name,
|
||||
style: UiTypography.body1m.copyWith( // 16px Medium
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
document.description ?? '', // Optional description
|
||||
style: UiTypography.body3r.copyWith( // 12px Regular
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -172,7 +169,7 @@ class CertificateCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
if (showComplete) _buildCompleteStatus(document.expiryDate),
|
||||
|
||||
@@ -186,9 +183,11 @@ class CertificateCard extends StatelessWidget {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Row(
|
||||
@@ -202,9 +201,7 @@ class CertificateCard extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_certificates.card.upload_button,
|
||||
style: UiTypography.body2m.copyWith( // 14px Medium
|
||||
color: UiColors.white,
|
||||
),
|
||||
style: UiTypography.body2m.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -212,7 +209,7 @@ class CertificateCard extends StatelessWidget {
|
||||
),
|
||||
|
||||
if (showComplete || isExpiring || isExpired) ...<Widget>[
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
@@ -223,13 +220,15 @@ class CertificateCard extends StatelessWidget {
|
||||
foregroundColor: UiColors.textPrimary,
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton.icon(
|
||||
@@ -238,9 +237,11 @@ class CertificateCard extends StatelessWidget {
|
||||
label: Text(t.staff_certificates.card.remove),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -274,16 +275,14 @@ class CertificateCard extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_certificates.card.verified,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (expiryDate != null)
|
||||
Text(
|
||||
t.staff_certificates.card.exp(date: DateFormat('MMM d, yyyy').format(expiryDate)),
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -6,7 +6,8 @@ import 'package:flutter/material.dart';
|
||||
class CertificateUploadModal extends StatelessWidget {
|
||||
/// The document being edited, or null for a new upload.
|
||||
// ignore: unused_field
|
||||
final dynamic document; // Using dynamic for now as we don't import domain here to avoid direct coupling if possible, but actually we should import domain.
|
||||
final dynamic
|
||||
document; // Using dynamic for now as we don't import domain here to avoid direct coupling if possible, but actually we should import domain.
|
||||
// Ideally, widgets should be dumb. Let's import domain.
|
||||
|
||||
final VoidCallback onSave;
|
||||
@@ -24,13 +25,13 @@ class CertificateUploadModal extends StatelessWidget {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
topLeft: Radius.circular(UiConstants.radiusBase),
|
||||
topRight: Radius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -39,7 +40,7 @@ class CertificateUploadModal extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.title,
|
||||
style: UiTypography.headline3m.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onCancel,
|
||||
@@ -47,35 +48,42 @@ class CertificateUploadModal extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.expiry_label,
|
||||
style: UiTypography.body1m,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.calendar, size: 20, color: UiColors.textSecondary),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(
|
||||
UiIcons.calendar,
|
||||
size: 20,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.select_date,
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.upload_file,
|
||||
style: UiTypography.body1m,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
@@ -84,16 +92,16 @@ class CertificateUploadModal extends StatelessWidget {
|
||||
color: UiColors.border,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
color: UiColors.background,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFEFF6FF), // Light blue
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagActive,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -102,7 +110,7 @@ class CertificateUploadModal extends StatelessWidget {
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.drag_drop,
|
||||
style: UiTypography.body1m,
|
||||
@@ -110,43 +118,51 @@ class CertificateUploadModal extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.staff_certificates.upload_modal.supported_formats,
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: onCancel,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
child: Text(t.staff_certificates.upload_modal.cancel,
|
||||
style: UiTypography.body1m.copyWith(color: UiColors.textPrimary)),
|
||||
child: Text(
|
||||
t.staff_certificates.upload_modal.cancel,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: onSave,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(t.staff_certificates.upload_modal.save,
|
||||
style: UiTypography.body1m.copyWith(color: Colors.white)),
|
||||
child: Text(
|
||||
t.staff_certificates.upload_modal.save,
|
||||
style: UiTypography.body1m.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -20,13 +20,21 @@ class CertificatesHeader extends StatelessWidget {
|
||||
final int progressPercent = totalCount == 0 ? 0 : (progressValue * 100).round();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 60, 20, 80),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
60,
|
||||
UiConstants.space5,
|
||||
80,
|
||||
),
|
||||
// Keeping gradient as per prototype layout requirement
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: <Color>[UiColors.primary, Color(0xFF1E40AF)], // Using Primary and a darker shade
|
||||
colors: <Color>[
|
||||
UiColors.primary,
|
||||
Color(0xFF1E40AF),
|
||||
], // Using Primary and a darker shade
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
@@ -39,7 +47,7 @@ class CertificatesHeader extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.1),
|
||||
color: UiColors.white.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -49,16 +57,14 @@ class CertificatesHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Text(
|
||||
t.staff_certificates.title,
|
||||
style: UiTypography.headline3m.copyWith( // 18px Bold
|
||||
color: UiColors.white,
|
||||
),
|
||||
style: UiTypography.headline3m.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
@@ -70,53 +76,48 @@ class CertificatesHeader extends StatelessWidget {
|
||||
CircularProgressIndicator(
|
||||
value: progressValue,
|
||||
strokeWidth: 8,
|
||||
backgroundColor: UiColors.white.withOpacity(0.2),
|
||||
backgroundColor: UiColors.white.withValues(alpha: 0.2),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
Color(0xFFF9E547), // Yellow from prototype
|
||||
UiColors.accent, // Yellow from prototype
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'$progressPercent%',
|
||||
style: UiTypography.display1b.copyWith( // 26px Bold
|
||||
color: UiColors.white,
|
||||
),
|
||||
style: UiTypography.display1b.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
const SizedBox(width: UiConstants.space6),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_certificates.progress.title,
|
||||
style: UiTypography.body1b.copyWith( // 16px Bold
|
||||
color: UiColors.white,
|
||||
),
|
||||
style: UiTypography.body1b.white,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.staff_certificates.progress.verified_count(completed: completedCount, total: totalCount),
|
||||
style: UiTypography.body3r.copyWith( // 12px Regular
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
t.staff_certificates.progress.verified_count(
|
||||
completed: completedCount, total: totalCount),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.shield,
|
||||
color: Color(0xFFF9E547),
|
||||
color: UiColors.accent,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_certificates.progress.active,
|
||||
style: UiTypography.body3m.copyWith( // 12px Medium
|
||||
color: const Color(0xFFF9E547),
|
||||
),
|
||||
style: UiTypography.body3m.accent,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
@@ -18,11 +17,11 @@ class DocumentCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
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: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
@@ -32,7 +31,7 @@ class DocumentCard extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
@@ -43,7 +42,7 @@ class DocumentCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -53,9 +52,7 @@ class DocumentCard extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
document.name,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
_getStatusIcon(document.status),
|
||||
],
|
||||
@@ -64,15 +61,13 @@ class DocumentCard extends StatelessWidget {
|
||||
if (document.description != null)
|
||||
Text(
|
||||
document.description!,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
_buildStatusBadge(document.status),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildActionButton(document.status),
|
||||
],
|
||||
),
|
||||
@@ -114,27 +109,27 @@ class DocumentCard extends StatelessWidget {
|
||||
|
||||
switch (status) {
|
||||
case DocumentStatus.verified:
|
||||
bg = UiColors.textSuccess.withOpacity(0.2);
|
||||
bg = UiColors.tagSuccess;
|
||||
text = UiColors.textSuccess;
|
||||
label = t.staff_documents.card.verified;
|
||||
break;
|
||||
case DocumentStatus.pending:
|
||||
bg = UiColors.textWarning.withOpacity(0.2);
|
||||
bg = UiColors.tagPending;
|
||||
text = UiColors.textWarning;
|
||||
label = t.staff_documents.card.pending;
|
||||
break;
|
||||
case DocumentStatus.missing:
|
||||
bg = UiColors.textError.withOpacity(0.2);
|
||||
bg = UiColors.textError.withValues(alpha: 0.1);
|
||||
text = UiColors.textError;
|
||||
label = t.staff_documents.card.missing;
|
||||
break;
|
||||
case DocumentStatus.rejected:
|
||||
bg = UiColors.textError.withOpacity(0.2);
|
||||
bg = UiColors.textError.withValues(alpha: 0.1);
|
||||
text = UiColors.textError;
|
||||
label = t.staff_documents.card.rejected;
|
||||
break;
|
||||
case DocumentStatus.expired:
|
||||
bg = UiColors.textError.withOpacity(0.2);
|
||||
bg = UiColors.textError.withValues(alpha: 0.1);
|
||||
text = UiColors.textError;
|
||||
label = t.staff_documents.card.rejected; // Or define "Expired" string
|
||||
break;
|
||||
@@ -165,7 +160,7 @@ class DocumentCard extends StatelessWidget {
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
isVerified ? UiIcons.eye : LucideIcons.upload,
|
||||
isVerified ? UiIcons.eye : UiIcons.upload,
|
||||
size: 16,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
@@ -174,9 +169,7 @@ class DocumentCard extends StatelessWidget {
|
||||
isVerified
|
||||
? t.staff_documents.card.view
|
||||
: t.staff_documents.card.upload,
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.body3m.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -24,10 +24,10 @@ class DocumentsProgressCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
@@ -37,16 +37,14 @@ class DocumentsProgressCard extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_documents.verification_card.title,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
t.staff_documents.verification_card.progress(
|
||||
completed: completedCount,
|
||||
total: totalCount,
|
||||
),
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.primary),
|
||||
style: UiTypography.body2r.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -83,10 +83,13 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
if (state.status == FormI9Status.success) {
|
||||
// Success view is handled by state check in build or we can navigate
|
||||
} else if (state.status == FormI9Status.failure) {
|
||||
final ScaffoldMessengerState messenger = ScaffoldMessenger.of(context);
|
||||
final ScaffoldMessengerState messenger =
|
||||
ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'An error occurred')),
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'An error occurred'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -100,7 +103,10 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
_buildHeader(context, state),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: _buildCurrentStep(context, state),
|
||||
),
|
||||
),
|
||||
@@ -118,9 +124,9 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
backgroundColor: UiColors.background,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
padding: const EdgeInsets.all(UiConstants.space8),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
@@ -132,40 +138,40 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFDCFCE7),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagSuccess,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.success,
|
||||
color: Color(0xFF16A34A),
|
||||
color: UiColors.textSuccess,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Form I-9 Submitted!',
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your employment eligibility verification has been submitted.',
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Modular.to.pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.bgPopup,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
@@ -183,7 +189,12 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
Widget _buildHeader(BuildContext context, FormI9State state) {
|
||||
return Container(
|
||||
color: UiColors.primary,
|
||||
padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60,
|
||||
bottom: UiConstants.space6,
|
||||
left: UiConstants.space5,
|
||||
right: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -193,31 +204,34 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
onTap: () => Modular.to.pop(),
|
||||
child: const Icon(
|
||||
UiIcons.arrowLeft,
|
||||
color: UiColors.bgPopup,
|
||||
color: UiColors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Form I-9',
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.bgPopup,
|
||||
),
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
Text(
|
||||
'Employment Eligibility Verification',
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: _steps.asMap().entries.map((MapEntry<int, Map<String, String>> entry) {
|
||||
children: _steps
|
||||
.asMap()
|
||||
.entries
|
||||
.map((MapEntry<int, Map<String, String>> entry) {
|
||||
final int idx = entry.key;
|
||||
final bool isLast = idx == _steps.length - 1;
|
||||
return Expanded(
|
||||
@@ -228,8 +242,8 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: idx <= state.currentStep
|
||||
? UiColors.bgPopup
|
||||
: UiColors.bgPopup.withOpacity(0.3),
|
||||
? UiColors.white
|
||||
: UiColors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
@@ -240,20 +254,21 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Step ${state.currentStep + 1} of ${_steps.length}',
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_steps[state.currentStep]['title']!,
|
||||
textAlign: TextAlign.end,
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.bgPopup,
|
||||
style: UiTypography.body3m.white.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -292,8 +307,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
style: UiTypography.body3m.textSecondary.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -305,26 +319,26 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
onChanged: onChanged,
|
||||
keyboardType: keyboardType,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
hintStyle: const TextStyle(color: UiColors.textPlaceholder),
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
@@ -455,15 +469,15 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'State *',
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
style: UiTypography.body3m.textSecondary.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
DropdownButtonFormField<String>(
|
||||
value: state.state.isEmpty ? null : state.state,
|
||||
onChanged: (String? val) => context.read<FormI9Cubit>().stateChanged(val ?? ''),
|
||||
onChanged: (String? val) =>
|
||||
context.read<FormI9Cubit>().stateChanged(val ?? ''),
|
||||
items: _usStates.map((String stateAbbr) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: stateAbbr,
|
||||
@@ -473,13 +487,15 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
),
|
||||
@@ -507,9 +523,9 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'I attest, under penalty of perjury, that I am (check one of the following boxes):',
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
_buildRadioOption(
|
||||
context,
|
||||
state,
|
||||
@@ -578,15 +594,21 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRadioOption(BuildContext context, FormI9State state, String value, String label, {Widget? child}) {
|
||||
Widget _buildRadioOption(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
String value,
|
||||
String label, {
|
||||
Widget? child,
|
||||
}) {
|
||||
final bool isSelected = state.citizenshipStatus == value;
|
||||
return GestureDetector(
|
||||
onTap: () => context.read<FormI9Cubit>().citizenshipStatusChanged(value),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: isSelected ? UiColors.primary : UiColors.border,
|
||||
width: isSelected ? 2 : 1,
|
||||
@@ -602,18 +624,16 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected ? UiColors.primary : Colors.grey,
|
||||
color: isSelected ? UiColors.primary : UiColors.border,
|
||||
width: isSelected ? 6 : 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -630,10 +650,10 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
@@ -643,15 +663,21 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
'Summary',
|
||||
style: UiTypography.headline4m.copyWith(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
_buildSummaryRow('Name', '${state.firstName} ${state.lastName}'),
|
||||
_buildSummaryRow('Address', '${state.address}, ${state.city}'),
|
||||
_buildSummaryRow('SSN', '***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}'),
|
||||
_buildSummaryRow('Citizenship', _getReadableCitizenship(state.citizenshipStatus)),
|
||||
_buildSummaryRow(
|
||||
'SSN',
|
||||
'***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}',
|
||||
),
|
||||
_buildSummaryRow(
|
||||
'Citizenship',
|
||||
_getReadableCitizenship(state.citizenshipStatus),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
CheckboxListTile(
|
||||
value: state.preparerUsed,
|
||||
onChanged: (bool? val) {
|
||||
@@ -660,29 +686,27 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
'I used a preparer or translator',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
activeColor: UiColors.primary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.accent.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'I am aware that federal law provides for imprisonment and/or fines for false statements or use of false documents in connection with the completion of this form.',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFFB45309)),
|
||||
style: UiTypography.body3r.textWarning.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'Signature (type your full name) *',
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
@@ -690,44 +714,46 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: state.signature.length),
|
||||
),
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().signatureChanged(val),
|
||||
onChanged: (String val) =>
|
||||
context.read<FormI9Cubit>().signatureChanged(val),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your full name',
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontFamily: 'Cursive', fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Date',
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3F4F6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Text(
|
||||
@@ -747,15 +773,13 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
textAlign: TextAlign.end,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -780,7 +804,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
|
||||
Widget _buildFooter(BuildContext context, FormI9State state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
@@ -791,24 +815,30 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
if (state.currentStep > 0)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.only(right: UiConstants.space3),
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _handleBack(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.arrowLeft, size: 16, color: UiColors.textPrimary),
|
||||
const Icon(
|
||||
UiIcons.arrowLeft,
|
||||
size: 16,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Back',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -818,16 +848,20 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_canProceed(state) && state.status != FormI9Status.submitting)
|
||||
onPressed: (
|
||||
_canProceed(state) &&
|
||||
state.status != FormI9Status.submitting)
|
||||
? () => _handleNext(context, state.currentStep)
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
disabledBackgroundColor: Colors.grey[300],
|
||||
foregroundColor: UiColors.bgPopup,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
disabledBackgroundColor: UiColors.bgSecondary,
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
@@ -836,7 +870,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: UiColors.bgPopup,
|
||||
color: UiColors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
@@ -850,7 +884,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
if (state.currentStep < _steps.length - 1) ...<Widget>[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.white),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -146,7 +146,10 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
_buildHeader(context, state),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: _buildCurrentStep(context, state),
|
||||
),
|
||||
),
|
||||
@@ -164,9 +167,9 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
backgroundColor: UiColors.background,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
padding: const EdgeInsets.all(UiConstants.space8),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
@@ -178,40 +181,40 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFDCFCE7),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagSuccess,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.success,
|
||||
color: Color(0xFF16A34A),
|
||||
color: UiColors.textSuccess,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Form W-4 Submitted!',
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your withholding certificate has been submitted to your employer.',
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Modular.to.pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.bgPopup,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
@@ -229,7 +232,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
Widget _buildHeader(BuildContext context, FormW4State state) {
|
||||
return Container(
|
||||
color: UiColors.primary,
|
||||
padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60,
|
||||
bottom: UiConstants.space6,
|
||||
left: UiConstants.space5,
|
||||
right: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -239,31 +247,34 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
onTap: () => Modular.to.pop(),
|
||||
child: const Icon(
|
||||
UiIcons.arrowLeft,
|
||||
color: UiColors.bgPopup,
|
||||
color: UiColors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Form W-4',
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.bgPopup,
|
||||
),
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
Text(
|
||||
'Employee\'s Withholding Certificate',
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: _steps.asMap().entries.map((MapEntry<int, Map<String, String>> entry) {
|
||||
children: _steps
|
||||
.asMap()
|
||||
.entries
|
||||
.map((MapEntry<int, Map<String, String>> entry) {
|
||||
final int idx = entry.key;
|
||||
final bool isLast = idx == _steps.length - 1;
|
||||
return Expanded(
|
||||
@@ -274,8 +285,8 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: idx <= state.currentStep
|
||||
? UiColors.bgPopup
|
||||
: UiColors.bgPopup.withOpacity(0.3),
|
||||
? UiColors.white
|
||||
: UiColors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
@@ -286,18 +297,19 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Step ${state.currentStep + 1} of ${_steps.length}',
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_steps[state.currentStep]['title']!,
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.bgPopup,
|
||||
style: UiTypography.body3m.white.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -339,8 +351,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
style: UiTypography.body3m.textSecondary.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -352,26 +363,26 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
onChanged: onChanged,
|
||||
keyboardType: keyboardType,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
hintStyle: const TextStyle(color: UiColors.textPlaceholder),
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
@@ -438,25 +449,25 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.tagActive,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(UiIcons.info, color: Color(0xFF2563EB), size: 20),
|
||||
SizedBox(width: 12),
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.info, color: UiColors.primary, size: 20),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Your filing status determines your standard deduction and tax rates.',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF1D4ED8)),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
_buildRadioOption(
|
||||
context,
|
||||
state,
|
||||
@@ -484,15 +495,21 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRadioOption(BuildContext context, FormW4State state, String value, String label, String? subLabel) {
|
||||
Widget _buildRadioOption(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
String value,
|
||||
String label,
|
||||
String? subLabel,
|
||||
) {
|
||||
final bool isSelected = state.filingStatus == value;
|
||||
return GestureDetector(
|
||||
onTap: () => context.read<FormW4Cubit>().filingStatusChanged(value),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: isSelected ? UiColors.primary : UiColors.border,
|
||||
width: isSelected ? 2 : 1,
|
||||
@@ -508,29 +525,25 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected ? UiColors.primary : Colors.grey,
|
||||
color: isSelected ? UiColors.primary : UiColors.border,
|
||||
width: isSelected ? 6 : 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
if (subLabel != null) ...<Widget>[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subLabel,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -546,36 +559,32 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.accent.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
const Icon(
|
||||
UiIcons.help,
|
||||
color: Color(0xFFD97706),
|
||||
color: UiColors.accent,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'When to complete this step?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF92400E),
|
||||
fontSize: 14,
|
||||
),
|
||||
style: UiTypography.body2m.accent,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Complete this step only if you hold more than one job at a time, or are married filing jointly and your spouse also works.',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFFB45309)),
|
||||
style: UiTypography.body3r.accent,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -583,18 +592,17 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
GestureDetector(
|
||||
onTap: () => context.read<FormW4Cubit>().multipleJobsChanged(!state.multipleJobs),
|
||||
onTap: () =>
|
||||
context.read<FormW4Cubit>().multipleJobsChanged(!state.multipleJobs),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(
|
||||
color: state.multipleJobs
|
||||
? UiColors.primary
|
||||
: UiColors.border,
|
||||
color: state.multipleJobs ? UiColors.primary : UiColors.border,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -604,20 +612,16 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: state.multipleJobs
|
||||
? UiColors.primary
|
||||
: UiColors.bgPopup,
|
||||
color: state.multipleJobs ? UiColors.primary : UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: state.multipleJobs
|
||||
? UiColors.primary
|
||||
: Colors.grey,
|
||||
color: state.multipleJobs ? UiColors.primary : UiColors.border,
|
||||
),
|
||||
),
|
||||
child: state.multipleJobs
|
||||
? const Icon(
|
||||
UiIcons.check,
|
||||
color: UiColors.bgPopup,
|
||||
color: UiColors.white,
|
||||
size: 16,
|
||||
)
|
||||
: null,
|
||||
@@ -629,16 +633,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'I have multiple jobs or my spouse works',
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Check this box if there are only two jobs total',
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -651,7 +651,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
Text(
|
||||
'If this does not apply, you can continue to the next step',
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -661,30 +661,30 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50], // Same note about blue migration
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.tagActive,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(UiIcons.info, color: Color(0xFF2563EB), size: 20),
|
||||
SizedBox(width: 12),
|
||||
const Icon(UiIcons.info, color: UiColors.primary, size: 20),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'If your total income will be \$200,000 or less (\$400,000 if married filing jointly), you may claim credits for dependents.',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF1D4ED8)),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
@@ -715,10 +715,10 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
if (_totalCredits(state) > 0) ...<Widget>[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDCFCE7),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -770,16 +770,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDCFCE7),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.tagSuccess,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
badge,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF15803D),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: UiTypography.footnote2b.textSuccess,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -834,7 +830,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'These adjustments are optional. You can skip them if they don\'t apply.',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTextField(
|
||||
@@ -848,7 +844,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'Include interest, dividends, retirement income',
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -863,7 +859,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'If you expect to claim deductions other than the standard deduction',
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -878,7 +874,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'Any additional tax you want withheld each pay period',
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -890,10 +886,10 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
@@ -927,22 +923,20 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.accent.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Under penalties of perjury, I declare that this certificate, to the best of my knowledge and belief, is true, correct, and complete.',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFFB45309)),
|
||||
style: UiTypography.body3r.textWarning.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'Signature (type your full name) *',
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
@@ -950,44 +944,46 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: state.signature.length),
|
||||
),
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().signatureChanged(val),
|
||||
onChanged: (String val) =>
|
||||
context.read<FormW4Cubit>().signatureChanged(val),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your full name',
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: const BorderSide(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(fontFamily: 'Cursive', fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Date',
|
||||
style: UiTypography.body3m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3F4F6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Text(
|
||||
@@ -1007,7 +1003,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
@@ -1035,7 +1031,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
|
||||
Widget _buildFooter(BuildContext context, FormW4State state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
@@ -1046,24 +1042,30 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
if (state.currentStep > 0)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.only(right: UiConstants.space3),
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _handleBack(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.arrowLeft, size: 16, color: UiColors.textPrimary),
|
||||
const Icon(
|
||||
UiIcons.arrowLeft,
|
||||
size: 16,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Back',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1073,16 +1075,20 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_canProceed(state) && state.status != FormW4Status.submitting)
|
||||
onPressed: (
|
||||
_canProceed(state) &&
|
||||
state.status != FormW4Status.submitting)
|
||||
? () => _handleNext(context, state.currentStep)
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
disabledBackgroundColor: Colors.grey[300],
|
||||
foregroundColor: UiColors.bgPopup,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
disabledBackgroundColor: UiColors.bgSecondary,
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
@@ -1091,7 +1097,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: UiColors.bgPopup,
|
||||
color: UiColors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
@@ -1105,7 +1111,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
if (state.currentStep < _steps.length - 1) ...<Widget>[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.white),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -22,7 +22,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
),
|
||||
title: Text(
|
||||
'Tax Documents',
|
||||
style: UiTypography.headline3m.copyWith(color: UiColors.bgPopup),
|
||||
style: UiTypography.headline3m.textSecondary,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(24),
|
||||
@@ -38,7 +38,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
child: Text(
|
||||
'Complete required forms to start working',
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.bgPopup.withOpacity(0.8),
|
||||
color: UiColors.primaryForeground.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -120,15 +120,11 @@ class TaxFormsPage extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Document Progress',
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
'$completedCount/$totalCount',
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -180,7 +176,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Center(child: Text(icon, style: UiTypography.headline1m)),
|
||||
@@ -195,9 +191,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
form.title,
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
_buildStatusBadge(form.status),
|
||||
],
|
||||
@@ -205,17 +199,14 @@ class TaxFormsPage extends StatelessWidget {
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
form.subtitle ?? '',
|
||||
style: UiTypography.body2m.copyWith(
|
||||
style: UiTypography.body2m.textSecondary.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
form.description ?? '',
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -256,9 +247,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
'Completed',
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.textSuccess,
|
||||
),
|
||||
style: UiTypography.footnote2b.textSuccess,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -280,9 +269,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
'In Progress',
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.textWarning,
|
||||
),
|
||||
style: UiTypography.footnote2b.textWarning,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -299,9 +286,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
'Not Started',
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote2b.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -325,16 +310,12 @@ class TaxFormsPage extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Why are these needed?',
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'I-9 and W-4 forms are required by federal law to verify your employment eligibility and set up correct tax withholding.',
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -37,7 +37,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
),
|
||||
title: Text(
|
||||
strings.title,
|
||||
style: UiTypography.headline3m.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
@@ -165,7 +165,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.08),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Row(
|
||||
@@ -179,15 +179,12 @@ class BankAccountPage extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
strings.secure_title,
|
||||
style: UiTypography.body2r.copyWith( // Was body2
|
||||
fontWeight: FontWeight.w500,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
strings.secure_subtitle,
|
||||
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary), // Was bodySmall
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -221,7 +218,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.1),
|
||||
color: primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: const Center(
|
||||
@@ -238,10 +235,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
account.bankName,
|
||||
style: UiTypography.body2r.copyWith( // Was body2
|
||||
fontWeight: FontWeight.w500,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
strings.account_ending(
|
||||
@@ -249,9 +243,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
? account.last4!
|
||||
: '----',
|
||||
),
|
||||
style: UiTypography.body2r.copyWith( // Was body2
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -261,7 +253,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.15),
|
||||
color: primaryColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
@@ -270,10 +262,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
strings.primary,
|
||||
style: UiTypography.body3r.copyWith( // Was bodySmall
|
||||
fontWeight: FontWeight.w500,
|
||||
color: primaryColor,
|
||||
),
|
||||
style: UiTypography.body3m.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -43,7 +43,7 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.strings.add_new_account,
|
||||
style: UiTypography.headline4m.copyWith(color: UiColors.textPrimary), // Was header4
|
||||
style: UiTypography.headline4m.textPrimary, // Was header4
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiTextField(
|
||||
@@ -71,8 +71,7 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||
child: Text(
|
||||
widget.strings.account_type,
|
||||
style: UiTypography.body2r.copyWith( // Was body2
|
||||
color: UiColors.textSecondary, fontWeight: FontWeight.w500),
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
@@ -122,7 +121,7 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? UiColors.primary.withOpacity(0.05)
|
||||
? UiColors.primary.withValues(alpha: 0.05)
|
||||
: UiColors.bgPopup, // Was surface
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
@@ -133,8 +132,7 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.body2r.copyWith( // Was body2
|
||||
fontWeight: FontWeight.w600,
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: isSelected ? UiColors.primary : UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -41,9 +41,7 @@ class _TimeCardPageState extends State<TimeCardPage> {
|
||||
),
|
||||
title: Text(
|
||||
t.staff_time_card.title,
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
|
||||
@@ -19,22 +19,22 @@ class TimesheetCard extends StatelessWidget {
|
||||
|
||||
switch (status) {
|
||||
case TimeCardStatus.approved:
|
||||
statusBg = UiColors.textSuccess.withOpacity(0.12);
|
||||
statusBg = UiColors.tagSuccess;
|
||||
statusColor = UiColors.textSuccess;
|
||||
statusText = t.staff_time_card.status.approved;
|
||||
break;
|
||||
case TimeCardStatus.disputed:
|
||||
statusBg = UiColors.destructive.withOpacity(0.12);
|
||||
statusBg = UiColors.destructive.withValues(alpha: 0.12);
|
||||
statusColor = UiColors.destructive;
|
||||
statusText = t.staff_time_card.status.disputed;
|
||||
break;
|
||||
case TimeCardStatus.paid:
|
||||
statusBg = UiColors.primary.withOpacity(0.12);
|
||||
statusBg = UiColors.primary.withValues(alpha: 0.12);
|
||||
statusColor = UiColors.primary;
|
||||
statusText = t.staff_time_card.status.paid;
|
||||
break;
|
||||
case TimeCardStatus.pending:
|
||||
statusBg = UiColors.textWarning.withOpacity(0.12);
|
||||
statusBg = UiColors.tagPending;
|
||||
statusColor = UiColors.textWarning;
|
||||
statusText = t.staff_time_card.status.pending;
|
||||
break;
|
||||
@@ -61,15 +61,11 @@ class TimesheetCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
timesheet.shiftTitle,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
timesheet.clientName,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -116,13 +112,11 @@ class TimesheetCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'${timesheet.totalHours.toStringAsFixed(1)} ${t.staff_time_card.hours} @ \$${timesheet.hourlyRate.toStringAsFixed(2)}${t.staff_time_card.per_hr}',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
'\$${timesheet.totalPay.toStringAsFixed(2)}',
|
||||
style: UiTypography.title2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.title2b.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -163,7 +157,7 @@ class _IconText extends StatelessWidget {
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
text,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -32,9 +32,7 @@ class AttirePage extends StatelessWidget {
|
||||
),
|
||||
title: Text(
|
||||
t.staff_profile_attire.title,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
|
||||
@@ -11,8 +11,8 @@ class AttireInfoCard extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
color: UiColors.primary.withValues(alpha: 0.08),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -26,16 +26,12 @@ class AttireInfoCard extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_profile_attire.info_card.title,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
t.staff_profile_attire.info_card.description,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -24,12 +24,12 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
||||
onPressed: () => Modular.to.pop(),
|
||||
),
|
||||
title: Text(
|
||||
'Emergency Contact',
|
||||
style: UiTypography.title1m.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
@@ -62,11 +62,11 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(UiConstants.space6),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
children: [
|
||||
const EmergencyContactInfoBanner(),
|
||||
SizedBox(height: UiConstants.space6),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
...state.contacts.asMap().entries.map(
|
||||
(entry) => EmergencyContactFormItem(
|
||||
index: entry.key,
|
||||
@@ -75,7 +75,7 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const EmergencyContactAddButton(),
|
||||
SizedBox(height: UiConstants.space16),
|
||||
const SizedBox(height: UiConstants.space16),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,20 +12,20 @@ class EmergencyContactAddButton extends StatelessWidget {
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
context.read<EmergencyContactBloc>().add(EmergencyContactAdded()),
|
||||
icon: Icon(UiIcons.add, size: 20.0),
|
||||
icon: const Icon(UiIcons.add, size: 20.0),
|
||||
label: Text(
|
||||
'Add Another Contact',
|
||||
style: UiTypography.title2b,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.primary,
|
||||
padding: EdgeInsets.symmetric(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space6,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
side: BorderSide(color: UiColors.primary),
|
||||
side: const BorderSide(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -19,8 +19,8 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: UiConstants.space4),
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
@@ -30,33 +30,27 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildLabel('Full Name'),
|
||||
_buildTextField(
|
||||
initialValue: contact.name,
|
||||
hint: 'Contact name',
|
||||
icon: UiIcons.user,
|
||||
onChanged: (val) => context.read<EmergencyContactBloc>().add(
|
||||
EmergencyContactUpdated(
|
||||
index,
|
||||
contact.copyWith(name: val),
|
||||
),
|
||||
),
|
||||
EmergencyContactUpdated(index, contact.copyWith(name: val)),
|
||||
),
|
||||
),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildLabel('Phone Number'),
|
||||
_buildTextField(
|
||||
initialValue: contact.phone,
|
||||
hint: '+1 (555) 000-0000',
|
||||
icon: UiIcons.phone,
|
||||
onChanged: (val) => context.read<EmergencyContactBloc>().add(
|
||||
EmergencyContactUpdated(
|
||||
index,
|
||||
contact.copyWith(phone: val),
|
||||
),
|
||||
),
|
||||
EmergencyContactUpdated(index, contact.copyWith(phone: val)),
|
||||
),
|
||||
),
|
||||
SizedBox(height: UiConstants.space4),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildLabel('Relationship'),
|
||||
_buildDropdown(
|
||||
context,
|
||||
@@ -65,11 +59,11 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
context.read<EmergencyContactBloc>().add(
|
||||
EmergencyContactUpdated(
|
||||
index,
|
||||
contact.copyWith(relationship: val),
|
||||
),
|
||||
);
|
||||
EmergencyContactUpdated(
|
||||
index,
|
||||
contact.copyWith(relationship: val),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -85,7 +79,7 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
required ValueChanged<RelationshipType?> onChanged,
|
||||
}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
@@ -99,13 +93,13 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
dropdownColor: UiColors.bgPopup,
|
||||
icon: Icon(UiIcons.chevronDown, color: UiColors.iconSecondary),
|
||||
icon: const Icon(UiIcons.chevronDown, color: UiColors.iconSecondary),
|
||||
items: items.map((type) {
|
||||
return DropdownMenuItem<RelationshipType>(
|
||||
value: type,
|
||||
child: Text(
|
||||
_formatRelationship(type),
|
||||
style: UiTypography.body1r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -116,11 +110,15 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
String _formatRelationship(RelationshipType type) {
|
||||
switch(type) {
|
||||
case RelationshipType.family: return 'Family';
|
||||
case RelationshipType.spouse: return 'Spouse';
|
||||
case RelationshipType.friend: return 'Friend';
|
||||
case RelationshipType.other: return 'Other';
|
||||
switch (type) {
|
||||
case RelationshipType.family:
|
||||
return 'Family';
|
||||
case RelationshipType.spouse:
|
||||
return 'Spouse';
|
||||
case RelationshipType.friend:
|
||||
return 'Friend';
|
||||
case RelationshipType.other:
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,22 +126,17 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Contact ${index + 1}',
|
||||
style: UiTypography.title2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text('Contact ${index + 1}', style: UiTypography.title2m.textPrimary),
|
||||
if (totalContacts > 1)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
icon: const Icon(
|
||||
UiIcons.delete,
|
||||
color: UiColors.textError,
|
||||
size: 20.0,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<EmergencyContactBloc>()
|
||||
.add(EmergencyContactRemoved(index)),
|
||||
onPressed: () => context.read<EmergencyContactBloc>().add(
|
||||
EmergencyContactRemoved(index),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -151,13 +144,8 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
|
||||
Widget _buildLabel(String label) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: UiConstants.space2),
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||
child: Text(label, style: UiTypography.body2m.textSecondary),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,16 +157,16 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
}) {
|
||||
return TextFormField(
|
||||
initialValue: initialValue,
|
||||
style: UiTypography.body1r.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(color: UiColors.textPlaceholder),
|
||||
hintStyle: const TextStyle(color: UiColors.textPlaceholder),
|
||||
prefixIcon: Icon(icon, color: UiColors.textSecondary, size: 20.0),
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
borderSide: BorderSide(color: UiColors.border),
|
||||
@@ -196,4 +184,3 @@ class EmergencyContactFormItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ class EmergencyContactInfoBanner extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.accent.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
color: UiColors.accent.withValues(alpha: 0.2),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
'Please provide at least one emergency contact. This information will only be used in case of an emergency during your shifts.',
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,19 +30,18 @@ class EmergencyContactSaveButton extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
final isLoading = state.status == EmergencyContactStatus.saving;
|
||||
return Container(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: UiButton.primary(
|
||||
fullWidth: true,
|
||||
onPressed: state.isValid && !isLoading
|
||||
? () => _onSave(context)
|
||||
: null,
|
||||
onPressed:
|
||||
state.isValid && !isLoading ? () => _onSave(context) : null,
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
? const SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
|
||||
@@ -80,16 +80,16 @@ class ExperiencePage extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ExperienceSectionTitle(title: i18n.industries_title),
|
||||
Text(
|
||||
i18n.industries_subtitle,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
@@ -97,23 +97,26 @@ class ExperiencePage extends StatelessWidget {
|
||||
.map(
|
||||
(i) => UiChip(
|
||||
label: _getIndustryLabel(i18n.industries, i),
|
||||
isSelected: state.selectedIndustries.contains(i),
|
||||
onTap: () => BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceIndustryToggled(i)),
|
||||
variant: state.selectedIndustries.contains(i)
|
||||
? UiChipVariant.primary
|
||||
: UiChipVariant.secondary,
|
||||
isSelected:
|
||||
state.selectedIndustries.contains(i),
|
||||
onTap: () =>
|
||||
BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceIndustryToggled(i)),
|
||||
variant:
|
||||
state.selectedIndustries.contains(i)
|
||||
? UiChipVariant.primary
|
||||
: UiChipVariant.secondary,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
SizedBox(height: UiConstants.space6),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
ExperienceSectionTitle(title: i18n.skills_title),
|
||||
Text(
|
||||
i18n.skills_subtitle,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
SizedBox(height: UiConstants.space3),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
@@ -121,19 +124,22 @@ class ExperiencePage extends StatelessWidget {
|
||||
.map(
|
||||
(s) => UiChip(
|
||||
label: _getSkillLabel(i18n.skills, s),
|
||||
isSelected: state.selectedSkills.contains(s.value),
|
||||
onTap: () => BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceSkillToggled(s.value)),
|
||||
variant: state.selectedSkills.contains(s.value)
|
||||
? UiChipVariant.primary
|
||||
: UiChipVariant.secondary,
|
||||
isSelected:
|
||||
state.selectedSkills.contains(s.value),
|
||||
onTap: () =>
|
||||
BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceSkillToggled(s.value)),
|
||||
variant:
|
||||
state.selectedSkills.contains(s.value)
|
||||
? UiChipVariant.primary
|
||||
: UiChipVariant.secondary,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildSaveButton(context, state, i18n),
|
||||
],
|
||||
@@ -155,9 +161,9 @@ class ExperiencePage extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
i18n.custom_skills_title,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Wrap(
|
||||
spacing: UiConstants.space2,
|
||||
runSpacing: UiConstants.space2,
|
||||
@@ -172,10 +178,14 @@ class ExperiencePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSaveButton(BuildContext context, ExperienceState state, dynamic i18n) {
|
||||
Widget _buildSaveButton(
|
||||
BuildContext context,
|
||||
ExperienceState state,
|
||||
dynamic i18n,
|
||||
) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.bgPopup,
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
),
|
||||
@@ -183,16 +193,21 @@ class ExperiencePage extends StatelessWidget {
|
||||
child: UiButton.primary(
|
||||
onPressed: state.status == ExperienceStatus.loading
|
||||
? null
|
||||
: () => BlocProvider.of<ExperienceBloc>(context).add(ExperienceSubmitted()),
|
||||
: () => BlocProvider.of<ExperienceBloc>(context)
|
||||
.add(ExperienceSubmitted()),
|
||||
fullWidth: true,
|
||||
text: state.status == ExperienceStatus.loading ? null : i18n.save_button,
|
||||
text: state.status == ExperienceStatus.loading
|
||||
? null
|
||||
: i18n.save_button,
|
||||
child: state.status == ExperienceStatus.loading
|
||||
? SizedBox(
|
||||
? const SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(UiColors.white), // UiColors.primaryForeground is white mostly
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
UiColors.white,
|
||||
), // UiColors.primaryForeground is white mostly
|
||||
),
|
||||
)
|
||||
: null,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -56,13 +55,16 @@ class PersonalInfoPage extends StatelessWidget {
|
||||
backgroundColor: UiColors.bgPopup,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
||||
icon: const Icon(
|
||||
UiIcons.chevronLeft,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
onPressed: () => Modular.to.pop(),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
),
|
||||
title: Text(
|
||||
i18n.title,
|
||||
style: UiTypography.title1m.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
@@ -86,9 +88,7 @@ class PersonalInfoPage extends StatelessWidget {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Failed to load personal information',
|
||||
style: UiTypography.body1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class _FieldLabel extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,6 @@ class _FieldLabel extends StatelessWidget {
|
||||
/// A read-only field widget for displaying non-editable information.
|
||||
/// A read-only field widget for displaying non-editable information.
|
||||
class _ReadOnlyField extends StatelessWidget {
|
||||
|
||||
const _ReadOnlyField({required this.value});
|
||||
final String value;
|
||||
|
||||
@@ -120,7 +119,7 @@ class _ReadOnlyField extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -129,7 +128,6 @@ class _ReadOnlyField extends StatelessWidget {
|
||||
/// An editable text field widget.
|
||||
/// An editable text field widget.
|
||||
class _EditableField extends StatelessWidget {
|
||||
|
||||
const _EditableField({
|
||||
required this.controller,
|
||||
required this.hint,
|
||||
@@ -150,10 +148,10 @@ class _EditableField extends StatelessWidget {
|
||||
enabled: enabled,
|
||||
keyboardType: keyboardType,
|
||||
autofillHints: autofillHints,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
hintStyle: UiTypography.body2r.textSecondary,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space3,
|
||||
vertical: UiConstants.space3,
|
||||
|
||||
@@ -28,7 +28,8 @@ class ProfilePhotoWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||
final TranslationsStaffOnboardingPersonalInfoEn i18n =
|
||||
t.staff.onboarding.personal_info;
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
@@ -41,7 +42,7 @@ class ProfilePhotoWidget extends StatelessWidget {
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
child: photoUrl != null
|
||||
? ClipOval(
|
||||
@@ -53,9 +54,7 @@ class ProfilePhotoWidget extends StatelessWidget {
|
||||
: Center(
|
||||
child: Text(
|
||||
fullName.isNotEmpty ? fullName[0].toUpperCase() : '?',
|
||||
style: UiTypography.displayL.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
style: UiTypography.displayL.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -71,7 +70,7 @@ class ProfilePhotoWidget extends StatelessWidget {
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.textPrimary.withOpacity(0.1),
|
||||
color: UiColors.textPrimary.withValues(alpha: 0.1),
|
||||
blurRadius: UiConstants.space1,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -92,7 +91,7 @@ class ProfilePhotoWidget extends StatelessWidget {
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Text(
|
||||
i18n.change_photo_hint,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ class ShiftsRepositoryImpl
|
||||
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _toDateTime(app.createdAt);
|
||||
|
||||
// Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED)
|
||||
// Override status to reflect the application state (e.g., CHECKED_OUT, CONFIRMED)
|
||||
final bool hasCheckIn = app.checkInTime != null;
|
||||
final bool hasCheckOut = app.checkOutTime != null;
|
||||
dc.ApplicationStatus? appStatus;
|
||||
@@ -187,7 +187,7 @@ class ShiftsRepositoryImpl
|
||||
? 'completed'
|
||||
: hasCheckIn
|
||||
? 'checked_in'
|
||||
: _mapStatus(appStatus ?? dc.ApplicationStatus.ACCEPTED);
|
||||
: _mapStatus(appStatus ?? dc.ApplicationStatus.CONFIRMED);
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
@@ -223,7 +223,6 @@ class ShiftsRepositoryImpl
|
||||
|
||||
String _mapStatus(dc.ApplicationStatus status) {
|
||||
switch (status) {
|
||||
case dc.ApplicationStatus.ACCEPTED:
|
||||
case dc.ApplicationStatus.CONFIRMED:
|
||||
return 'confirmed';
|
||||
case dc.ApplicationStatus.PENDING:
|
||||
@@ -477,7 +476,7 @@ class ShiftsRepositoryImpl
|
||||
shiftId: shiftId,
|
||||
staffId: staffId,
|
||||
roleId: targetRoleId,
|
||||
status: dc.ApplicationStatus.ACCEPTED,
|
||||
status: dc.ApplicationStatus.CONFIRMED,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
)
|
||||
// TODO: this should be PENDING so a vendor can accept it.
|
||||
@@ -518,7 +517,7 @@ class ShiftsRepositoryImpl
|
||||
|
||||
@override
|
||||
Future<void> acceptShift(String shiftId) async {
|
||||
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
|
||||
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -281,15 +281,20 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
|
||||
|
||||
List<Shift> _filterPastShifts(List<Shift> shifts) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
return shifts.where((shift) {
|
||||
if (shift.date.isEmpty) return false;
|
||||
try {
|
||||
final shiftDate = DateTime.parse(shift.date);
|
||||
return shiftDate.isAfter(now);
|
||||
final dateOnly = DateTime(
|
||||
shiftDate.year,
|
||||
shiftDate.month,
|
||||
shiftDate.day,
|
||||
);
|
||||
return !dateOnly.isBefore(today);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
|
||||
Widget _buildStatCard(IconData icon, String value, String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.background,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
@@ -76,21 +76,19 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: UiColors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 20, color: UiColors.iconSecondary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.title1m.copyWith(color: UiColors.textPrimary),
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -99,29 +97,21 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
|
||||
Widget _buildTimeBox(String label, String time) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: UiColors.background,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
_formatTime(time),
|
||||
style: UiTypography.display2m.copyWith(
|
||||
fontSize: 20,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline2m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -149,7 +139,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: UiColors.tagSuccess,
|
||||
backgroundColor: UiColors.success,
|
||||
),
|
||||
);
|
||||
Modular.to.toShifts(selectedDate: state.shiftDate);
|
||||
@@ -158,7 +148,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: const Color(0xFFEF4444),
|
||||
backgroundColor: UiColors.destructive,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -203,7 +193,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -211,16 +201,11 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
"VENDOR",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -229,7 +214,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
child: displayShift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
6,
|
||||
UiConstants.radiusMdValue,
|
||||
),
|
||||
child: Image.network(
|
||||
displayShift.logoUrl!,
|
||||
@@ -244,33 +229,26 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
displayShift.clientName,
|
||||
style: UiTypography.headline5m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline5m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Date Section
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
"SHIFT DATE",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
@@ -278,104 +256,44 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
size: 20,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
_formatDate(displayShift.date),
|
||||
style: UiTypography.headline5m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.headline5m.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Worker Capacity / Open Slots
|
||||
if ((displayShift.requiredSlots ?? 0) > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0FDF4), // green-50
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFBBF7D0),
|
||||
), // green-200
|
||||
color: UiColors.success.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.people_alt_outlined,
|
||||
size: 20,
|
||||
color: Color(0xFF15803D),
|
||||
), // green-700, using Material Icon as generic fallback
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"$openSlots spots remaining",
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: const Color(0xFF15803D),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${displayShift.filledSlots ?? 0} filled out of ${displayShift.requiredSlots}",
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: const Color(0xFF166534),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
UiIcons.users,
|
||||
size: 16,
|
||||
color: UiColors.success,
|
||||
),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: LinearProgressIndicator(
|
||||
value: (displayShift.requiredSlots! > 0)
|
||||
? (displayShift.filledSlots ?? 0) /
|
||||
displayShift.requiredSlots!
|
||||
: 0,
|
||||
backgroundColor: Colors.white,
|
||||
color: const Color(0xFF15803D),
|
||||
minHeight: 6,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
"$openSlots slots remaining",
|
||||
style: UiTypography.footnote1m.textSuccess,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Stats Grid
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.72,
|
||||
children: [
|
||||
_buildStatCard(
|
||||
UiIcons.dollar,
|
||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||
"Total Pay",
|
||||
),
|
||||
_buildStatCard(
|
||||
UiIcons.dollar,
|
||||
"\$${displayShift.hourlyRate.toInt()}",
|
||||
"Per Hour",
|
||||
),
|
||||
_buildStatCard(
|
||||
UiIcons.clock,
|
||||
"${duration.toInt()}h",
|
||||
"Duration",
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Shift Timing
|
||||
// Time Section
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -384,7 +302,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
displayShift.startTime,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: _buildTimeBox(
|
||||
"END TIME",
|
||||
@@ -393,129 +311,142 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Location
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// Quick Info Grid
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
"LOCATION",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
UiIcons.dollar,
|
||||
"\$${displayShift.hourlyRate.toStringAsFixed(0)}/hr",
|
||||
"Base Rate",
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayShift.location.isEmpty
|
||||
? "TBD"
|
||||
: displayShift.location,
|
||||
style: UiTypography.title1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
displayShift.location.isEmpty
|
||||
? "TBD"
|
||||
: displayShift.locationAddress,
|
||||
style: UiTypography.title1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
UiIcons.clock,
|
||||
"${duration.toInt()} hours",
|
||||
"Duration",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
UiIcons.wallet,
|
||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||
"Est. Total",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// Additional Info
|
||||
if (displayShift.description != null) ...[
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"ADDITIONAL INFO",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
displayShift.description!,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Location Section
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"LOCATION",
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
if (displayShift.status != 'confirmed' &&
|
||||
displayShift.hasApplied != true &&
|
||||
(displayShift.requiredSlots == null ||
|
||||
displayShift.filledSlots == null ||
|
||||
displayShift.filledSlots! <
|
||||
displayShift.requiredSlots!))
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _declineShift(
|
||||
context,
|
||||
displayShift!.id,
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFEF4444),
|
||||
side: const BorderSide(
|
||||
color: Color(0xFFEF4444),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
child: const Text("Decline"),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _bookShift(
|
||||
context,
|
||||
displayShift!,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.mapPin,
|
||||
color: UiColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayShift.location,
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
Text(
|
||||
displayShift.locationAddress,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(
|
||||
0xFF10B981,
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
const Divider(),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
UiIcons.arrowRight,
|
||||
size: 16,
|
||||
),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
label: const Text("Open in Maps"),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.primary,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
child: const Text("Book Shift"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom + 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// Description / Instructions
|
||||
if ((displayShift.description ?? '').isNotEmpty) ...[
|
||||
Text(
|
||||
"JOB DESCRIPTION",
|
||||
style: UiTypography.titleUppercase4b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
displayShift.description!,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom Action Bar
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space4,
|
||||
UiConstants.space5,
|
||||
MediaQuery.of(context).padding.bottom + UiConstants.space4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildBottomButton(displayShift, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -552,7 +483,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF10B981),
|
||||
foregroundColor: UiColors.success,
|
||||
),
|
||||
child: const Text('Book'),
|
||||
),
|
||||
@@ -581,7 +512,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
).add(DeclineShiftDetailsEvent(id));
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFEF4444),
|
||||
foregroundColor: UiColors.destructive,
|
||||
),
|
||||
child: const Text('Decline'),
|
||||
),
|
||||
@@ -608,29 +539,23 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
width: 36,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
shift.title,
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${_formatDate(shift.date)} • ${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}',
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (shift.clientName.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
shift.clientName,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -647,4 +572,150 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
_actionDialogOpen = false;
|
||||
}
|
||||
|
||||
Widget _buildBottomButton(Shift shift, BuildContext context) {
|
||||
final String status = shift.status ?? 'open';
|
||||
|
||||
if (status == 'confirmed') {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _openCancelDialog(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.destructive,
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("CANCEL SHIFT", style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Modular.to.toClockIn(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.success,
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("CLOCK IN", style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (status == 'pending') {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => BlocProvider.of<ShiftDetailsBloc>(context).add(DeclineShiftDetailsEvent(shift.id)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
side: const BorderSide(color: UiColors.destructive),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
child: Text("DECLINE", style: UiTypography.body2b.textError),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => BlocProvider.of<ShiftDetailsBloc>(context).add(BookShiftDetailsEvent(shift.id, roleId: shift.roleId)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("ACCEPT SHIFT", style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (status == 'open' || status == 'available') {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _declineShift(context, shift.id),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.textSecondary,
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
child: Text("DECLINE", style: UiTypography.body2b.textSecondary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _bookShift(context, shift),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text("APPLY NOW", style: UiTypography.body2b.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
void _openCancelDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Cancel Shift'),
|
||||
content: const Text('Are you sure you want to cancel this shift?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Modular.to.pop(),
|
||||
child: const Text('No'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Modular.to.pop();
|
||||
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||
DeclineShiftDetailsEvent(widget.shiftId),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
),
|
||||
child: const Text('Yes, cancel it'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import '../blocs/shifts/shifts_bloc.dart';
|
||||
import '../widgets/tabs/my_shifts_tab.dart';
|
||||
import '../widgets/tabs/find_shifts_tab.dart';
|
||||
import '../widgets/tabs/history_shifts_tab.dart';
|
||||
import '../styles/shifts_styles.dart';
|
||||
|
||||
class ShiftsPage extends StatefulWidget {
|
||||
final String? initialTab;
|
||||
@@ -119,29 +118,25 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
// Note: Calendar logic moved to MyShiftsTab
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.krowBackground,
|
||||
backgroundColor: UiColors.background,
|
||||
body: Column(
|
||||
children: [
|
||||
// Header (Blue)
|
||||
Container(
|
||||
color: AppColors.krowBlue,
|
||||
color: UiColors.primary,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20,
|
||||
MediaQuery.of(context).padding.top + 10,
|
||||
20,
|
||||
20,
|
||||
UiConstants.space5,
|
||||
MediaQuery.of(context).padding.top + UiConstants.space2,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 16,
|
||||
spacing: UiConstants.space4,
|
||||
children: [
|
||||
Text(
|
||||
t.staff_shifts.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: UiTypography.display1b.white,
|
||||
),
|
||||
|
||||
// Tabs
|
||||
@@ -155,17 +150,16 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
showCount: myShiftsLoaded,
|
||||
enabled: !blockTabsForFind,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildTab(
|
||||
"find",
|
||||
t.staff_shifts.tabs.find_work,
|
||||
UiIcons.search,
|
||||
availableJobs
|
||||
.length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs.
|
||||
availableJobs.length,
|
||||
showCount: availableLoaded,
|
||||
enabled: baseLoaded,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildTab(
|
||||
"history",
|
||||
t.staff_shifts.tabs.history,
|
||||
@@ -276,12 +270,15 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space2,
|
||||
horizontal: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? Colors.white
|
||||
: Colors.white.withAlpha((0.2 * 255).round()),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
? UiColors.white
|
||||
: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -291,23 +288,17 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
icon,
|
||||
size: 14,
|
||||
color: !enabled
|
||||
? Colors.white.withAlpha((0.5 * 255).round())
|
||||
? UiColors.white.withValues(alpha: 0.5)
|
||||
: isActive
|
||||
? AppColors.krowBlue
|
||||
: Colors.white,
|
||||
? UiColors.primary
|
||||
: UiColors.white,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: !enabled
|
||||
? Colors.white.withAlpha((0.5 * 255).round())
|
||||
: isActive
|
||||
? AppColors.krowBlue
|
||||
: Colors.white,
|
||||
style: (isActive ? UiTypography.body3m.copyWith(color: UiColors.primary) : UiTypography.body3m.white).copyWith(
|
||||
color: !enabled ? UiColors.white.withValues(alpha: 0.5) : null,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -316,23 +307,21 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
horizontal: UiConstants.space1,
|
||||
vertical: 2,
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? AppColors.krowBlue.withAlpha((0.1 * 255).round())
|
||||
: Colors.white.withAlpha((0.2 * 255).round()),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
? UiColors.primary.withValues(alpha: 0.1)
|
||||
: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$count",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isActive ? AppColors.krowBlue : Colors.white,
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: isActive ? UiColors.primary : UiColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -118,14 +118,14 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
Modular.to.pushShiftDetails(widget.shift);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -142,7 +142,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
// Status Badge
|
||||
if (statusText.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||
child: Row(
|
||||
children: [
|
||||
if (statusIcon != null)
|
||||
@@ -173,14 +173,14 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
),
|
||||
// Shift Type Badge for available/pending shifts
|
||||
if (status == 'open' || status == 'pending') ...[
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
@@ -205,20 +205,20 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
UiColors.primary.withOpacity(0.09),
|
||||
UiColors.primary.withOpacity(0.03),
|
||||
UiColors.primary.withValues(alpha: 0.09),
|
||||
UiColors.primary.withValues(alpha: 0.03),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
color: UiColors.primary.withOpacity(0.09),
|
||||
color: UiColors.primary.withValues(alpha: 0.09),
|
||||
),
|
||||
),
|
||||
child: widget.shift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
child: Image.network(
|
||||
widget.shift.logoUrl!,
|
||||
fit: BoxFit.contain,
|
||||
@@ -232,7 +232,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
// Details
|
||||
Expanded(
|
||||
@@ -249,42 +249,34 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
children: [
|
||||
Text(
|
||||
widget.shift.title,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
widget.shift.clientName,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||
style: UiTypography.title1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
"\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h",
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
|
||||
// Date & Time - Multi-Day or Single Day
|
||||
if (widget.shift.durationDays != null &&
|
||||
@@ -332,11 +324,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatDate(widget.shift.date),
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
const Icon(
|
||||
UiIcons.clock,
|
||||
size: 12,
|
||||
@@ -345,9 +335,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}",
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -368,9 +356,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
widget.shift.locationAddress.isNotEmpty
|
||||
? widget.shift.locationAddress
|
||||
: widget.shift.location,
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class ShiftAssignmentCard extends StatelessWidget {
|
||||
final Shift shift;
|
||||
@@ -66,12 +65,12 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
@@ -81,7 +80,7 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -97,20 +96,20 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
UiColors.primary.withOpacity(0.09),
|
||||
UiColors.primary.withOpacity(0.03),
|
||||
UiColors.primary.withValues(alpha: 0.09),
|
||||
UiColors.primary.withValues(alpha: 0.03),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
color: UiColors.primary.withOpacity(0.09),
|
||||
color: UiColors.primary.withValues(alpha: 0.09),
|
||||
),
|
||||
),
|
||||
child: shift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
child: Image.network(
|
||||
shift.logoUrl!,
|
||||
fit: BoxFit.contain,
|
||||
@@ -124,7 +123,7 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
// Details
|
||||
Expanded(
|
||||
@@ -140,42 +139,34 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
shift.title,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
shift.clientName,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"\$${totalPay.toStringAsFixed(0)}",
|
||||
style: UiTypography.title1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
style: UiTypography.title1m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
"\$${shift.hourlyRate.toInt()}/hr · ${hours.toInt()}h",
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// Date & Time
|
||||
Row(
|
||||
@@ -188,11 +179,9 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatDate(shift.date),
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
const Icon(
|
||||
UiIcons.clock,
|
||||
size: 12,
|
||||
@@ -201,9 +190,7 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -223,9 +210,7 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
shift.locationAddress.isNotEmpty
|
||||
? shift.locationAddress
|
||||
: shift.location,
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -240,38 +225,55 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
// Actions
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space2),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.secondary,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(UiConstants.radiusBase),
|
||||
bottomRight: Radius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
child: TextButton(
|
||||
onPressed: onDecline,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.iconSecondary,
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
),
|
||||
child: Text(
|
||||
"Decline", // Fallback if translation is broken
|
||||
style: UiTypography.body2m.textError,
|
||||
),
|
||||
child: Text(t.staff_shifts.action.decline),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: onConfirm,
|
||||
onPressed: isConfirming ? null : onConfirm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
foregroundColor: UiColors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
),
|
||||
child: Text(t.staff_shifts.action.confirm),
|
||||
child: isConfirming
|
||||
? const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: UiColors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Accept", // Fallback
|
||||
style: UiTypography.body2m.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user