Initial commit: Krow Worker App

This commit is contained in:
R-Bharathraj
2026-09-17 22:26:11 +05:30
commit cafffd27e0
113 changed files with 13067 additions and 0 deletions

51
lib/core/formatters.dart Normal file
View File

@@ -0,0 +1,51 @@
import 'package:intl/intl.dart';
final _money = NumberFormat.currency(locale: 'en_US', symbol: r'$');
final _time = DateFormat('h:mm a');
final _dayFull = DateFormat('EEEE d MMMM');
final _dayShort = DateFormat('EEE, d MMM yyyy');
final _monthYear = DateFormat('MMMM yyyy');
abstract final class Fmt {
static String money(num v) => _money.format(v);
static String time(DateTime d) => _time.format(d);
static String window(DateTime start, DateTime end) =>
'${_time.format(start)} - ${_time.format(end)}';
static String dayFull(DateTime d) => _dayFull.format(d);
static String dayShort(DateTime d) => _dayShort.format(d);
static String monthYear(DateTime d) => _monthYear.format(d);
/// `5h 48m`, the form used on every hours figure.
static String duration(Duration d) {
final h = d.inHours;
final m = d.inMinutes.remainder(60);
if (h == 0) return '${m}m';
return '${h}h ${m.toString().padLeft(2, '0')}m';
}
/// `05:48:12`, for the running clock-in timer.
static String stopwatch(Duration d) {
final h = d.inHours.toString().padLeft(2, '0');
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$h:$m:$s';
}
/// A human countdown to a shift start: `in 41 min`, `in 3 hr`, `tomorrow`.
static String untilStart(DateTime start, {DateTime? now}) {
final from = now ?? DateTime.now();
final diff = start.difference(from);
if (diff.isNegative) return 'Started';
if (diff.inMinutes < 60) return 'In ${diff.inMinutes} min';
if (diff.inHours < 24) return 'In ${diff.inHours} hr';
return 'In ${diff.inDays} d';
}
static bool sameDay(DateTime a, DateTime b) =>
a.year == b.year && a.month == b.month && a.day == b.day;
}

137
lib/core/router.dart Normal file
View File

@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../screens/auth_screens.dart';
import '../screens/availability_screen.dart';
import '../screens/clock_screen.dart';
import '../screens/compliance_screens.dart';
import '../screens/home_shell.dart';
import '../screens/onboarding_screens.dart';
import '../screens/pay_screens.dart';
import '../screens/profile_detail_screens.dart';
import '../screens/profile_screen.dart';
import '../screens/settings_screens.dart';
import '../screens/shift_detail_screen.dart';
import '../screens/shifts_screen.dart';
import '../screens/timecard_screens.dart';
import '../screens/today_screen.dart';
import '../state/compliance_controller.dart';
import 'transitions.dart';
final _rootKey = GlobalKey<NavigatorState>();
ComplianceSection _sectionFromName(String name) =>
ComplianceSection.values.firstWhere(
(s) => s.name == name,
orElse: () => ComplianceSection.documents,
);
/// A screen that replaces what came before it.
GoRoute _fadeRoute(String path, Widget Function() build) => GoRoute(
path: path,
pageBuilder: (_, state) => Transitions.fade(state, build()),
);
/// A screen pushed on top of the shell.
GoRoute _detailRoute(
String path,
Widget Function(GoRouterState) build, {
List<RouteBase> routes = const [],
}) => GoRoute(
path: path,
parentNavigatorKey: _rootKey,
pageBuilder: (_, state) => Transitions.push(state, build(state)),
routes: routes,
);
/// Sign-in lives outside the shell; the five tabs each keep their own stack,
/// and detail screens push over the whole shell.
final router = GoRouter(
navigatorKey: _rootKey,
initialLocation: '/',
routes: [
_fadeRoute('/', SplashScreen.new),
_fadeRoute('/onboarding', OnboardingScreen.new),
_fadeRoute('/welcome', WelcomeScreen.new),
_fadeRoute('/verified', VerifiedScreen.new),
_fadeRoute('/permissions', PermissionsScreen.new),
// The two sign-in steps are pushed, so back gestures read correctly.
_detailRoute('/phone', (_) => const PhoneScreen()),
_detailRoute('/otp', (_) => const OtpScreen()),
StatefulShellRoute.indexedStack(
builder: (_, _, shell) => HomeShell(navigationShell: shell),
branches: [
StatefulShellBranch(
routes: [
GoRoute(path: '/today', builder: (_, _) => const TodayScreen()),
],
),
StatefulShellBranch(
routes: [
GoRoute(path: '/shifts', builder: (_, _) => const ShiftsScreen()),
],
),
StatefulShellBranch(
routes: [
GoRoute(path: '/clock', builder: (_, _) => const ClockScreen()),
],
),
StatefulShellBranch(
routes: [GoRoute(path: '/pay', builder: (_, _) => const PayScreen())],
),
StatefulShellBranch(
routes: [
GoRoute(path: '/profile', builder: (_, _) => const ProfileScreen()),
],
),
],
),
_detailRoute(
'/shift/:id',
(state) => ShiftDetailScreen(shiftId: state.pathParameters['id']!),
),
_detailRoute('/availability', (_) => const AvailabilityScreen()),
_detailRoute('/timecards', (_) => const TimecardsScreen()),
_detailRoute(
'/timecard/:id',
(state) => TimecardReviewScreen(timecardId: state.pathParameters['id']!),
),
_detailRoute('/payroll', (_) => const PayrollScheduleScreen()),
_detailRoute(
'/payment/:id',
(state) => PaymentDetailScreen(shiftId: state.pathParameters['id']!),
),
_detailRoute(
'/compliance',
(_) => const ComplianceHubScreen(),
routes: [
_detailRoute(
':section',
(state) => ComplianceListScreen(
section: _sectionFromName(state.pathParameters['section']!),
),
routes: [
_detailRoute(
':itemId',
(state) => ComplianceDetailScreen(
section: _sectionFromName(state.pathParameters['section']!),
itemId: state.pathParameters['itemId']!,
),
),
],
),
],
),
_detailRoute('/emergency-contacts', (_) => const EmergencyContactsScreen()),
_detailRoute('/skills', (_) => const SkillsScreen()),
_detailRoute('/direct-deposit', (_) => const DirectDepositScreen()),
_detailRoute('/benefits', (_) => const BenefitsScreen()),
_detailRoute('/settings', (_) => const SettingsScreen()),
_detailRoute('/support', (_) => const SupportScreen()),
_detailRoute('/faqs', (_) => const FaqScreen()),
_detailRoute('/privacy', (_) => const PrivacyScreen()),
],
);

144
lib/core/theme.dart Normal file
View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// KROW brand palette: 286CP blue carries every surface and action, 106CP
/// yellow marks money and anything waiting on you, 621CP sage is the canvas.
abstract final class KrowPalette {
static const blue = Color(0xFF3752A8);
static const blueDeep = Color(0xFF22366F);
static const blueTint = Color(0xFFE1E6F4);
static const yellow = Color(0xFFF5C842);
static const yellowSoft = Color(0xFFF7E4AE);
static const sage = Color(0xFFE9F3EC);
static const ink = Color(0xFF26353E);
static const line = Color(0xFFCFDDD3);
static const mute = Color(0xFF65727A);
}
ThemeData buildKrowTheme() {
final scheme = ColorScheme.fromSeed(
seedColor: KrowPalette.blue,
primary: KrowPalette.blue,
onPrimary: Colors.white,
secondary: KrowPalette.yellow,
onSecondary: KrowPalette.ink,
surface: Colors.white,
onSurface: KrowPalette.ink,
brightness: Brightness.light,
);
final text = GoogleFonts.poppinsTextTheme().apply(
bodyColor: KrowPalette.ink,
displayColor: KrowPalette.ink,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
textTheme: text,
scaffoldBackgroundColor: KrowPalette.sage,
appBarTheme: AppBarTheme(
backgroundColor: KrowPalette.sage,
surfaceTintColor: Colors.transparent,
foregroundColor: KrowPalette.ink,
elevation: 0,
centerTitle: false,
titleTextStyle: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: KrowPalette.ink,
),
),
cardTheme: CardThemeData(
color: Colors.white,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: KrowPalette.line),
),
),
dividerTheme: const DividerThemeData(
color: KrowPalette.line,
thickness: 1,
space: 1,
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
backgroundColor: KrowPalette.blue,
foregroundColor: Colors.white,
disabledBackgroundColor: const Color(0xFFD5E2D9),
disabledForegroundColor: const Color(0xFF949FA5),
textStyle: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w600,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(52),
foregroundColor: KrowPalette.ink,
backgroundColor: Colors.white,
side: const BorderSide(color: KrowPalette.line),
textStyle: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w600,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: KrowPalette.blue,
textStyle: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
hintStyle: GoogleFonts.poppins(
fontSize: 14,
color: const Color(0xFF949FA5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: KrowPalette.line),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: KrowPalette.line),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: KrowPalette.blue, width: 1.6),
),
),
chipTheme: ChipThemeData(
backgroundColor: Colors.white,
side: const BorderSide(color: KrowPalette.line),
labelStyle: GoogleFonts.poppins(
fontSize: 12.5,
fontWeight: FontWeight.w500,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
// Bottom navigation is KrowNavBar, not Material's NavigationBar, so there
// is no navigationBarTheme to configure — its styling lives in the widget.
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: KrowPalette.ink,
contentTextStyle: GoogleFonts.poppins(
fontSize: 13.5,
color: Colors.white,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}

84
lib/core/transitions.dart Normal file
View File

@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// Page transitions, kept in one place so every screen arrives the same way.
///
/// Two kinds only. Moving *deeper* (a shift, a document, a timecard) slides
/// forward, and the screen behind it slides back a little so the stack reads
/// as a stack. Moving *sideways* — between steps of sign-up, or replacing the
/// whole screen — cross-fades, because nothing is being pushed onto anything.
abstract final class Transitions {
static const _forward = Duration(milliseconds: 320);
static const _back = Duration(milliseconds: 260);
/// Pushed detail screens.
static CustomTransitionPage<T> push<T>(GoRouterState state, Widget child) {
return CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionDuration: _forward,
reverseTransitionDuration: _back,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final incoming = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
// The outgoing screen eases back rather than sitting still, which is
// what gives the push its depth.
final outgoing = CurvedAnimation(
parent: secondaryAnimation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
// secondaryAnimation rests at 0 while this page is on top, so the
// resting state has to be `begin`. Reversing these two leaves every
// pushed screen sitting 12% off to the left.
return SlideTransition(
position: Tween(
begin: Offset.zero,
end: const Offset(-0.12, 0),
).animate(outgoing),
child: FadeTransition(
opacity: Tween(begin: 1.0, end: 0.0).animate(outgoing),
child: SlideTransition(
position: Tween(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(incoming),
child: child,
),
),
);
},
);
}
/// Screens that replace what came before: splash, onboarding, sign-in steps.
static CustomTransitionPage<T> fade<T>(GoRouterState state, Widget child) {
return CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionDuration: _forward,
reverseTransitionDuration: _back,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
return FadeTransition(
opacity: curved,
// A few pixels of rise stops the cross-fade feeling inert.
child: SlideTransition(
position: Tween(
begin: const Offset(0, 0.015),
end: Offset.zero,
).animate(curved),
child: child,
),
);
},
);
}
}