Merge pull request #347 from Oloodi/312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation

Continuation of the development of the mobile apps
This commit is contained in:
Achintha Isuru
2026-02-01 22:18:43 -05:00
committed by GitHub
109 changed files with 25853 additions and 24849 deletions

View File

@@ -0,0 +1,9 @@
## Recommended tasks for the next sprint
* In the mobile applications, since the structure is now finalized (at least for the existing features), we need to **strictly follow best practices while coding**:
* 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

View File

@@ -5,42 +5,6 @@
"storage_bucket": "krow-workforce-dev.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:87d41566f8dda41d7757db",
"android_client_info": {
"package_name": "com.example.krow_workforce"
}
},
"oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db",
@@ -67,10 +31,10 @@
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
"bundle_id": "com.krowwithus.staff"
}
}
]
@@ -103,10 +67,10 @@
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
"bundle_id": "com.krowwithus.staff"
}
}
]
@@ -139,10 +103,10 @@
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
"bundle_id": "com.krowwithus.staff"
}
}
]
@@ -151,12 +115,20 @@
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:d26bde4ee337b0b17757db",
"mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db",
"android_client_info": {
"package_name": "com.krowwithus.krow_workforce.dev"
"package_name": "com.krowwithus.staff"
}
},
"oauth_client": [
{
"client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.krowwithus.staff",
"certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d"
}
},
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
@@ -175,10 +147,10 @@
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
"bundle_id": "com.krowwithus.staff"
}
}
]

View File

@@ -14,6 +14,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
E8C1A28BFABAEE32FB779C9A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 221E00B70DE845BE3D50D0A0 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -42,6 +43,7 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
221E00B70DE845BE3D50D0A0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
@@ -94,6 +96,7 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
221E00B70DE845BE3D50D0A0 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -216,6 +219,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
E8C1A28BFABAEE32FB779C9A /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>933560802882-jqpv1l3gjmi3m87b2gu1iq4lg46lkdfg.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.933560802882-jqpv1l3gjmi3m87b2gu1iq4lg46lkdfg</string>
<key>ANDROID_CLIENT_ID</key>
<string>933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA</string>
<key>GCM_SENDER_ID</key>
<string>933560802882</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.krowwithus.client</string>
<key>PROJECT_ID</key>
<string>krow-workforce-dev</string>
<key>STORAGE_BUCKET</key>
<string>krow-workforce-dev.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:933560802882:ios:d2b6d743608e2a527757db</string>
</dict>
</plist>

View File

@@ -0,0 +1,74 @@
// File generated by FlutterFire CLI.
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8',
appId: '1:933560802882:web:173a841992885bb27757db',
messagingSenderId: '933560802882',
projectId: 'krow-workforce-dev',
authDomain: 'krow-workforce-dev.firebaseapp.com',
storageBucket: 'krow-workforce-dev.firebasestorage.app',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4',
appId: '1:933560802882:android:da13569105659ead7757db',
messagingSenderId: '933560802882',
projectId: 'krow-workforce-dev',
storageBucket: 'krow-workforce-dev.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA',
appId: '1:933560802882:ios:d2b6d743608e2a527757db',
messagingSenderId: '933560802882',
projectId: 'krow-workforce-dev',
storageBucket: 'krow-workforce-dev.firebasestorage.app',
iosBundleId: 'com.krowwithus.client',
);
}

View File

@@ -12,10 +12,15 @@ import 'package:client_hubs/client_hubs.dart' as client_hubs;
import 'package:client_create_order/client_create_order.dart'
as client_create_order;
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:krow_core/core.dart';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await Firebase.initializeApp(
options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null,
);
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
}
@@ -54,34 +59,38 @@ class AppWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<core_localization.LocaleBloc>(
create: (BuildContext context) =>
Modular.get<core_localization.LocaleBloc>(),
child:
BlocBuilder<
core_localization.LocaleBloc,
core_localization.LocaleState
>(
builder:
(BuildContext context, core_localization.LocaleState state) {
return core_localization.TranslationProvider(
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
title: "Krow Client",
theme: UiTheme.light,
routerConfig: Modular.routerConfig,
locale: state.locale,
supportedLocales: state.supportedLocales,
localizationsDelegates:
const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
),
);
},
),
return WebMobileFrame(
appName: 'KROW Client\nApplication',
logo: Image.asset('assets/logo.png'),
child: BlocProvider<core_localization.LocaleBloc>(
create: (BuildContext context) =>
Modular.get<core_localization.LocaleBloc>(),
child:
BlocBuilder<
core_localization.LocaleBloc,
core_localization.LocaleState
>(
builder:
(BuildContext context, core_localization.LocaleState state) {
return core_localization.TranslationProvider(
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
title: "Krow Client",
theme: UiTheme.light,
routerConfig: Modular.routerConfig,
locale: state.locale,
supportedLocales: state.supportedLocales,
localizationsDelegates:
const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
),
);
},
),
),
);
}
}

View File

@@ -32,6 +32,8 @@ dependencies:
path: ../../packages/features/client/hubs
client_create_order:
path: ../../packages/features/client/create_order
krow_core:
path: ../../packages/core
cupertino_icons: ^1.0.8
flutter_modular: ^6.3.2

View File

@@ -33,6 +33,29 @@
<link rel="manifest" href="manifest.json">
</head>
<body>
<script type="module">
// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-analytics.js";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8",
authDomain: "krow-workforce-dev.firebaseapp.com",
projectId: "krow-workforce-dev",
storageBucket: "krow-workforce-dev.firebasestorage.app",
messagingSenderId: "933560802882",
appId: "1:933560802882:web:173a841992885bb27757db",
measurementId: "G-9S7WEQTDKX"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
</script>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@@ -5,42 +5,6 @@
"storage_bucket": "krow-workforce-dev.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:87d41566f8dda41d7757db",
"android_client_info": {
"package_name": "com.example.krow_workforce"
}
},
"oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db",
@@ -149,42 +113,6 @@
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:d26bde4ee337b0b17757db",
"android_client_info": {
"package_name": "com.krowwithus.krow_workforce.dev"
}
},
"oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db",

View File

@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1E967D034ADA3A16EF82CB3E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9F0B07DEC91B141354438F79 /* GoogleService-Info.plist */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -55,6 +56,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9F0B07DEC91B141354438F79 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -94,6 +96,7 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
9F0B07DEC91B141354438F79 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -216,6 +219,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
1E967D034ADA3A16EF82CB3E /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh</string>
<key>ANDROID_CLIENT_ID</key>
<string>933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA</string>
<key>GCM_SENDER_ID</key>
<string>933560802882</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.krowwithus.staff</string>
<key>PROJECT_ID</key>
<string>krow-workforce-dev</string>
<key>STORAGE_BUCKET</key>
<string>krow-workforce-dev.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:933560802882:ios:fa584205b356de937757db</string>
</dict>
</plist>

View File

@@ -0,0 +1,74 @@
// File generated by FlutterFire CLI.
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8',
appId: '1:933560802882:web:4508ef1ee6d4e6907757db',
messagingSenderId: '933560802882',
projectId: 'krow-workforce-dev',
authDomain: 'krow-workforce-dev.firebaseapp.com',
storageBucket: 'krow-workforce-dev.firebasestorage.app',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4',
appId: '1:933560802882:android:d49b8c0f4d19e95e7757db',
messagingSenderId: '933560802882',
projectId: 'krow-workforce-dev',
storageBucket: 'krow-workforce-dev.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDyEXkzZAWpXXe4dAesYaZflt5BEtMn9tA',
appId: '1:933560802882:ios:fa584205b356de937757db',
messagingSenderId: '933560802882',
projectId: 'krow-workforce-dev',
storageBucket: 'krow-workforce-dev.firebasestorage.app',
iosBundleId: 'com.krowwithus.staff',
);
}

View File

@@ -5,13 +5,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krowwithus_staff/firebase_options.dart';
import 'package:staff_authentication/staff_authentication.dart'
as staff_authentication;
import 'package:staff_main/staff_main.dart' as staff_main;
import 'package:krow_core/core.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
}
@@ -34,33 +38,37 @@ class AppWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<core_localization.LocaleBloc>(
create: (BuildContext context) =>
Modular.get<core_localization.LocaleBloc>(),
child:
BlocBuilder<
core_localization.LocaleBloc,
core_localization.LocaleState
>(
builder:
(BuildContext context, core_localization.LocaleState state) {
return core_localization.TranslationProvider(
child: MaterialApp.router(
title: "KROW Staff",
theme: UiTheme.light,
routerConfig: Modular.routerConfig,
locale: state.locale,
supportedLocales: state.supportedLocales,
localizationsDelegates:
const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
),
);
},
),
return WebMobileFrame(
appName: 'KROW Staff\nApplication',
logo: Image.asset('assets/logo.png'),
child: BlocProvider<core_localization.LocaleBloc>(
create: (BuildContext context) =>
Modular.get<core_localization.LocaleBloc>(),
child:
BlocBuilder<
core_localization.LocaleBloc,
core_localization.LocaleState
>(
builder:
(BuildContext context, core_localization.LocaleState state) {
return core_localization.TranslationProvider(
child: MaterialApp.router(
title: "KROW Staff",
theme: UiTheme.light,
routerConfig: Modular.routerConfig,
locale: state.locale,
supportedLocales: state.supportedLocales,
localizationsDelegates:
const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
),
);
},
),
),
);
}
}

View File

@@ -1,21 +1,143 @@
PODS:
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- Firebase/AppCheck (12.8.0):
- Firebase/CoreOnly
- FirebaseAppCheck (~> 12.8.0)
- Firebase/Auth (12.8.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 12.8.0)
- Firebase/CoreOnly (12.8.0):
- FirebaseCore (~> 12.8.0)
- firebase_app_check (0.4.1-4):
- Firebase/AppCheck (~> 12.8.0)
- Firebase/CoreOnly (~> 12.8.0)
- firebase_core
- FlutterMacOS
- firebase_auth (6.1.4):
- Firebase/Auth (~> 12.8.0)
- Firebase/CoreOnly (~> 12.8.0)
- firebase_core
- FlutterMacOS
- firebase_core (4.4.0):
- Firebase/CoreOnly (~> 12.8.0)
- FlutterMacOS
- FirebaseAppCheck (12.8.0):
- AppCheckCore (~> 11.0)
- FirebaseAppCheckInterop (~> 12.8.0)
- FirebaseCore (~> 12.8.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- FirebaseAppCheckInterop (12.8.0)
- FirebaseAuth (12.8.0):
- FirebaseAppCheckInterop (~> 12.8.0)
- FirebaseAuthInterop (~> 12.8.0)
- FirebaseCore (~> 12.8.0)
- FirebaseCoreExtension (~> 12.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 6.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (12.8.0)
- FirebaseCore (12.8.0):
- FirebaseCoreInternal (~> 12.8.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseCoreInternal (12.8.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FlutterMacOS (1.0.0)
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (5.0.0)
- PromisesObjC (2.4.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`)
- firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`)
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
SPEC REPOS:
trunk:
- AppCheckCore
- Firebase
- FirebaseAppCheck
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- GoogleUtilities
- GTMSessionFetcher
- PromisesObjC
EXTERNAL SOURCES:
firebase_app_check:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos
firebase_auth:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos
firebase_core:
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
FlutterMacOS:
:path: Flutter/ephemeral
geolocator_apple:
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
SPEC CHECKSUMS:
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
firebase_app_check: daf97f2d7044e28b68d23bc90e16751acee09732
firebase_auth: 2c2438e41f061c03bd67dcb045dfd7bc843b5f52
firebase_core: b1697fb64ff2b9ca16baaa821205f8b0c058e5d2
FirebaseAppCheck: 11da425929a45c677d537adfff3520ccd57c1690
FirebaseAppCheckInterop: ba3dc604a89815379e61ec2365101608d365cf7d
FirebaseAuth: 4c289b1a43f5955283244a55cf6bd616de344be5
FirebaseAuthInterop: 95363fe96493cb4f106656666a0768b420cba090
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMSessionFetcher: 02d6e866e90bc236f48a703a041dfe43e6221a29
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009

View File

@@ -11,10 +11,7 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.8
flutter_modular: ^6.3.0
# Architecture Packages
# Architecture Packages
design_system:
path: ../../packages/design_system
core_localization:
@@ -27,6 +24,14 @@ dependencies:
path: ../../packages/features/staff/availability
staff_clock_in:
path: ../../packages/features/staff/clock_in
staff_main:
path: ../../packages/features/staff/staff_main
krow_core:
path: ../../packages/core
cupertino_icons: ^1.0.8
flutter_modular: ^6.3.0
firebase_core: ^4.4.0
flutter_bloc: ^8.1.6
dev_dependencies:
flutter_test:

View File

@@ -34,5 +34,28 @@
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
<script type="module">
// Import the functions you need from the SDKs you need
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-analytics.js";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "AIzaSyBqRtZPMGU-Sz5x5UnRrunKu5NSWYyPRn8",
authDomain: "krow-workforce-dev.firebaseapp.com",
projectId: "krow-workforce-dev",
storageBucket: "krow-workforce-dev.firebasestorage.app",
messagingSenderId: "933560802882",
appId: "1:933560802882:web:4508ef1ee6d4e6907757db",
measurementId: "G-DTDL7YRRM6"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
</script>
</body>
</html>

View File

@@ -3,3 +3,4 @@ library core;
export 'src/domain/arguments/usecase_argument.dart';
export 'src/domain/usecases/usecase.dart';
export 'src/utils/date_time_utils.dart';
export 'src/presentation/widgets/web_mobile_frame.dart';

View File

@@ -0,0 +1,263 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// A wrapper widget that renders the application inside an iPhone-like frame
/// specifically for Flutter Web. On other platforms, it simply returns the child.
class WebMobileFrame extends StatelessWidget {
const WebMobileFrame({
super.key,
required this.child,
required this.logo,
required this.appName,
});
final Widget child;
final Widget logo;
final String appName;
@override
Widget build(BuildContext context) {
if (!kIsWeb) return child;
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: _WebFrameContent(logo: logo, appName: appName, child: child),
);
}
}
class _WebFrameContent extends StatefulWidget {
const _WebFrameContent({
required this.child,
required this.logo,
required this.appName,
});
final Widget child;
final Widget logo;
final String appName;
@override
State<_WebFrameContent> createState() => _WebFrameContentState();
}
class _WebFrameContentState extends State<_WebFrameContent> {
// ignore: unused_field
Offset _cursorPosition = Offset.zero;
// ignore: unused_field
bool _isHovering = false;
@override
Widget build(BuildContext context) {
// iPhone 14 Pro Max-ish dimensions (scaled for frame look)
const double frameWidth = 390 * 1.2;
const double frameHeight = 844 * 1.3;
const double borderRadius = 54.0;
const double borderThickness = 12.0;
return Scaffold(
backgroundColor: UiColors.foreground,
body: MouseRegion(
cursor: SystemMouseCursors.none,
onHover: (PointerHoverEvent event) {
setState(() {
_cursorPosition = event.position;
_isHovering = true;
});
},
onExit: (_) => setState(() => _isHovering = false),
child: Stack(
children: <Widget>[
// Logo and Title on the left (Web only)
Positioned(
left: 60,
top: 0,
bottom: 0,
child: Center(
child: Opacity(
opacity: 0.5,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(width: 140, child: widget.logo),
const SizedBox(height: 12),
Text(
widget.appName,
textAlign: TextAlign.left,
style: UiTypography.display1b.copyWith(
color: UiColors.white,
),
),
const SizedBox(height: 4),
Container(
height: 2,
width: 40,
color: UiColors.white.withOpacity(0.3),
),
],
),
),
),
),
// Frame and Content
Center(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Scale down if screen is too small
final double scaleX = constraints.maxWidth / (frameWidth - 150);
final double scaleY = constraints.maxHeight / (frameHeight - 220);
final double scale = (scaleX < 1 || scaleY < 1)
? (scaleX < scaleY ? scaleX : scaleY)
: 1.0;
return Transform.scale(
scale: scale,
child: Container(
width: frameWidth,
height: frameHeight,
decoration: BoxDecoration(
color: UiColors.black,
borderRadius: BorderRadius.circular(borderRadius),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withOpacity(0.6),
blurRadius: 40,
spreadRadius: 10,
),
],
border: Border.all(
color: const Color(0xFF2C2C2C),
width: borderThickness,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
borderRadius - borderThickness,
),
child: Stack(
children: <Widget>[
// The actual app + status bar
Column(
children: <Widget>[
// Mock iOS Status Bar
Container(
height: 48,
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
decoration: const BoxDecoration(
color: UiColors.background,
border: Border(
bottom: BorderSide(
color: UiColors.border,
width: 0.5,
),
),
),
child: const Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
// Time side
SizedBox(
width: 80,
child: Text(
'9:41 PM',
textAlign: TextAlign.center,
style: TextStyle(
color: UiColors.black,
fontWeight: FontWeight.w700,
fontSize: 14,
letterSpacing: -0.2,
),
),
),
// Status Icons side
SizedBox(
width: 80,
child: Row(
mainAxisAlignment:
MainAxisAlignment.end,
spacing: 12,
children: <Widget>[
Icon(
Icons.signal_cellular_alt,
size: 14,
color: UiColors.black,
),
Icon(
Icons.wifi,
size: 14,
color: UiColors.black,
),
Icon(
Icons.battery_full,
size: 14,
color: UiColors.black,
),
],
),
),
],
),
),
// The main app content
Expanded(child: widget.child),
],
),
// Dynamic Island / Notch Mockup
Align(
alignment: Alignment.topCenter,
child: Container(
width: 120,
height: 35,
margin: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: UiColors.black,
borderRadius: BorderRadius.circular(20),
),
),
),
],
),
),
),
);
},
),
),
if (_isHovering)
Positioned(
left: _cursorPosition.dx - 15,
top: _cursorPosition.dy - 15,
child: IgnorePointer(
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: UiColors.mutedForeground.withOpacity(0.3),
shape: BoxShape.circle,
border: Border.all(color: UiColors.white.withOpacity(0.7), width: 2),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withOpacity(0.2),
blurRadius: 4,
spreadRadius: 1,
),
],
),
),
),
),
],
),
),
);
}
}

View File

@@ -11,3 +11,5 @@ environment:
dependencies:
flutter:
sdk: flutter
design_system:
path: ../design_system

View File

@@ -1,183 +0,0 @@
/// Generated file. Do not edit.
///
/// Source: lib/src/l10n
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 1108 (554 per locale)
///
/// Built on 2026-01-31 at 17:37 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
// dart format off
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:slang/generated.dart';
import 'package:slang_flutter/slang_flutter.dart';
export 'package:slang_flutter/slang_flutter.dart';
import 'strings_es.g.dart' deferred as l_es;
part 'strings_en.g.dart';
/// Supported locales.
///
/// Usage:
/// - LocaleSettings.setLocale(AppLocale.en) // set locale
/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum
/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
enum AppLocale with BaseAppLocale<AppLocale, Translations> {
en(languageCode: 'en'),
es(languageCode: 'es');
const AppLocale({
required this.languageCode,
this.scriptCode, // ignore: unused_element, unused_element_parameter
this.countryCode, // ignore: unused_element, unused_element_parameter
});
@override final String languageCode;
@override final String? scriptCode;
@override final String? countryCode;
@override
Future<Translations> build({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) async {
switch (this) {
case AppLocale.en:
return TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.es:
await l_es.loadLibrary();
return l_es.TranslationsEs(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
@override
Translations buildSync({
Map<String, Node>? overrides,
PluralResolver? cardinalResolver,
PluralResolver? ordinalResolver,
}) {
switch (this) {
case AppLocale.en:
return TranslationsEn(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
case AppLocale.es:
return l_es.TranslationsEs(
overrides: overrides,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
}
/// Gets current instance managed by [LocaleSettings].
Translations get translations => LocaleSettings.instance.getTranslations(this);
}
/// Method A: Simple
///
/// No rebuild after locale change.
/// Translation happens during initialization of the widget (call of t).
/// Configurable via 'translate_var'.
///
/// Usage:
/// String a = t.someKey.anotherKey;
/// String b = t['someKey.anotherKey']; // Only for edge cases!
Translations get t => LocaleSettings.instance.currentTranslations;
/// Method B: Advanced
///
/// All widgets using this method will trigger a rebuild when locale changes.
/// Use this if you have e.g. a settings page where the user can select the locale during runtime.
///
/// Step 1:
/// wrap your App with
/// TranslationProvider(
/// child: MyApp()
/// );
///
/// Step 2:
/// final t = Translations.of(context); // Get t variable.
/// String a = t.someKey.anotherKey; // Use t variable.
/// String b = t['someKey.anotherKey']; // Only for edge cases!
class TranslationProvider extends BaseTranslationProvider<AppLocale, Translations> {
TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
static InheritedLocaleData<AppLocale, Translations> of(BuildContext context) => InheritedLocaleData.of<AppLocale, Translations>(context);
}
/// Method B shorthand via [BuildContext] extension method.
/// Configurable via 'translate_var'.
///
/// Usage (e.g. in a widget's build method):
/// context.t.someKey.anotherKey
extension BuildContextTranslationsExtension on BuildContext {
Translations get t => TranslationProvider.of(this).translations;
}
/// Manages all translation instances and the current locale
class LocaleSettings extends BaseFlutterLocaleSettings<AppLocale, Translations> {
LocaleSettings._() : super(
utils: AppLocaleUtils.instance,
lazy: true,
);
static final instance = LocaleSettings._();
// static aliases (checkout base methods for documentation)
static AppLocale get currentLocale => instance.currentLocale;
static Stream<AppLocale> getLocaleStream() => instance.getLocaleStream();
static Future<AppLocale> setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static Future<AppLocale> useDeviceLocale() => instance.useDeviceLocale();
static Future<void> setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
// synchronous versions
static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
static AppLocale useDeviceLocaleSync() => instance.useDeviceLocaleSync();
static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync(
language: language,
locale: locale,
cardinalResolver: cardinalResolver,
ordinalResolver: ordinalResolver,
);
}
/// Provides utility functions without any side effects.
class AppLocaleUtils extends BaseAppLocaleUtils<AppLocale, Translations> {
AppLocaleUtils._() : super(
baseLocale: AppLocale.en,
locales: AppLocale.values,
);
static final instance = AppLocaleUtils._();
// static aliases (checkout base methods for documentation)
static AppLocale parse(String rawLocale) => instance.parse(rawLocale);
static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode);
static AppLocale findDeviceLocale() => instance.findDeviceLocale();
static List<Locale> get supportedLocales => instance.supportedLocales;
static List<String> get supportedLocalesRaw => instance.supportedLocalesRaw;
}

View File

@@ -1,16 +1,16 @@
# Basic Usage
```dart
ExampleConnector.instance.createRecentPayment(createRecentPaymentVariables).execute();
ExampleConnector.instance.updateRecentPayment(updateRecentPaymentVariables).execute();
ExampleConnector.instance.deleteRecentPayment(deleteRecentPaymentVariables).execute();
ExampleConnector.instance.CreateStaff(createStaffVariables).execute();
ExampleConnector.instance.UpdateStaff(updateStaffVariables).execute();
ExampleConnector.instance.DeleteStaff(deleteStaffVariables).execute();
ExampleConnector.instance.getStaffDocumentByKey(getStaffDocumentByKeyVariables).execute();
ExampleConnector.instance.listStaffDocumentsByStaffId(listStaffDocumentsByStaffIdVariables).execute();
ExampleConnector.instance.listStaffDocumentsByDocumentType(listStaffDocumentsByDocumentTypeVariables).execute();
ExampleConnector.instance.listStaffDocumentsByStatus(listStaffDocumentsByStatusVariables).execute();
ExampleConnector.instance.listStaffAvailabilities(listStaffAvailabilitiesVariables).execute();
ExampleConnector.instance.listStaffAvailabilitiesByStaffId(listStaffAvailabilitiesByStaffIdVariables).execute();
ExampleConnector.instance.getStaffAvailabilityByKey(getStaffAvailabilityByKeyVariables).execute();
ExampleConnector.instance.listStaffAvailabilitiesByDay(listStaffAvailabilitiesByDayVariables).execute();
ExampleConnector.instance.createStaffAvailabilityStats(createStaffAvailabilityStatsVariables).execute();
ExampleConnector.instance.updateStaffAvailabilityStats(updateStaffAvailabilityStatsVariables).execute();
ExampleConnector.instance.deleteStaffAvailabilityStats(deleteStaffAvailabilityStatsVariables).execute();
```
@@ -23,8 +23,8 @@ Optional fields can be discovered based on classes that have `Optional` object t
This is an example of a mutation with an optional field:
```dart
await ExampleConnector.instance.updateAttireOption({ ... })
.itemId(...)
await ExampleConnector.instance.searchInvoiceTemplatesByOwnerAndName({ ... })
.offset(...)
.execute();
```

View File

@@ -0,0 +1,645 @@
part of 'generated.dart';
class GetApplicationByStaffShiftAndRoleVariablesBuilder {
String staffId;
String shiftId;
String roleId;
Optional<int> _offset = Optional.optional(nativeFromJson, nativeToJson);
Optional<int> _limit = Optional.optional(nativeFromJson, nativeToJson);
final FirebaseDataConnect _dataConnect; GetApplicationByStaffShiftAndRoleVariablesBuilder offset(int? t) {
_offset.value = t;
return this;
}
GetApplicationByStaffShiftAndRoleVariablesBuilder limit(int? t) {
_limit.value = t;
return this;
}
GetApplicationByStaffShiftAndRoleVariablesBuilder(this._dataConnect, {required this.staffId,required this.shiftId,required this.roleId,});
Deserializer<GetApplicationByStaffShiftAndRoleData> dataDeserializer = (dynamic json) => GetApplicationByStaffShiftAndRoleData.fromJson(jsonDecode(json));
Serializer<GetApplicationByStaffShiftAndRoleVariables> varsSerializer = (GetApplicationByStaffShiftAndRoleVariables vars) => jsonEncode(vars.toJson());
Future<QueryResult<GetApplicationByStaffShiftAndRoleData, GetApplicationByStaffShiftAndRoleVariables>> execute() {
return ref().execute();
}
QueryRef<GetApplicationByStaffShiftAndRoleData, GetApplicationByStaffShiftAndRoleVariables> ref() {
GetApplicationByStaffShiftAndRoleVariables vars= GetApplicationByStaffShiftAndRoleVariables(staffId: staffId,shiftId: shiftId,roleId: roleId,offset: _offset,limit: _limit,);
return _dataConnect.query("getApplicationByStaffShiftAndRole", dataDeserializer, varsSerializer, vars);
}
}
@immutable
class GetApplicationByStaffShiftAndRoleApplications {
final String id;
final String shiftId;
final String staffId;
final EnumValue<ApplicationStatus> status;
final Timestamp? appliedAt;
final Timestamp? checkInTime;
final Timestamp? checkOutTime;
final EnumValue<ApplicationOrigin> origin;
final Timestamp? createdAt;
final GetApplicationByStaffShiftAndRoleApplicationsShift shift;
final GetApplicationByStaffShiftAndRoleApplicationsShiftRole shiftRole;
GetApplicationByStaffShiftAndRoleApplications.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
shiftId = nativeFromJson<String>(json['shiftId']),
staffId = nativeFromJson<String>(json['staffId']),
status = applicationStatusDeserializer(json['status']),
appliedAt = json['appliedAt'] == null ? null : Timestamp.fromJson(json['appliedAt']),
checkInTime = json['checkInTime'] == null ? null : Timestamp.fromJson(json['checkInTime']),
checkOutTime = json['checkOutTime'] == null ? null : Timestamp.fromJson(json['checkOutTime']),
origin = applicationOriginDeserializer(json['origin']),
createdAt = json['createdAt'] == null ? null : Timestamp.fromJson(json['createdAt']),
shift = GetApplicationByStaffShiftAndRoleApplicationsShift.fromJson(json['shift']),
shiftRole = GetApplicationByStaffShiftAndRoleApplicationsShiftRole.fromJson(json['shiftRole']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplications otherTyped = other as GetApplicationByStaffShiftAndRoleApplications;
return id == otherTyped.id &&
shiftId == otherTyped.shiftId &&
staffId == otherTyped.staffId &&
status == otherTyped.status &&
appliedAt == otherTyped.appliedAt &&
checkInTime == otherTyped.checkInTime &&
checkOutTime == otherTyped.checkOutTime &&
origin == otherTyped.origin &&
createdAt == otherTyped.createdAt &&
shift == otherTyped.shift &&
shiftRole == otherTyped.shiftRole;
}
@override
int get hashCode => Object.hashAll([id.hashCode, shiftId.hashCode, staffId.hashCode, status.hashCode, appliedAt.hashCode, checkInTime.hashCode, checkOutTime.hashCode, origin.hashCode, createdAt.hashCode, shift.hashCode, shiftRole.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['shiftId'] = nativeToJson<String>(shiftId);
json['staffId'] = nativeToJson<String>(staffId);
json['status'] =
applicationStatusSerializer(status)
;
if (appliedAt != null) {
json['appliedAt'] = appliedAt!.toJson();
}
if (checkInTime != null) {
json['checkInTime'] = checkInTime!.toJson();
}
if (checkOutTime != null) {
json['checkOutTime'] = checkOutTime!.toJson();
}
json['origin'] =
applicationOriginSerializer(origin)
;
if (createdAt != null) {
json['createdAt'] = createdAt!.toJson();
}
json['shift'] = shift.toJson();
json['shiftRole'] = shiftRole.toJson();
return json;
}
GetApplicationByStaffShiftAndRoleApplications({
required this.id,
required this.shiftId,
required this.staffId,
required this.status,
this.appliedAt,
this.checkInTime,
this.checkOutTime,
required this.origin,
this.createdAt,
required this.shift,
required this.shiftRole,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleApplicationsShift {
final String id;
final String title;
final Timestamp? date;
final Timestamp? startTime;
final Timestamp? endTime;
final String? location;
final EnumValue<ShiftStatus>? status;
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrder order;
GetApplicationByStaffShiftAndRoleApplicationsShift.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
title = nativeFromJson<String>(json['title']),
date = json['date'] == null ? null : Timestamp.fromJson(json['date']),
startTime = json['startTime'] == null ? null : Timestamp.fromJson(json['startTime']),
endTime = json['endTime'] == null ? null : Timestamp.fromJson(json['endTime']),
location = json['location'] == null ? null : nativeFromJson<String>(json['location']),
status = json['status'] == null ? null : shiftStatusDeserializer(json['status']),
order = GetApplicationByStaffShiftAndRoleApplicationsShiftOrder.fromJson(json['order']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplicationsShift otherTyped = other as GetApplicationByStaffShiftAndRoleApplicationsShift;
return id == otherTyped.id &&
title == otherTyped.title &&
date == otherTyped.date &&
startTime == otherTyped.startTime &&
endTime == otherTyped.endTime &&
location == otherTyped.location &&
status == otherTyped.status &&
order == otherTyped.order;
}
@override
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, startTime.hashCode, endTime.hashCode, location.hashCode, status.hashCode, order.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['title'] = nativeToJson<String>(title);
if (date != null) {
json['date'] = date!.toJson();
}
if (startTime != null) {
json['startTime'] = startTime!.toJson();
}
if (endTime != null) {
json['endTime'] = endTime!.toJson();
}
if (location != null) {
json['location'] = nativeToJson<String?>(location);
}
if (status != null) {
json['status'] =
shiftStatusSerializer(status!)
;
}
json['order'] = order.toJson();
return json;
}
GetApplicationByStaffShiftAndRoleApplicationsShift({
required this.id,
required this.title,
this.date,
this.startTime,
this.endTime,
this.location,
this.status,
required this.order,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleApplicationsShiftOrder {
final String id;
final String? eventName;
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrderTeamHub teamHub;
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrderBusiness business;
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrderVendor? vendor;
GetApplicationByStaffShiftAndRoleApplicationsShiftOrder.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
eventName = json['eventName'] == null ? null : nativeFromJson<String>(json['eventName']),
teamHub = GetApplicationByStaffShiftAndRoleApplicationsShiftOrderTeamHub.fromJson(json['teamHub']),
business = GetApplicationByStaffShiftAndRoleApplicationsShiftOrderBusiness.fromJson(json['business']),
vendor = json['vendor'] == null ? null : GetApplicationByStaffShiftAndRoleApplicationsShiftOrderVendor.fromJson(json['vendor']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrder otherTyped = other as GetApplicationByStaffShiftAndRoleApplicationsShiftOrder;
return id == otherTyped.id &&
eventName == otherTyped.eventName &&
teamHub == otherTyped.teamHub &&
business == otherTyped.business &&
vendor == otherTyped.vendor;
}
@override
int get hashCode => Object.hashAll([id.hashCode, eventName.hashCode, teamHub.hashCode, business.hashCode, vendor.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
if (eventName != null) {
json['eventName'] = nativeToJson<String?>(eventName);
}
json['teamHub'] = teamHub.toJson();
json['business'] = business.toJson();
if (vendor != null) {
json['vendor'] = vendor!.toJson();
}
return json;
}
GetApplicationByStaffShiftAndRoleApplicationsShiftOrder({
required this.id,
this.eventName,
required this.teamHub,
required this.business,
this.vendor,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleApplicationsShiftOrderTeamHub {
final String address;
final String? placeId;
final String hubName;
GetApplicationByStaffShiftAndRoleApplicationsShiftOrderTeamHub.fromJson(dynamic json):
address = nativeFromJson<String>(json['address']),
placeId = json['placeId'] == null ? null : nativeFromJson<String>(json['placeId']),
hubName = nativeFromJson<String>(json['hubName']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrderTeamHub otherTyped = other as GetApplicationByStaffShiftAndRoleApplicationsShiftOrderTeamHub;
return address == otherTyped.address &&
placeId == otherTyped.placeId &&
hubName == otherTyped.hubName;
}
@override
int get hashCode => Object.hashAll([address.hashCode, placeId.hashCode, hubName.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['address'] = nativeToJson<String>(address);
if (placeId != null) {
json['placeId'] = nativeToJson<String?>(placeId);
}
json['hubName'] = nativeToJson<String>(hubName);
return json;
}
GetApplicationByStaffShiftAndRoleApplicationsShiftOrderTeamHub({
required this.address,
this.placeId,
required this.hubName,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleApplicationsShiftOrderBusiness {
final String id;
final String businessName;
final String? email;
final String? contactName;
final String? companyLogoUrl;
GetApplicationByStaffShiftAndRoleApplicationsShiftOrderBusiness.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
businessName = nativeFromJson<String>(json['businessName']),
email = json['email'] == null ? null : nativeFromJson<String>(json['email']),
contactName = json['contactName'] == null ? null : nativeFromJson<String>(json['contactName']),
companyLogoUrl = json['companyLogoUrl'] == null ? null : nativeFromJson<String>(json['companyLogoUrl']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrderBusiness otherTyped = other as GetApplicationByStaffShiftAndRoleApplicationsShiftOrderBusiness;
return id == otherTyped.id &&
businessName == otherTyped.businessName &&
email == otherTyped.email &&
contactName == otherTyped.contactName &&
companyLogoUrl == otherTyped.companyLogoUrl;
}
@override
int get hashCode => Object.hashAll([id.hashCode, businessName.hashCode, email.hashCode, contactName.hashCode, companyLogoUrl.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['businessName'] = nativeToJson<String>(businessName);
if (email != null) {
json['email'] = nativeToJson<String?>(email);
}
if (contactName != null) {
json['contactName'] = nativeToJson<String?>(contactName);
}
if (companyLogoUrl != null) {
json['companyLogoUrl'] = nativeToJson<String?>(companyLogoUrl);
}
return json;
}
GetApplicationByStaffShiftAndRoleApplicationsShiftOrderBusiness({
required this.id,
required this.businessName,
this.email,
this.contactName,
this.companyLogoUrl,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleApplicationsShiftOrderVendor {
final String id;
final String companyName;
GetApplicationByStaffShiftAndRoleApplicationsShiftOrderVendor.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
companyName = nativeFromJson<String>(json['companyName']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplicationsShiftOrderVendor otherTyped = other as GetApplicationByStaffShiftAndRoleApplicationsShiftOrderVendor;
return id == otherTyped.id &&
companyName == otherTyped.companyName;
}
@override
int get hashCode => Object.hashAll([id.hashCode, companyName.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['companyName'] = nativeToJson<String>(companyName);
return json;
}
GetApplicationByStaffShiftAndRoleApplicationsShiftOrderVendor({
required this.id,
required this.companyName,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleApplicationsShiftRole {
final String id;
final String roleId;
final int count;
final int? assigned;
final Timestamp? startTime;
final Timestamp? endTime;
final double? hours;
final double? totalValue;
final GetApplicationByStaffShiftAndRoleApplicationsShiftRoleRole role;
GetApplicationByStaffShiftAndRoleApplicationsShiftRole.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
roleId = nativeFromJson<String>(json['roleId']),
count = nativeFromJson<int>(json['count']),
assigned = json['assigned'] == null ? null : nativeFromJson<int>(json['assigned']),
startTime = json['startTime'] == null ? null : Timestamp.fromJson(json['startTime']),
endTime = json['endTime'] == null ? null : Timestamp.fromJson(json['endTime']),
hours = json['hours'] == null ? null : nativeFromJson<double>(json['hours']),
totalValue = json['totalValue'] == null ? null : nativeFromJson<double>(json['totalValue']),
role = GetApplicationByStaffShiftAndRoleApplicationsShiftRoleRole.fromJson(json['role']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplicationsShiftRole otherTyped = other as GetApplicationByStaffShiftAndRoleApplicationsShiftRole;
return id == otherTyped.id &&
roleId == otherTyped.roleId &&
count == otherTyped.count &&
assigned == otherTyped.assigned &&
startTime == otherTyped.startTime &&
endTime == otherTyped.endTime &&
hours == otherTyped.hours &&
totalValue == otherTyped.totalValue &&
role == otherTyped.role;
}
@override
int get hashCode => Object.hashAll([id.hashCode, roleId.hashCode, count.hashCode, assigned.hashCode, startTime.hashCode, endTime.hashCode, hours.hashCode, totalValue.hashCode, role.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['roleId'] = nativeToJson<String>(roleId);
json['count'] = nativeToJson<int>(count);
if (assigned != null) {
json['assigned'] = nativeToJson<int?>(assigned);
}
if (startTime != null) {
json['startTime'] = startTime!.toJson();
}
if (endTime != null) {
json['endTime'] = endTime!.toJson();
}
if (hours != null) {
json['hours'] = nativeToJson<double?>(hours);
}
if (totalValue != null) {
json['totalValue'] = nativeToJson<double?>(totalValue);
}
json['role'] = role.toJson();
return json;
}
GetApplicationByStaffShiftAndRoleApplicationsShiftRole({
required this.id,
required this.roleId,
required this.count,
this.assigned,
this.startTime,
this.endTime,
this.hours,
this.totalValue,
required this.role,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleApplicationsShiftRoleRole {
final String id;
final String name;
final double costPerHour;
GetApplicationByStaffShiftAndRoleApplicationsShiftRoleRole.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
name = nativeFromJson<String>(json['name']),
costPerHour = nativeFromJson<double>(json['costPerHour']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleApplicationsShiftRoleRole otherTyped = other as GetApplicationByStaffShiftAndRoleApplicationsShiftRoleRole;
return id == otherTyped.id &&
name == otherTyped.name &&
costPerHour == otherTyped.costPerHour;
}
@override
int get hashCode => Object.hashAll([id.hashCode, name.hashCode, costPerHour.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['name'] = nativeToJson<String>(name);
json['costPerHour'] = nativeToJson<double>(costPerHour);
return json;
}
GetApplicationByStaffShiftAndRoleApplicationsShiftRoleRole({
required this.id,
required this.name,
required this.costPerHour,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleData {
final List<GetApplicationByStaffShiftAndRoleApplications> applications;
GetApplicationByStaffShiftAndRoleData.fromJson(dynamic json):
applications = (json['applications'] as List<dynamic>)
.map((e) => GetApplicationByStaffShiftAndRoleApplications.fromJson(e))
.toList();
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleData otherTyped = other as GetApplicationByStaffShiftAndRoleData;
return applications == otherTyped.applications;
}
@override
int get hashCode => applications.hashCode;
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['applications'] = applications.map((e) => e.toJson()).toList();
return json;
}
GetApplicationByStaffShiftAndRoleData({
required this.applications,
});
}
@immutable
class GetApplicationByStaffShiftAndRoleVariables {
final String staffId;
final String shiftId;
final String roleId;
late final Optional<int>offset;
late final Optional<int>limit;
@Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.')
GetApplicationByStaffShiftAndRoleVariables.fromJson(Map<String, dynamic> json):
staffId = nativeFromJson<String>(json['staffId']),
shiftId = nativeFromJson<String>(json['shiftId']),
roleId = nativeFromJson<String>(json['roleId']) {
offset = Optional.optional(nativeFromJson, nativeToJson);
offset.value = json['offset'] == null ? null : nativeFromJson<int>(json['offset']);
limit = Optional.optional(nativeFromJson, nativeToJson);
limit.value = json['limit'] == null ? null : nativeFromJson<int>(json['limit']);
}
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetApplicationByStaffShiftAndRoleVariables otherTyped = other as GetApplicationByStaffShiftAndRoleVariables;
return staffId == otherTyped.staffId &&
shiftId == otherTyped.shiftId &&
roleId == otherTyped.roleId &&
offset == otherTyped.offset &&
limit == otherTyped.limit;
}
@override
int get hashCode => Object.hashAll([staffId.hashCode, shiftId.hashCode, roleId.hashCode, offset.hashCode, limit.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['staffId'] = nativeToJson<String>(staffId);
json['shiftId'] = nativeToJson<String>(shiftId);
json['roleId'] = nativeToJson<String>(roleId);
if(offset.state == OptionalState.set) {
json['offset'] = offset.toJson();
}
if(limit.state == OptionalState.set) {
json['limit'] = limit.toJson();
}
return json;
}
GetApplicationByStaffShiftAndRoleVariables({
required this.staffId,
required this.shiftId,
required this.roleId,
required this.offset,
required this.limit,
});
}

View File

@@ -4,6 +4,8 @@ class GetApplicationsByStaffIdVariablesBuilder {
String staffId;
Optional<int> _offset = Optional.optional(nativeFromJson, nativeToJson);
Optional<int> _limit = Optional.optional(nativeFromJson, nativeToJson);
Optional<Timestamp> _dayStart = Optional.optional((json) => json['dayStart'] = Timestamp.fromJson(json['dayStart']), defaultSerializer);
Optional<Timestamp> _dayEnd = Optional.optional((json) => json['dayEnd'] = Timestamp.fromJson(json['dayEnd']), defaultSerializer);
final FirebaseDataConnect _dataConnect; GetApplicationsByStaffIdVariablesBuilder offset(int? t) {
_offset.value = t;
@@ -13,6 +15,14 @@ class GetApplicationsByStaffIdVariablesBuilder {
_limit.value = t;
return this;
}
GetApplicationsByStaffIdVariablesBuilder dayStart(Timestamp? t) {
_dayStart.value = t;
return this;
}
GetApplicationsByStaffIdVariablesBuilder dayEnd(Timestamp? t) {
_dayEnd.value = t;
return this;
}
GetApplicationsByStaffIdVariablesBuilder(this._dataConnect, {required this.staffId,});
Deserializer<GetApplicationsByStaffIdData> dataDeserializer = (dynamic json) => GetApplicationsByStaffIdData.fromJson(jsonDecode(json));
@@ -22,7 +32,7 @@ class GetApplicationsByStaffIdVariablesBuilder {
}
QueryRef<GetApplicationsByStaffIdData, GetApplicationsByStaffIdVariables> ref() {
GetApplicationsByStaffIdVariables vars= GetApplicationsByStaffIdVariables(staffId: staffId,offset: _offset,limit: _limit,);
GetApplicationsByStaffIdVariables vars= GetApplicationsByStaffIdVariables(staffId: staffId,offset: _offset,limit: _limit,dayStart: _dayStart,dayEnd: _dayEnd,);
return _dataConnect.query("getApplicationsByStaffId", dataDeserializer, varsSerializer, vars);
}
}
@@ -132,6 +142,10 @@ class GetApplicationsByStaffIdApplicationsShift {
final Timestamp? endTime;
final String? location;
final EnumValue<ShiftStatus>? status;
final int? durationDays;
final String? description;
final double? latitude;
final double? longitude;
final GetApplicationsByStaffIdApplicationsShiftOrder order;
GetApplicationsByStaffIdApplicationsShift.fromJson(dynamic json):
@@ -142,6 +156,10 @@ class GetApplicationsByStaffIdApplicationsShift {
endTime = json['endTime'] == null ? null : Timestamp.fromJson(json['endTime']),
location = json['location'] == null ? null : nativeFromJson<String>(json['location']),
status = json['status'] == null ? null : shiftStatusDeserializer(json['status']),
durationDays = json['durationDays'] == null ? null : nativeFromJson<int>(json['durationDays']),
description = json['description'] == null ? null : nativeFromJson<String>(json['description']),
latitude = json['latitude'] == null ? null : nativeFromJson<double>(json['latitude']),
longitude = json['longitude'] == null ? null : nativeFromJson<double>(json['longitude']),
order = GetApplicationsByStaffIdApplicationsShiftOrder.fromJson(json['order']);
@override
bool operator ==(Object other) {
@@ -160,11 +178,15 @@ class GetApplicationsByStaffIdApplicationsShift {
endTime == otherTyped.endTime &&
location == otherTyped.location &&
status == otherTyped.status &&
durationDays == otherTyped.durationDays &&
description == otherTyped.description &&
latitude == otherTyped.latitude &&
longitude == otherTyped.longitude &&
order == otherTyped.order;
}
@override
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, startTime.hashCode, endTime.hashCode, location.hashCode, status.hashCode, order.hashCode]);
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, startTime.hashCode, endTime.hashCode, location.hashCode, status.hashCode, durationDays.hashCode, description.hashCode, latitude.hashCode, longitude.hashCode, order.hashCode]);
Map<String, dynamic> toJson() {
@@ -188,6 +210,18 @@ class GetApplicationsByStaffIdApplicationsShift {
shiftStatusSerializer(status!)
;
}
if (durationDays != null) {
json['durationDays'] = nativeToJson<int?>(durationDays);
}
if (description != null) {
json['description'] = nativeToJson<String?>(description);
}
if (latitude != null) {
json['latitude'] = nativeToJson<double?>(latitude);
}
if (longitude != null) {
json['longitude'] = nativeToJson<double?>(longitude);
}
json['order'] = order.toJson();
return json;
}
@@ -200,6 +234,10 @@ class GetApplicationsByStaffIdApplicationsShift {
this.endTime,
this.location,
this.status,
this.durationDays,
this.description,
this.latitude,
this.longitude,
required this.order,
});
}
@@ -314,12 +352,14 @@ class GetApplicationsByStaffIdApplicationsShiftOrderBusiness {
final String businessName;
final String? email;
final String? contactName;
final String? companyLogoUrl;
GetApplicationsByStaffIdApplicationsShiftOrderBusiness.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
businessName = nativeFromJson<String>(json['businessName']),
email = json['email'] == null ? null : nativeFromJson<String>(json['email']),
contactName = json['contactName'] == null ? null : nativeFromJson<String>(json['contactName']);
contactName = json['contactName'] == null ? null : nativeFromJson<String>(json['contactName']),
companyLogoUrl = json['companyLogoUrl'] == null ? null : nativeFromJson<String>(json['companyLogoUrl']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
@@ -333,11 +373,12 @@ class GetApplicationsByStaffIdApplicationsShiftOrderBusiness {
return id == otherTyped.id &&
businessName == otherTyped.businessName &&
email == otherTyped.email &&
contactName == otherTyped.contactName;
contactName == otherTyped.contactName &&
companyLogoUrl == otherTyped.companyLogoUrl;
}
@override
int get hashCode => Object.hashAll([id.hashCode, businessName.hashCode, email.hashCode, contactName.hashCode]);
int get hashCode => Object.hashAll([id.hashCode, businessName.hashCode, email.hashCode, contactName.hashCode, companyLogoUrl.hashCode]);
Map<String, dynamic> toJson() {
@@ -350,6 +391,9 @@ class GetApplicationsByStaffIdApplicationsShiftOrderBusiness {
if (contactName != null) {
json['contactName'] = nativeToJson<String?>(contactName);
}
if (companyLogoUrl != null) {
json['companyLogoUrl'] = nativeToJson<String?>(companyLogoUrl);
}
return json;
}
@@ -358,6 +402,7 @@ class GetApplicationsByStaffIdApplicationsShiftOrderBusiness {
required this.businessName,
this.email,
this.contactName,
this.companyLogoUrl,
});
}
@@ -569,6 +614,8 @@ class GetApplicationsByStaffIdVariables {
final String staffId;
late final Optional<int>offset;
late final Optional<int>limit;
late final Optional<Timestamp>dayStart;
late final Optional<Timestamp>dayEnd;
@Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.')
GetApplicationsByStaffIdVariables.fromJson(Map<String, dynamic> json):
@@ -583,6 +630,14 @@ class GetApplicationsByStaffIdVariables {
limit = Optional.optional(nativeFromJson, nativeToJson);
limit.value = json['limit'] == null ? null : nativeFromJson<int>(json['limit']);
dayStart = Optional.optional((json) => json['dayStart'] = Timestamp.fromJson(json['dayStart']), defaultSerializer);
dayStart.value = json['dayStart'] == null ? null : Timestamp.fromJson(json['dayStart']);
dayEnd = Optional.optional((json) => json['dayEnd'] = Timestamp.fromJson(json['dayEnd']), defaultSerializer);
dayEnd.value = json['dayEnd'] == null ? null : Timestamp.fromJson(json['dayEnd']);
}
@override
bool operator ==(Object other) {
@@ -596,11 +651,13 @@ class GetApplicationsByStaffIdVariables {
final GetApplicationsByStaffIdVariables otherTyped = other as GetApplicationsByStaffIdVariables;
return staffId == otherTyped.staffId &&
offset == otherTyped.offset &&
limit == otherTyped.limit;
limit == otherTyped.limit &&
dayStart == otherTyped.dayStart &&
dayEnd == otherTyped.dayEnd;
}
@override
int get hashCode => Object.hashAll([staffId.hashCode, offset.hashCode, limit.hashCode]);
int get hashCode => Object.hashAll([staffId.hashCode, offset.hashCode, limit.hashCode, dayStart.hashCode, dayEnd.hashCode]);
Map<String, dynamic> toJson() {
@@ -612,6 +669,12 @@ class GetApplicationsByStaffIdVariables {
if(limit.state == OptionalState.set) {
json['limit'] = limit.toJson();
}
if(dayStart.state == OptionalState.set) {
json['dayStart'] = dayStart.toJson();
}
if(dayEnd.state == OptionalState.set) {
json['dayEnd'] = dayEnd.toJson();
}
return json;
}
@@ -619,6 +682,8 @@ class GetApplicationsByStaffIdVariables {
required this.staffId,
required this.offset,
required this.limit,
required this.dayStart,
required this.dayEnd,
});
}

View File

@@ -253,13 +253,15 @@ class GetShiftRoleByIdShiftRoleShiftOrder {
final String? notes;
final GetShiftRoleByIdShiftRoleShiftOrderBusiness business;
final GetShiftRoleByIdShiftRoleShiftOrderVendor? vendor;
final GetShiftRoleByIdShiftRoleShiftOrderTeamHub teamHub;
GetShiftRoleByIdShiftRoleShiftOrder.fromJson(dynamic json):
recurringDays = json['recurringDays'] == null ? null : AnyValue.fromJson(json['recurringDays']),
permanentDays = json['permanentDays'] == null ? null : AnyValue.fromJson(json['permanentDays']),
notes = json['notes'] == null ? null : nativeFromJson<String>(json['notes']),
business = GetShiftRoleByIdShiftRoleShiftOrderBusiness.fromJson(json['business']),
vendor = json['vendor'] == null ? null : GetShiftRoleByIdShiftRoleShiftOrderVendor.fromJson(json['vendor']);
vendor = json['vendor'] == null ? null : GetShiftRoleByIdShiftRoleShiftOrderVendor.fromJson(json['vendor']),
teamHub = GetShiftRoleByIdShiftRoleShiftOrderTeamHub.fromJson(json['teamHub']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
@@ -274,11 +276,12 @@ class GetShiftRoleByIdShiftRoleShiftOrder {
permanentDays == otherTyped.permanentDays &&
notes == otherTyped.notes &&
business == otherTyped.business &&
vendor == otherTyped.vendor;
vendor == otherTyped.vendor &&
teamHub == otherTyped.teamHub;
}
@override
int get hashCode => Object.hashAll([recurringDays.hashCode, permanentDays.hashCode, notes.hashCode, business.hashCode, vendor.hashCode]);
int get hashCode => Object.hashAll([recurringDays.hashCode, permanentDays.hashCode, notes.hashCode, business.hashCode, vendor.hashCode, teamHub.hashCode]);
Map<String, dynamic> toJson() {
@@ -296,6 +299,7 @@ class GetShiftRoleByIdShiftRoleShiftOrder {
if (vendor != null) {
json['vendor'] = vendor!.toJson();
}
json['teamHub'] = teamHub.toJson();
return json;
}
@@ -305,6 +309,7 @@ class GetShiftRoleByIdShiftRoleShiftOrder {
this.notes,
required this.business,
this.vendor,
required this.teamHub,
});
}
@@ -312,10 +317,12 @@ class GetShiftRoleByIdShiftRoleShiftOrder {
class GetShiftRoleByIdShiftRoleShiftOrderBusiness {
final String id;
final String businessName;
final String? companyLogoUrl;
GetShiftRoleByIdShiftRoleShiftOrderBusiness.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
businessName = nativeFromJson<String>(json['businessName']);
businessName = nativeFromJson<String>(json['businessName']),
companyLogoUrl = json['companyLogoUrl'] == null ? null : nativeFromJson<String>(json['companyLogoUrl']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
@@ -327,23 +334,28 @@ class GetShiftRoleByIdShiftRoleShiftOrderBusiness {
final GetShiftRoleByIdShiftRoleShiftOrderBusiness otherTyped = other as GetShiftRoleByIdShiftRoleShiftOrderBusiness;
return id == otherTyped.id &&
businessName == otherTyped.businessName;
businessName == otherTyped.businessName &&
companyLogoUrl == otherTyped.companyLogoUrl;
}
@override
int get hashCode => Object.hashAll([id.hashCode, businessName.hashCode]);
int get hashCode => Object.hashAll([id.hashCode, businessName.hashCode, companyLogoUrl.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['businessName'] = nativeToJson<String>(businessName);
if (companyLogoUrl != null) {
json['companyLogoUrl'] = nativeToJson<String?>(companyLogoUrl);
}
return json;
}
GetShiftRoleByIdShiftRoleShiftOrderBusiness({
required this.id,
required this.businessName,
this.companyLogoUrl,
});
}
@@ -386,6 +398,40 @@ class GetShiftRoleByIdShiftRoleShiftOrderVendor {
});
}
@immutable
class GetShiftRoleByIdShiftRoleShiftOrderTeamHub {
final String hubName;
GetShiftRoleByIdShiftRoleShiftOrderTeamHub.fromJson(dynamic json):
hubName = nativeFromJson<String>(json['hubName']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final GetShiftRoleByIdShiftRoleShiftOrderTeamHub otherTyped = other as GetShiftRoleByIdShiftRoleShiftOrderTeamHub;
return hubName == otherTyped.hubName;
}
@override
int get hashCode => hubName.hashCode;
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['hubName'] = nativeToJson<String>(hubName);
return json;
}
GetShiftRoleByIdShiftRoleShiftOrderTeamHub({
required this.hubName,
});
}
@immutable
class GetShiftRoleByIdData {
final GetShiftRoleByIdShiftRole? shiftRole;

View File

@@ -0,0 +1,645 @@
part of 'generated.dart';
class ListCompletedApplicationsByStaffIdVariablesBuilder {
String staffId;
Optional<int> _offset = Optional.optional(nativeFromJson, nativeToJson);
Optional<int> _limit = Optional.optional(nativeFromJson, nativeToJson);
final FirebaseDataConnect _dataConnect; ListCompletedApplicationsByStaffIdVariablesBuilder offset(int? t) {
_offset.value = t;
return this;
}
ListCompletedApplicationsByStaffIdVariablesBuilder limit(int? t) {
_limit.value = t;
return this;
}
ListCompletedApplicationsByStaffIdVariablesBuilder(this._dataConnect, {required this.staffId,});
Deserializer<ListCompletedApplicationsByStaffIdData> dataDeserializer = (dynamic json) => ListCompletedApplicationsByStaffIdData.fromJson(jsonDecode(json));
Serializer<ListCompletedApplicationsByStaffIdVariables> varsSerializer = (ListCompletedApplicationsByStaffIdVariables vars) => jsonEncode(vars.toJson());
Future<QueryResult<ListCompletedApplicationsByStaffIdData, ListCompletedApplicationsByStaffIdVariables>> execute() {
return ref().execute();
}
QueryRef<ListCompletedApplicationsByStaffIdData, ListCompletedApplicationsByStaffIdVariables> ref() {
ListCompletedApplicationsByStaffIdVariables vars= ListCompletedApplicationsByStaffIdVariables(staffId: staffId,offset: _offset,limit: _limit,);
return _dataConnect.query("listCompletedApplicationsByStaffId", dataDeserializer, varsSerializer, vars);
}
}
@immutable
class ListCompletedApplicationsByStaffIdApplications {
final String id;
final String shiftId;
final String staffId;
final EnumValue<ApplicationStatus> status;
final Timestamp? appliedAt;
final Timestamp? checkInTime;
final Timestamp? checkOutTime;
final EnumValue<ApplicationOrigin> origin;
final Timestamp? createdAt;
final ListCompletedApplicationsByStaffIdApplicationsShift shift;
final ListCompletedApplicationsByStaffIdApplicationsShiftRole shiftRole;
ListCompletedApplicationsByStaffIdApplications.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
shiftId = nativeFromJson<String>(json['shiftId']),
staffId = nativeFromJson<String>(json['staffId']),
status = applicationStatusDeserializer(json['status']),
appliedAt = json['appliedAt'] == null ? null : Timestamp.fromJson(json['appliedAt']),
checkInTime = json['checkInTime'] == null ? null : Timestamp.fromJson(json['checkInTime']),
checkOutTime = json['checkOutTime'] == null ? null : Timestamp.fromJson(json['checkOutTime']),
origin = applicationOriginDeserializer(json['origin']),
createdAt = json['createdAt'] == null ? null : Timestamp.fromJson(json['createdAt']),
shift = ListCompletedApplicationsByStaffIdApplicationsShift.fromJson(json['shift']),
shiftRole = ListCompletedApplicationsByStaffIdApplicationsShiftRole.fromJson(json['shiftRole']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplications otherTyped = other as ListCompletedApplicationsByStaffIdApplications;
return id == otherTyped.id &&
shiftId == otherTyped.shiftId &&
staffId == otherTyped.staffId &&
status == otherTyped.status &&
appliedAt == otherTyped.appliedAt &&
checkInTime == otherTyped.checkInTime &&
checkOutTime == otherTyped.checkOutTime &&
origin == otherTyped.origin &&
createdAt == otherTyped.createdAt &&
shift == otherTyped.shift &&
shiftRole == otherTyped.shiftRole;
}
@override
int get hashCode => Object.hashAll([id.hashCode, shiftId.hashCode, staffId.hashCode, status.hashCode, appliedAt.hashCode, checkInTime.hashCode, checkOutTime.hashCode, origin.hashCode, createdAt.hashCode, shift.hashCode, shiftRole.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['shiftId'] = nativeToJson<String>(shiftId);
json['staffId'] = nativeToJson<String>(staffId);
json['status'] =
applicationStatusSerializer(status)
;
if (appliedAt != null) {
json['appliedAt'] = appliedAt!.toJson();
}
if (checkInTime != null) {
json['checkInTime'] = checkInTime!.toJson();
}
if (checkOutTime != null) {
json['checkOutTime'] = checkOutTime!.toJson();
}
json['origin'] =
applicationOriginSerializer(origin)
;
if (createdAt != null) {
json['createdAt'] = createdAt!.toJson();
}
json['shift'] = shift.toJson();
json['shiftRole'] = shiftRole.toJson();
return json;
}
ListCompletedApplicationsByStaffIdApplications({
required this.id,
required this.shiftId,
required this.staffId,
required this.status,
this.appliedAt,
this.checkInTime,
this.checkOutTime,
required this.origin,
this.createdAt,
required this.shift,
required this.shiftRole,
});
}
@immutable
class ListCompletedApplicationsByStaffIdApplicationsShift {
final String id;
final String title;
final Timestamp? date;
final Timestamp? startTime;
final Timestamp? endTime;
final String? location;
final EnumValue<ShiftStatus>? status;
final String? description;
final int? durationDays;
final ListCompletedApplicationsByStaffIdApplicationsShiftOrder order;
ListCompletedApplicationsByStaffIdApplicationsShift.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
title = nativeFromJson<String>(json['title']),
date = json['date'] == null ? null : Timestamp.fromJson(json['date']),
startTime = json['startTime'] == null ? null : Timestamp.fromJson(json['startTime']),
endTime = json['endTime'] == null ? null : Timestamp.fromJson(json['endTime']),
location = json['location'] == null ? null : nativeFromJson<String>(json['location']),
status = json['status'] == null ? null : shiftStatusDeserializer(json['status']),
description = json['description'] == null ? null : nativeFromJson<String>(json['description']),
durationDays = json['durationDays'] == null ? null : nativeFromJson<int>(json['durationDays']),
order = ListCompletedApplicationsByStaffIdApplicationsShiftOrder.fromJson(json['order']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplicationsShift otherTyped = other as ListCompletedApplicationsByStaffIdApplicationsShift;
return id == otherTyped.id &&
title == otherTyped.title &&
date == otherTyped.date &&
startTime == otherTyped.startTime &&
endTime == otherTyped.endTime &&
location == otherTyped.location &&
status == otherTyped.status &&
description == otherTyped.description &&
durationDays == otherTyped.durationDays &&
order == otherTyped.order;
}
@override
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, startTime.hashCode, endTime.hashCode, location.hashCode, status.hashCode, description.hashCode, durationDays.hashCode, order.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['title'] = nativeToJson<String>(title);
if (date != null) {
json['date'] = date!.toJson();
}
if (startTime != null) {
json['startTime'] = startTime!.toJson();
}
if (endTime != null) {
json['endTime'] = endTime!.toJson();
}
if (location != null) {
json['location'] = nativeToJson<String?>(location);
}
if (status != null) {
json['status'] =
shiftStatusSerializer(status!)
;
}
if (description != null) {
json['description'] = nativeToJson<String?>(description);
}
if (durationDays != null) {
json['durationDays'] = nativeToJson<int?>(durationDays);
}
json['order'] = order.toJson();
return json;
}
ListCompletedApplicationsByStaffIdApplicationsShift({
required this.id,
required this.title,
this.date,
this.startTime,
this.endTime,
this.location,
this.status,
this.description,
this.durationDays,
required this.order,
});
}
@immutable
class ListCompletedApplicationsByStaffIdApplicationsShiftOrder {
final String id;
final String? eventName;
final ListCompletedApplicationsByStaffIdApplicationsShiftOrderTeamHub teamHub;
final ListCompletedApplicationsByStaffIdApplicationsShiftOrderBusiness business;
final ListCompletedApplicationsByStaffIdApplicationsShiftOrderVendor? vendor;
ListCompletedApplicationsByStaffIdApplicationsShiftOrder.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
eventName = json['eventName'] == null ? null : nativeFromJson<String>(json['eventName']),
teamHub = ListCompletedApplicationsByStaffIdApplicationsShiftOrderTeamHub.fromJson(json['teamHub']),
business = ListCompletedApplicationsByStaffIdApplicationsShiftOrderBusiness.fromJson(json['business']),
vendor = json['vendor'] == null ? null : ListCompletedApplicationsByStaffIdApplicationsShiftOrderVendor.fromJson(json['vendor']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplicationsShiftOrder otherTyped = other as ListCompletedApplicationsByStaffIdApplicationsShiftOrder;
return id == otherTyped.id &&
eventName == otherTyped.eventName &&
teamHub == otherTyped.teamHub &&
business == otherTyped.business &&
vendor == otherTyped.vendor;
}
@override
int get hashCode => Object.hashAll([id.hashCode, eventName.hashCode, teamHub.hashCode, business.hashCode, vendor.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
if (eventName != null) {
json['eventName'] = nativeToJson<String?>(eventName);
}
json['teamHub'] = teamHub.toJson();
json['business'] = business.toJson();
if (vendor != null) {
json['vendor'] = vendor!.toJson();
}
return json;
}
ListCompletedApplicationsByStaffIdApplicationsShiftOrder({
required this.id,
this.eventName,
required this.teamHub,
required this.business,
this.vendor,
});
}
@immutable
class ListCompletedApplicationsByStaffIdApplicationsShiftOrderTeamHub {
final String address;
final String? placeId;
final String hubName;
ListCompletedApplicationsByStaffIdApplicationsShiftOrderTeamHub.fromJson(dynamic json):
address = nativeFromJson<String>(json['address']),
placeId = json['placeId'] == null ? null : nativeFromJson<String>(json['placeId']),
hubName = nativeFromJson<String>(json['hubName']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplicationsShiftOrderTeamHub otherTyped = other as ListCompletedApplicationsByStaffIdApplicationsShiftOrderTeamHub;
return address == otherTyped.address &&
placeId == otherTyped.placeId &&
hubName == otherTyped.hubName;
}
@override
int get hashCode => Object.hashAll([address.hashCode, placeId.hashCode, hubName.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['address'] = nativeToJson<String>(address);
if (placeId != null) {
json['placeId'] = nativeToJson<String?>(placeId);
}
json['hubName'] = nativeToJson<String>(hubName);
return json;
}
ListCompletedApplicationsByStaffIdApplicationsShiftOrderTeamHub({
required this.address,
this.placeId,
required this.hubName,
});
}
@immutable
class ListCompletedApplicationsByStaffIdApplicationsShiftOrderBusiness {
final String id;
final String businessName;
final String? email;
final String? contactName;
final String? companyLogoUrl;
ListCompletedApplicationsByStaffIdApplicationsShiftOrderBusiness.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
businessName = nativeFromJson<String>(json['businessName']),
email = json['email'] == null ? null : nativeFromJson<String>(json['email']),
contactName = json['contactName'] == null ? null : nativeFromJson<String>(json['contactName']),
companyLogoUrl = json['companyLogoUrl'] == null ? null : nativeFromJson<String>(json['companyLogoUrl']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplicationsShiftOrderBusiness otherTyped = other as ListCompletedApplicationsByStaffIdApplicationsShiftOrderBusiness;
return id == otherTyped.id &&
businessName == otherTyped.businessName &&
email == otherTyped.email &&
contactName == otherTyped.contactName &&
companyLogoUrl == otherTyped.companyLogoUrl;
}
@override
int get hashCode => Object.hashAll([id.hashCode, businessName.hashCode, email.hashCode, contactName.hashCode, companyLogoUrl.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['businessName'] = nativeToJson<String>(businessName);
if (email != null) {
json['email'] = nativeToJson<String?>(email);
}
if (contactName != null) {
json['contactName'] = nativeToJson<String?>(contactName);
}
if (companyLogoUrl != null) {
json['companyLogoUrl'] = nativeToJson<String?>(companyLogoUrl);
}
return json;
}
ListCompletedApplicationsByStaffIdApplicationsShiftOrderBusiness({
required this.id,
required this.businessName,
this.email,
this.contactName,
this.companyLogoUrl,
});
}
@immutable
class ListCompletedApplicationsByStaffIdApplicationsShiftOrderVendor {
final String id;
final String companyName;
ListCompletedApplicationsByStaffIdApplicationsShiftOrderVendor.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
companyName = nativeFromJson<String>(json['companyName']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplicationsShiftOrderVendor otherTyped = other as ListCompletedApplicationsByStaffIdApplicationsShiftOrderVendor;
return id == otherTyped.id &&
companyName == otherTyped.companyName;
}
@override
int get hashCode => Object.hashAll([id.hashCode, companyName.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['companyName'] = nativeToJson<String>(companyName);
return json;
}
ListCompletedApplicationsByStaffIdApplicationsShiftOrderVendor({
required this.id,
required this.companyName,
});
}
@immutable
class ListCompletedApplicationsByStaffIdApplicationsShiftRole {
final String id;
final String roleId;
final int count;
final int? assigned;
final Timestamp? startTime;
final Timestamp? endTime;
final double? hours;
final double? totalValue;
final ListCompletedApplicationsByStaffIdApplicationsShiftRoleRole role;
ListCompletedApplicationsByStaffIdApplicationsShiftRole.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
roleId = nativeFromJson<String>(json['roleId']),
count = nativeFromJson<int>(json['count']),
assigned = json['assigned'] == null ? null : nativeFromJson<int>(json['assigned']),
startTime = json['startTime'] == null ? null : Timestamp.fromJson(json['startTime']),
endTime = json['endTime'] == null ? null : Timestamp.fromJson(json['endTime']),
hours = json['hours'] == null ? null : nativeFromJson<double>(json['hours']),
totalValue = json['totalValue'] == null ? null : nativeFromJson<double>(json['totalValue']),
role = ListCompletedApplicationsByStaffIdApplicationsShiftRoleRole.fromJson(json['role']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplicationsShiftRole otherTyped = other as ListCompletedApplicationsByStaffIdApplicationsShiftRole;
return id == otherTyped.id &&
roleId == otherTyped.roleId &&
count == otherTyped.count &&
assigned == otherTyped.assigned &&
startTime == otherTyped.startTime &&
endTime == otherTyped.endTime &&
hours == otherTyped.hours &&
totalValue == otherTyped.totalValue &&
role == otherTyped.role;
}
@override
int get hashCode => Object.hashAll([id.hashCode, roleId.hashCode, count.hashCode, assigned.hashCode, startTime.hashCode, endTime.hashCode, hours.hashCode, totalValue.hashCode, role.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['roleId'] = nativeToJson<String>(roleId);
json['count'] = nativeToJson<int>(count);
if (assigned != null) {
json['assigned'] = nativeToJson<int?>(assigned);
}
if (startTime != null) {
json['startTime'] = startTime!.toJson();
}
if (endTime != null) {
json['endTime'] = endTime!.toJson();
}
if (hours != null) {
json['hours'] = nativeToJson<double?>(hours);
}
if (totalValue != null) {
json['totalValue'] = nativeToJson<double?>(totalValue);
}
json['role'] = role.toJson();
return json;
}
ListCompletedApplicationsByStaffIdApplicationsShiftRole({
required this.id,
required this.roleId,
required this.count,
this.assigned,
this.startTime,
this.endTime,
this.hours,
this.totalValue,
required this.role,
});
}
@immutable
class ListCompletedApplicationsByStaffIdApplicationsShiftRoleRole {
final String id;
final String name;
final double costPerHour;
ListCompletedApplicationsByStaffIdApplicationsShiftRoleRole.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
name = nativeFromJson<String>(json['name']),
costPerHour = nativeFromJson<double>(json['costPerHour']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdApplicationsShiftRoleRole otherTyped = other as ListCompletedApplicationsByStaffIdApplicationsShiftRoleRole;
return id == otherTyped.id &&
name == otherTyped.name &&
costPerHour == otherTyped.costPerHour;
}
@override
int get hashCode => Object.hashAll([id.hashCode, name.hashCode, costPerHour.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['name'] = nativeToJson<String>(name);
json['costPerHour'] = nativeToJson<double>(costPerHour);
return json;
}
ListCompletedApplicationsByStaffIdApplicationsShiftRoleRole({
required this.id,
required this.name,
required this.costPerHour,
});
}
@immutable
class ListCompletedApplicationsByStaffIdData {
final List<ListCompletedApplicationsByStaffIdApplications> applications;
ListCompletedApplicationsByStaffIdData.fromJson(dynamic json):
applications = (json['applications'] as List<dynamic>)
.map((e) => ListCompletedApplicationsByStaffIdApplications.fromJson(e))
.toList();
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdData otherTyped = other as ListCompletedApplicationsByStaffIdData;
return applications == otherTyped.applications;
}
@override
int get hashCode => applications.hashCode;
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['applications'] = applications.map((e) => e.toJson()).toList();
return json;
}
ListCompletedApplicationsByStaffIdData({
required this.applications,
});
}
@immutable
class ListCompletedApplicationsByStaffIdVariables {
final String staffId;
late final Optional<int>offset;
late final Optional<int>limit;
@Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.')
ListCompletedApplicationsByStaffIdVariables.fromJson(Map<String, dynamic> json):
staffId = nativeFromJson<String>(json['staffId']) {
offset = Optional.optional(nativeFromJson, nativeToJson);
offset.value = json['offset'] == null ? null : nativeFromJson<int>(json['offset']);
limit = Optional.optional(nativeFromJson, nativeToJson);
limit.value = json['limit'] == null ? null : nativeFromJson<int>(json['limit']);
}
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final ListCompletedApplicationsByStaffIdVariables otherTyped = other as ListCompletedApplicationsByStaffIdVariables;
return staffId == otherTyped.staffId &&
offset == otherTyped.offset &&
limit == otherTyped.limit;
}
@override
int get hashCode => Object.hashAll([staffId.hashCode, offset.hashCode, limit.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['staffId'] = nativeToJson<String>(staffId);
if(offset.state == OptionalState.set) {
json['offset'] = offset.toJson();
}
if(limit.state == OptionalState.set) {
json['limit'] = limit.toJson();
}
return json;
}
ListCompletedApplicationsByStaffIdVariables({
required this.staffId,
required this.offset,
required this.limit,
});
}

View File

@@ -204,6 +204,8 @@ class ListShiftRolesByVendorIdShiftRolesShift {
final String? locationAddress;
final String? description;
final String orderId;
final EnumValue<ShiftStatus>? status;
final int? durationDays;
final ListShiftRolesByVendorIdShiftRolesShiftOrder order;
ListShiftRolesByVendorIdShiftRolesShift.fromJson(dynamic json):
@@ -214,6 +216,8 @@ class ListShiftRolesByVendorIdShiftRolesShift {
locationAddress = json['locationAddress'] == null ? null : nativeFromJson<String>(json['locationAddress']),
description = json['description'] == null ? null : nativeFromJson<String>(json['description']),
orderId = nativeFromJson<String>(json['orderId']),
status = json['status'] == null ? null : shiftStatusDeserializer(json['status']),
durationDays = json['durationDays'] == null ? null : nativeFromJson<int>(json['durationDays']),
order = ListShiftRolesByVendorIdShiftRolesShiftOrder.fromJson(json['order']);
@override
bool operator ==(Object other) {
@@ -232,11 +236,13 @@ class ListShiftRolesByVendorIdShiftRolesShift {
locationAddress == otherTyped.locationAddress &&
description == otherTyped.description &&
orderId == otherTyped.orderId &&
status == otherTyped.status &&
durationDays == otherTyped.durationDays &&
order == otherTyped.order;
}
@override
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, location.hashCode, locationAddress.hashCode, description.hashCode, orderId.hashCode, order.hashCode]);
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, location.hashCode, locationAddress.hashCode, description.hashCode, orderId.hashCode, status.hashCode, durationDays.hashCode, order.hashCode]);
Map<String, dynamic> toJson() {
@@ -256,6 +262,14 @@ class ListShiftRolesByVendorIdShiftRolesShift {
json['description'] = nativeToJson<String?>(description);
}
json['orderId'] = nativeToJson<String>(orderId);
if (status != null) {
json['status'] =
shiftStatusSerializer(status!)
;
}
if (durationDays != null) {
json['durationDays'] = nativeToJson<int?>(durationDays);
}
json['order'] = order.toJson();
return json;
}
@@ -268,6 +282,8 @@ class ListShiftRolesByVendorIdShiftRolesShift {
this.locationAddress,
this.description,
required this.orderId,
this.status,
this.durationDays,
required this.order,
});
}

View File

@@ -0,0 +1,675 @@
part of 'generated.dart';
class VaidateDayStaffApplicationVariablesBuilder {
String staffId;
Optional<int> _offset = Optional.optional(nativeFromJson, nativeToJson);
Optional<int> _limit = Optional.optional(nativeFromJson, nativeToJson);
Optional<Timestamp> _dayStart = Optional.optional((json) => json['dayStart'] = Timestamp.fromJson(json['dayStart']), defaultSerializer);
Optional<Timestamp> _dayEnd = Optional.optional((json) => json['dayEnd'] = Timestamp.fromJson(json['dayEnd']), defaultSerializer);
final FirebaseDataConnect _dataConnect; VaidateDayStaffApplicationVariablesBuilder offset(int? t) {
_offset.value = t;
return this;
}
VaidateDayStaffApplicationVariablesBuilder limit(int? t) {
_limit.value = t;
return this;
}
VaidateDayStaffApplicationVariablesBuilder dayStart(Timestamp? t) {
_dayStart.value = t;
return this;
}
VaidateDayStaffApplicationVariablesBuilder dayEnd(Timestamp? t) {
_dayEnd.value = t;
return this;
}
VaidateDayStaffApplicationVariablesBuilder(this._dataConnect, {required this.staffId,});
Deserializer<VaidateDayStaffApplicationData> dataDeserializer = (dynamic json) => VaidateDayStaffApplicationData.fromJson(jsonDecode(json));
Serializer<VaidateDayStaffApplicationVariables> varsSerializer = (VaidateDayStaffApplicationVariables vars) => jsonEncode(vars.toJson());
Future<QueryResult<VaidateDayStaffApplicationData, VaidateDayStaffApplicationVariables>> execute() {
return ref().execute();
}
QueryRef<VaidateDayStaffApplicationData, VaidateDayStaffApplicationVariables> ref() {
VaidateDayStaffApplicationVariables vars= VaidateDayStaffApplicationVariables(staffId: staffId,offset: _offset,limit: _limit,dayStart: _dayStart,dayEnd: _dayEnd,);
return _dataConnect.query("vaidateDayStaffApplication", dataDeserializer, varsSerializer, vars);
}
}
@immutable
class VaidateDayStaffApplicationApplications {
final String id;
final String shiftId;
final String staffId;
final EnumValue<ApplicationStatus> status;
final Timestamp? appliedAt;
final Timestamp? checkInTime;
final Timestamp? checkOutTime;
final EnumValue<ApplicationOrigin> origin;
final Timestamp? createdAt;
final VaidateDayStaffApplicationApplicationsShift shift;
final VaidateDayStaffApplicationApplicationsShiftRole shiftRole;
VaidateDayStaffApplicationApplications.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
shiftId = nativeFromJson<String>(json['shiftId']),
staffId = nativeFromJson<String>(json['staffId']),
status = applicationStatusDeserializer(json['status']),
appliedAt = json['appliedAt'] == null ? null : Timestamp.fromJson(json['appliedAt']),
checkInTime = json['checkInTime'] == null ? null : Timestamp.fromJson(json['checkInTime']),
checkOutTime = json['checkOutTime'] == null ? null : Timestamp.fromJson(json['checkOutTime']),
origin = applicationOriginDeserializer(json['origin']),
createdAt = json['createdAt'] == null ? null : Timestamp.fromJson(json['createdAt']),
shift = VaidateDayStaffApplicationApplicationsShift.fromJson(json['shift']),
shiftRole = VaidateDayStaffApplicationApplicationsShiftRole.fromJson(json['shiftRole']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplications otherTyped = other as VaidateDayStaffApplicationApplications;
return id == otherTyped.id &&
shiftId == otherTyped.shiftId &&
staffId == otherTyped.staffId &&
status == otherTyped.status &&
appliedAt == otherTyped.appliedAt &&
checkInTime == otherTyped.checkInTime &&
checkOutTime == otherTyped.checkOutTime &&
origin == otherTyped.origin &&
createdAt == otherTyped.createdAt &&
shift == otherTyped.shift &&
shiftRole == otherTyped.shiftRole;
}
@override
int get hashCode => Object.hashAll([id.hashCode, shiftId.hashCode, staffId.hashCode, status.hashCode, appliedAt.hashCode, checkInTime.hashCode, checkOutTime.hashCode, origin.hashCode, createdAt.hashCode, shift.hashCode, shiftRole.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['shiftId'] = nativeToJson<String>(shiftId);
json['staffId'] = nativeToJson<String>(staffId);
json['status'] =
applicationStatusSerializer(status)
;
if (appliedAt != null) {
json['appliedAt'] = appliedAt!.toJson();
}
if (checkInTime != null) {
json['checkInTime'] = checkInTime!.toJson();
}
if (checkOutTime != null) {
json['checkOutTime'] = checkOutTime!.toJson();
}
json['origin'] =
applicationOriginSerializer(origin)
;
if (createdAt != null) {
json['createdAt'] = createdAt!.toJson();
}
json['shift'] = shift.toJson();
json['shiftRole'] = shiftRole.toJson();
return json;
}
VaidateDayStaffApplicationApplications({
required this.id,
required this.shiftId,
required this.staffId,
required this.status,
this.appliedAt,
this.checkInTime,
this.checkOutTime,
required this.origin,
this.createdAt,
required this.shift,
required this.shiftRole,
});
}
@immutable
class VaidateDayStaffApplicationApplicationsShift {
final String id;
final String title;
final Timestamp? date;
final Timestamp? startTime;
final Timestamp? endTime;
final String? location;
final EnumValue<ShiftStatus>? status;
final int? durationDays;
final String? description;
final VaidateDayStaffApplicationApplicationsShiftOrder order;
VaidateDayStaffApplicationApplicationsShift.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
title = nativeFromJson<String>(json['title']),
date = json['date'] == null ? null : Timestamp.fromJson(json['date']),
startTime = json['startTime'] == null ? null : Timestamp.fromJson(json['startTime']),
endTime = json['endTime'] == null ? null : Timestamp.fromJson(json['endTime']),
location = json['location'] == null ? null : nativeFromJson<String>(json['location']),
status = json['status'] == null ? null : shiftStatusDeserializer(json['status']),
durationDays = json['durationDays'] == null ? null : nativeFromJson<int>(json['durationDays']),
description = json['description'] == null ? null : nativeFromJson<String>(json['description']),
order = VaidateDayStaffApplicationApplicationsShiftOrder.fromJson(json['order']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplicationsShift otherTyped = other as VaidateDayStaffApplicationApplicationsShift;
return id == otherTyped.id &&
title == otherTyped.title &&
date == otherTyped.date &&
startTime == otherTyped.startTime &&
endTime == otherTyped.endTime &&
location == otherTyped.location &&
status == otherTyped.status &&
durationDays == otherTyped.durationDays &&
description == otherTyped.description &&
order == otherTyped.order;
}
@override
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, startTime.hashCode, endTime.hashCode, location.hashCode, status.hashCode, durationDays.hashCode, description.hashCode, order.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['title'] = nativeToJson<String>(title);
if (date != null) {
json['date'] = date!.toJson();
}
if (startTime != null) {
json['startTime'] = startTime!.toJson();
}
if (endTime != null) {
json['endTime'] = endTime!.toJson();
}
if (location != null) {
json['location'] = nativeToJson<String?>(location);
}
if (status != null) {
json['status'] =
shiftStatusSerializer(status!)
;
}
if (durationDays != null) {
json['durationDays'] = nativeToJson<int?>(durationDays);
}
if (description != null) {
json['description'] = nativeToJson<String?>(description);
}
json['order'] = order.toJson();
return json;
}
VaidateDayStaffApplicationApplicationsShift({
required this.id,
required this.title,
this.date,
this.startTime,
this.endTime,
this.location,
this.status,
this.durationDays,
this.description,
required this.order,
});
}
@immutable
class VaidateDayStaffApplicationApplicationsShiftOrder {
final String id;
final String? eventName;
final VaidateDayStaffApplicationApplicationsShiftOrderTeamHub teamHub;
final VaidateDayStaffApplicationApplicationsShiftOrderBusiness business;
final VaidateDayStaffApplicationApplicationsShiftOrderVendor? vendor;
VaidateDayStaffApplicationApplicationsShiftOrder.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
eventName = json['eventName'] == null ? null : nativeFromJson<String>(json['eventName']),
teamHub = VaidateDayStaffApplicationApplicationsShiftOrderTeamHub.fromJson(json['teamHub']),
business = VaidateDayStaffApplicationApplicationsShiftOrderBusiness.fromJson(json['business']),
vendor = json['vendor'] == null ? null : VaidateDayStaffApplicationApplicationsShiftOrderVendor.fromJson(json['vendor']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplicationsShiftOrder otherTyped = other as VaidateDayStaffApplicationApplicationsShiftOrder;
return id == otherTyped.id &&
eventName == otherTyped.eventName &&
teamHub == otherTyped.teamHub &&
business == otherTyped.business &&
vendor == otherTyped.vendor;
}
@override
int get hashCode => Object.hashAll([id.hashCode, eventName.hashCode, teamHub.hashCode, business.hashCode, vendor.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
if (eventName != null) {
json['eventName'] = nativeToJson<String?>(eventName);
}
json['teamHub'] = teamHub.toJson();
json['business'] = business.toJson();
if (vendor != null) {
json['vendor'] = vendor!.toJson();
}
return json;
}
VaidateDayStaffApplicationApplicationsShiftOrder({
required this.id,
this.eventName,
required this.teamHub,
required this.business,
this.vendor,
});
}
@immutable
class VaidateDayStaffApplicationApplicationsShiftOrderTeamHub {
final String address;
final String? placeId;
final String hubName;
VaidateDayStaffApplicationApplicationsShiftOrderTeamHub.fromJson(dynamic json):
address = nativeFromJson<String>(json['address']),
placeId = json['placeId'] == null ? null : nativeFromJson<String>(json['placeId']),
hubName = nativeFromJson<String>(json['hubName']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplicationsShiftOrderTeamHub otherTyped = other as VaidateDayStaffApplicationApplicationsShiftOrderTeamHub;
return address == otherTyped.address &&
placeId == otherTyped.placeId &&
hubName == otherTyped.hubName;
}
@override
int get hashCode => Object.hashAll([address.hashCode, placeId.hashCode, hubName.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['address'] = nativeToJson<String>(address);
if (placeId != null) {
json['placeId'] = nativeToJson<String?>(placeId);
}
json['hubName'] = nativeToJson<String>(hubName);
return json;
}
VaidateDayStaffApplicationApplicationsShiftOrderTeamHub({
required this.address,
this.placeId,
required this.hubName,
});
}
@immutable
class VaidateDayStaffApplicationApplicationsShiftOrderBusiness {
final String id;
final String businessName;
final String? email;
final String? contactName;
final String? companyLogoUrl;
VaidateDayStaffApplicationApplicationsShiftOrderBusiness.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
businessName = nativeFromJson<String>(json['businessName']),
email = json['email'] == null ? null : nativeFromJson<String>(json['email']),
contactName = json['contactName'] == null ? null : nativeFromJson<String>(json['contactName']),
companyLogoUrl = json['companyLogoUrl'] == null ? null : nativeFromJson<String>(json['companyLogoUrl']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplicationsShiftOrderBusiness otherTyped = other as VaidateDayStaffApplicationApplicationsShiftOrderBusiness;
return id == otherTyped.id &&
businessName == otherTyped.businessName &&
email == otherTyped.email &&
contactName == otherTyped.contactName &&
companyLogoUrl == otherTyped.companyLogoUrl;
}
@override
int get hashCode => Object.hashAll([id.hashCode, businessName.hashCode, email.hashCode, contactName.hashCode, companyLogoUrl.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['businessName'] = nativeToJson<String>(businessName);
if (email != null) {
json['email'] = nativeToJson<String?>(email);
}
if (contactName != null) {
json['contactName'] = nativeToJson<String?>(contactName);
}
if (companyLogoUrl != null) {
json['companyLogoUrl'] = nativeToJson<String?>(companyLogoUrl);
}
return json;
}
VaidateDayStaffApplicationApplicationsShiftOrderBusiness({
required this.id,
required this.businessName,
this.email,
this.contactName,
this.companyLogoUrl,
});
}
@immutable
class VaidateDayStaffApplicationApplicationsShiftOrderVendor {
final String id;
final String companyName;
VaidateDayStaffApplicationApplicationsShiftOrderVendor.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
companyName = nativeFromJson<String>(json['companyName']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplicationsShiftOrderVendor otherTyped = other as VaidateDayStaffApplicationApplicationsShiftOrderVendor;
return id == otherTyped.id &&
companyName == otherTyped.companyName;
}
@override
int get hashCode => Object.hashAll([id.hashCode, companyName.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['companyName'] = nativeToJson<String>(companyName);
return json;
}
VaidateDayStaffApplicationApplicationsShiftOrderVendor({
required this.id,
required this.companyName,
});
}
@immutable
class VaidateDayStaffApplicationApplicationsShiftRole {
final String id;
final String roleId;
final int count;
final int? assigned;
final Timestamp? startTime;
final Timestamp? endTime;
final double? hours;
final double? totalValue;
final VaidateDayStaffApplicationApplicationsShiftRoleRole role;
VaidateDayStaffApplicationApplicationsShiftRole.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
roleId = nativeFromJson<String>(json['roleId']),
count = nativeFromJson<int>(json['count']),
assigned = json['assigned'] == null ? null : nativeFromJson<int>(json['assigned']),
startTime = json['startTime'] == null ? null : Timestamp.fromJson(json['startTime']),
endTime = json['endTime'] == null ? null : Timestamp.fromJson(json['endTime']),
hours = json['hours'] == null ? null : nativeFromJson<double>(json['hours']),
totalValue = json['totalValue'] == null ? null : nativeFromJson<double>(json['totalValue']),
role = VaidateDayStaffApplicationApplicationsShiftRoleRole.fromJson(json['role']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplicationsShiftRole otherTyped = other as VaidateDayStaffApplicationApplicationsShiftRole;
return id == otherTyped.id &&
roleId == otherTyped.roleId &&
count == otherTyped.count &&
assigned == otherTyped.assigned &&
startTime == otherTyped.startTime &&
endTime == otherTyped.endTime &&
hours == otherTyped.hours &&
totalValue == otherTyped.totalValue &&
role == otherTyped.role;
}
@override
int get hashCode => Object.hashAll([id.hashCode, roleId.hashCode, count.hashCode, assigned.hashCode, startTime.hashCode, endTime.hashCode, hours.hashCode, totalValue.hashCode, role.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['roleId'] = nativeToJson<String>(roleId);
json['count'] = nativeToJson<int>(count);
if (assigned != null) {
json['assigned'] = nativeToJson<int?>(assigned);
}
if (startTime != null) {
json['startTime'] = startTime!.toJson();
}
if (endTime != null) {
json['endTime'] = endTime!.toJson();
}
if (hours != null) {
json['hours'] = nativeToJson<double?>(hours);
}
if (totalValue != null) {
json['totalValue'] = nativeToJson<double?>(totalValue);
}
json['role'] = role.toJson();
return json;
}
VaidateDayStaffApplicationApplicationsShiftRole({
required this.id,
required this.roleId,
required this.count,
this.assigned,
this.startTime,
this.endTime,
this.hours,
this.totalValue,
required this.role,
});
}
@immutable
class VaidateDayStaffApplicationApplicationsShiftRoleRole {
final String id;
final String name;
final double costPerHour;
VaidateDayStaffApplicationApplicationsShiftRoleRole.fromJson(dynamic json):
id = nativeFromJson<String>(json['id']),
name = nativeFromJson<String>(json['name']),
costPerHour = nativeFromJson<double>(json['costPerHour']);
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationApplicationsShiftRoleRole otherTyped = other as VaidateDayStaffApplicationApplicationsShiftRoleRole;
return id == otherTyped.id &&
name == otherTyped.name &&
costPerHour == otherTyped.costPerHour;
}
@override
int get hashCode => Object.hashAll([id.hashCode, name.hashCode, costPerHour.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['id'] = nativeToJson<String>(id);
json['name'] = nativeToJson<String>(name);
json['costPerHour'] = nativeToJson<double>(costPerHour);
return json;
}
VaidateDayStaffApplicationApplicationsShiftRoleRole({
required this.id,
required this.name,
required this.costPerHour,
});
}
@immutable
class VaidateDayStaffApplicationData {
final List<VaidateDayStaffApplicationApplications> applications;
VaidateDayStaffApplicationData.fromJson(dynamic json):
applications = (json['applications'] as List<dynamic>)
.map((e) => VaidateDayStaffApplicationApplications.fromJson(e))
.toList();
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationData otherTyped = other as VaidateDayStaffApplicationData;
return applications == otherTyped.applications;
}
@override
int get hashCode => applications.hashCode;
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['applications'] = applications.map((e) => e.toJson()).toList();
return json;
}
VaidateDayStaffApplicationData({
required this.applications,
});
}
@immutable
class VaidateDayStaffApplicationVariables {
final String staffId;
late final Optional<int>offset;
late final Optional<int>limit;
late final Optional<Timestamp>dayStart;
late final Optional<Timestamp>dayEnd;
@Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.')
VaidateDayStaffApplicationVariables.fromJson(Map<String, dynamic> json):
staffId = nativeFromJson<String>(json['staffId']) {
offset = Optional.optional(nativeFromJson, nativeToJson);
offset.value = json['offset'] == null ? null : nativeFromJson<int>(json['offset']);
limit = Optional.optional(nativeFromJson, nativeToJson);
limit.value = json['limit'] == null ? null : nativeFromJson<int>(json['limit']);
dayStart = Optional.optional((json) => json['dayStart'] = Timestamp.fromJson(json['dayStart']), defaultSerializer);
dayStart.value = json['dayStart'] == null ? null : Timestamp.fromJson(json['dayStart']);
dayEnd = Optional.optional((json) => json['dayEnd'] = Timestamp.fromJson(json['dayEnd']), defaultSerializer);
dayEnd.value = json['dayEnd'] == null ? null : Timestamp.fromJson(json['dayEnd']);
}
@override
bool operator ==(Object other) {
if(identical(this, other)) {
return true;
}
if(other.runtimeType != runtimeType) {
return false;
}
final VaidateDayStaffApplicationVariables otherTyped = other as VaidateDayStaffApplicationVariables;
return staffId == otherTyped.staffId &&
offset == otherTyped.offset &&
limit == otherTyped.limit &&
dayStart == otherTyped.dayStart &&
dayEnd == otherTyped.dayEnd;
}
@override
int get hashCode => Object.hashAll([staffId.hashCode, offset.hashCode, limit.hashCode, dayStart.hashCode, dayEnd.hashCode]);
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json['staffId'] = nativeToJson<String>(staffId);
if(offset.state == OptionalState.set) {
json['offset'] = offset.toJson();
}
if(limit.state == OptionalState.set) {
json['limit'] = limit.toJson();
}
if(dayStart.state == OptionalState.set) {
json['dayStart'] = dayStart.toJson();
}
if(dayEnd.state == OptionalState.set) {
json['dayEnd'] = dayEnd.toJson();
}
return json;
}
VaidateDayStaffApplicationVariables({
required this.staffId,
required this.offset,
required this.limit,
required this.dayStart,
required this.dayEnd,
});
}

View File

@@ -3,10 +3,12 @@ import 'package:krow_domain/krow_domain.dart' as domain;
class StaffSession {
final domain.User user;
final domain.Staff? staff;
final String? ownerId;
const StaffSession({
required this.user,
this.staff,
this.ownerId,
});
}

View File

@@ -63,6 +63,9 @@ class UiIcons {
/// Checkmark icon
static const IconData check = _IconLib.check;
/// Checkmark circle icon
static const IconData checkCircle = _IconLib.checkCircle;
/// X/Cancel icon
static const IconData close = _IconLib.x;

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'ui_colors.dart';
import 'ui_typography.dart';
import 'ui_constants.dart';
import 'ui_typography.dart';
/// The main entry point for the Staff Design System theme.
/// Assembles colors, typography, and constants into a comprehensive Material 3 theme.

View File

@@ -50,7 +50,7 @@ class UiButton extends StatelessWidget {
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : assert(
text != null || child != null,
@@ -67,7 +67,7 @@ class UiButton extends StatelessWidget {
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _elevatedButtonBuilder,
assert(
@@ -85,7 +85,7 @@ class UiButton extends StatelessWidget {
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _outlinedButtonBuilder,
assert(
@@ -103,7 +103,7 @@ class UiButton extends StatelessWidget {
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
@@ -121,7 +121,7 @@ class UiButton extends StatelessWidget {
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.medium,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
@@ -132,10 +132,14 @@ class UiButton extends StatelessWidget {
@override
/// Builds the button UI.
Widget build(BuildContext context) {
final ButtonStyle? mergedStyle = style != null
? _getSizeStyle().merge(style)
: _getSizeStyle();
final Widget button = buttonBuilder(
context,
onPressed,
style,
mergedStyle,
_buildButtonContent(),
);
@@ -146,6 +150,65 @@ class UiButton extends StatelessWidget {
return button;
}
/// Gets the style based on the button size.
ButtonStyle _getSizeStyle() {
switch (size) {
case UiButtonSize.extraSmall:
return ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1,
),
),
minimumSize: WidgetStateProperty.all(const Size(0, 28)),
maximumSize: WidgetStateProperty.all(const Size(double.infinity, 28)),
textStyle: WidgetStateProperty.all(
const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
);
case UiButtonSize.small:
return ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: UiConstants.space2,
),
),
minimumSize: WidgetStateProperty.all(const Size(0, 36)),
maximumSize: WidgetStateProperty.all(const Size(double.infinity, 36)),
textStyle: WidgetStateProperty.all(
const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
);
case UiButtonSize.medium:
return ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
),
minimumSize: WidgetStateProperty.all(const Size(0, 44)),
maximumSize: WidgetStateProperty.all(const Size(double.infinity, 44)),
);
case UiButtonSize.large:
return ButtonStyle(
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
vertical: UiConstants.space4,
),
),
minimumSize: WidgetStateProperty.all(const Size(0, 52)),
maximumSize: WidgetStateProperty.all(const Size(double.infinity, 52)),
textStyle: WidgetStateProperty.all(
const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
);
}
}
/// Builds the button content with optional leading and trailing icons.
Widget _buildButtonContent() {
if (child != null) {
@@ -229,6 +292,9 @@ class UiButton extends StatelessWidget {
/// Defines the size of a [UiButton].
enum UiButtonSize {
/// Extra small button (very compact)
extraSmall,
/// Small button (compact)
small,

View File

@@ -87,6 +87,11 @@ export 'src/adapters/clock_in/clock_in_adapter.dart';
export 'src/entities/availability/availability_slot.dart';
export 'src/entities/availability/day_availability.dart';
// Coverage
export 'src/entities/coverage_domain/coverage_shift.dart';
export 'src/entities/coverage_domain/coverage_worker.dart';
export 'src/entities/coverage_domain/coverage_stats.dart';
// Adapters
export 'src/adapters/profile/emergency_contact_adapter.dart';
export 'src/adapters/profile/experience_adapter.dart';

View File

@@ -1,10 +1,59 @@
import 'package:intl/intl.dart';
import '../../entities/shifts/shift.dart';
/// Adapter for Shift related data.
class ShiftAdapter {
// Note: Conversion logic will likely live in RepoImpl or here if we pass raw objects.
// Given we are dealing with generated types that aren't exported by domain,
// we might put the logic in Repo or make this accept dynamic/Map if strictly required.
// For now, placeholders or simple status helpers.
/// Maps application data to a Shift entity.
///
/// This method handles the common mapping logic used across different
/// repositories when converting application data from Data Connect to
/// domain Shift entities.
static Shift fromApplicationData({
required String shiftId,
required String roleId,
required String roleName,
required String businessName,
String? companyLogoUrl,
required double costPerHour,
String? shiftLocation,
required String teamHubName,
DateTime? shiftDate,
DateTime? startTime,
DateTime? endTime,
DateTime? createdAt,
required String status,
String? description,
int? durationDays,
required int count,
int? assigned,
String? eventName,
bool hasApplied = false,
}) {
final String orderName = (eventName ?? '').trim().isNotEmpty
? eventName!
: businessName;
final String title = '$roleName - $orderName';
return Shift(
id: shiftId,
roleId: roleId,
title: title,
clientName: businessName,
logoUrl: companyLogoUrl,
hourlyRate: costPerHour,
location: shiftLocation ?? '',
locationAddress: teamHubName,
date: shiftDate?.toIso8601String() ?? '',
startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '',
endTime: endTime != null ? DateFormat('HH:mm').format(endTime) : '',
createdDate: createdAt?.toIso8601String() ?? '',
status: status,
description: description,
durationDays: durationDays,
requiredSlots: count,
filledSlots: assigned ?? 0,
hasApplied: hasApplied,
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:equatable/equatable.dart';
import 'coverage_worker.dart';
/// Domain entity representing a shift in the coverage view.
///
/// This is a feature-specific domain entity that encapsulates shift information
/// including scheduling details and assigned workers.
class CoverageShift extends Equatable {
/// Creates a [CoverageShift].
const CoverageShift({
required this.id,
required this.title,
required this.location,
required this.startTime,
required this.workersNeeded,
required this.date,
required this.workers,
});
/// The unique identifier for the shift.
final String id;
/// The title or role of the shift.
final String title;
/// The location where the shift takes place.
final String location;
/// The start time of the shift (e.g., "16:00").
final String startTime;
/// The number of workers needed for this shift.
final int workersNeeded;
/// The date of the shift.
final DateTime date;
/// The list of workers assigned to this shift.
final List<CoverageWorker> workers;
/// Calculates the coverage percentage for this shift.
int get coveragePercent {
if (workersNeeded == 0) return 0;
return ((workers.length / workersNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
id,
title,
location,
startTime,
workersNeeded,
date,
workers,
];
}

View File

@@ -0,0 +1,45 @@
import 'package:equatable/equatable.dart';
/// Domain entity representing coverage statistics.
///
/// Aggregates coverage metrics for a specific date.
class CoverageStats extends Equatable {
/// Creates a [CoverageStats].
const CoverageStats({
required this.totalNeeded,
required this.totalConfirmed,
required this.checkedIn,
required this.enRoute,
required this.late,
});
/// The total number of workers needed.
final int totalNeeded;
/// The total number of confirmed workers.
final int totalConfirmed;
/// The number of workers who have checked in.
final int checkedIn;
/// The number of workers en route.
final int enRoute;
/// The number of late workers.
final int late;
/// Calculates the overall coverage percentage.
int get coveragePercent {
if (totalNeeded == 0) return 0;
return ((totalConfirmed / totalNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
totalNeeded,
totalConfirmed,
checkedIn,
enRoute,
late,
];
}

View File

@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
/// Worker status enum matching ApplicationStatus from Data Connect.
enum CoverageWorkerStatus {
/// Application is pending approval.
pending,
/// Application has been accepted.
accepted,
/// Application has been rejected.
rejected,
/// Worker has confirmed attendance.
confirmed,
/// Worker has checked in.
checkedIn,
/// Worker has checked out.
checkedOut,
/// Worker is late.
late,
/// Worker did not show up.
noShow,
/// Shift is completed.
completed,
}
/// Domain entity representing a worker in the coverage view.
///
/// This entity tracks worker status including check-in information.
class CoverageWorker extends Equatable {
/// Creates a [CoverageWorker].
const CoverageWorker({
required this.name,
required this.status,
this.checkInTime,
});
/// The name of the worker.
final String name;
/// The status of the worker.
final CoverageWorkerStatus status;
/// The time the worker checked in, if applicable.
final String? checkInTime;
@override
List<Object?> get props => <Object?>[name, status, checkInTime];
}

View File

@@ -26,6 +26,9 @@ class Shift extends Equatable {
final int? durationDays; // For multi-day shifts
final int? requiredSlots;
final int? filledSlots;
final String? roleId;
final bool? hasApplied;
final double? totalValue;
const Shift({
required this.id,
@@ -53,6 +56,9 @@ class Shift extends Equatable {
this.durationDays,
this.requiredSlots,
this.filledSlots,
this.roleId,
this.hasApplied,
this.totalValue,
});
@override
@@ -82,6 +88,9 @@ class Shift extends Equatable {
durationDays,
requiredSlots,
filledSlots,
roleId,
hasApplied,
totalValue,
];
}

View File

@@ -200,6 +200,8 @@ class _BillingViewState extends State<BillingView> {
const SpendingBreakdownCard(),
if (state.invoiceHistory.isEmpty) _buildEmptyState(context)
else InvoiceHistorySection(invoices: state.invoiceHistory),
const SizedBox(height: UiConstants.space32),
],
),
);

View File

@@ -1,7 +1,7 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/coverage_repository.dart';
import '../../domain/ui_entities/coverage_entities.dart';
/// Implementation of [CoverageRepository] in the Data layer.
///
@@ -25,18 +25,14 @@ class CoverageRepositoryImpl implements CoverageRepository {
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
final String? businessId =
dc.ClientSessionStore.instance.session?.business?.id;
print('Coverage: now=${DateTime.now().toIso8601String()}');
if (businessId == null || businessId.isEmpty) {
print('Coverage: missing businessId for date=${date.toIso8601String()}');
return <CoverageShift>[];
}
final DateTime start = DateTime(date.year, date.month, date.day);
final DateTime end =
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
print(
'Coverage: request businessId=$businessId dayStart=${start.toIso8601String()} dayEnd=${end.toIso8601String()}',
);
final fdc.QueryResult<
dc.ListShiftRolesByBusinessAndDateRangeData,
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
@@ -58,9 +54,6 @@ class CoverageRepositoryImpl implements CoverageRepository {
dayEnd: _toTimestamp(end),
)
.execute();
print(
'Coverage: ${date.toIso8601String()} staffsApplications=${applicationsResult.data.applications.length}',
);
return _mapCoverageShifts(
shiftRolesResult.data.shiftRoles,
@@ -84,11 +77,16 @@ class CoverageRepositoryImpl implements CoverageRepository {
final List<CoverageWorker> allWorkers =
shifts.expand((CoverageShift shift) => shift.workers).toList();
final int totalConfirmed = allWorkers.length;
final int checkedIn =
allWorkers.where((CoverageWorker w) => w.isCheckedIn).length;
final int enRoute =
allWorkers.where((CoverageWorker w) => w.isEnRoute).length;
final int late = allWorkers.where((CoverageWorker w) => w.isLate).length;
final int checkedIn = allWorkers
.where((CoverageWorker w) => w.status == CoverageWorkerStatus.checkedIn)
.length;
final int enRoute = allWorkers
.where((CoverageWorker w) =>
w.status == CoverageWorkerStatus.confirmed && w.checkInTime == null)
.length;
final int late = allWorkers
.where((CoverageWorker w) => w.status == CoverageWorkerStatus.late)
.length;
return CoverageStats(
totalNeeded: totalNeeded,
@@ -172,24 +170,32 @@ class CoverageRepositoryImpl implements CoverageRepository {
.toList();
}
String _mapWorkerStatus(
CoverageWorkerStatus _mapWorkerStatus(
dc.EnumValue<dc.ApplicationStatus> status,
) {
if (status is dc.Known<dc.ApplicationStatus>) {
switch (status.value) {
case dc.ApplicationStatus.LATE:
return 'late';
case dc.ApplicationStatus.CHECKED_IN:
case dc.ApplicationStatus.CHECKED_OUT:
case dc.ApplicationStatus.ACCEPTED:
case dc.ApplicationStatus.CONFIRMED:
case dc.ApplicationStatus.PENDING:
return CoverageWorkerStatus.pending;
case dc.ApplicationStatus.ACCEPTED:
return CoverageWorkerStatus.confirmed;
case dc.ApplicationStatus.REJECTED:
return CoverageWorkerStatus.rejected;
case dc.ApplicationStatus.CONFIRMED:
return CoverageWorkerStatus.confirmed;
case dc.ApplicationStatus.CHECKED_IN:
return CoverageWorkerStatus.checkedIn;
case dc.ApplicationStatus.CHECKED_OUT:
return CoverageWorkerStatus.checkedOut;
case dc.ApplicationStatus.LATE:
return CoverageWorkerStatus.late;
case dc.ApplicationStatus.NO_SHOW:
return 'confirmed';
return CoverageWorkerStatus.noShow;
case dc.ApplicationStatus.COMPLETED:
return CoverageWorkerStatus.completed;
}
}
return 'confirmed';
return CoverageWorkerStatus.pending;
}
String? _formatTime(fdc.Timestamp? timestamp) {

View File

@@ -1,4 +1,4 @@
import '../ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for coverage-related operations.
///

View File

@@ -1,133 +0,0 @@
import 'package:equatable/equatable.dart';
/// Domain entity representing a shift in the coverage view.
///
/// This is a feature-specific domain entity that encapsulates shift information
/// including scheduling details and assigned workers.
class CoverageShift extends Equatable {
/// Creates a [CoverageShift].
const CoverageShift({
required this.id,
required this.title,
required this.location,
required this.startTime,
required this.workersNeeded,
required this.date,
required this.workers,
});
/// The unique identifier for the shift.
final String id;
/// The title or role of the shift.
final String title;
/// The location where the shift takes place.
final String location;
/// The start time of the shift (e.g., "16:00").
final String startTime;
/// The number of workers needed for this shift.
final int workersNeeded;
/// The date of the shift.
final DateTime date;
/// The list of workers assigned to this shift.
final List<CoverageWorker> workers;
/// Calculates the coverage percentage for this shift.
int get coveragePercent {
if (workersNeeded == 0) return 0;
return ((workers.length / workersNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
id,
title,
location,
startTime,
workersNeeded,
date,
workers,
];
}
/// Domain entity representing a worker in the coverage view.
///
/// This entity tracks worker status including check-in information.
class CoverageWorker extends Equatable {
/// Creates a [CoverageWorker].
const CoverageWorker({
required this.name,
required this.status,
this.checkInTime,
});
/// The name of the worker.
final String name;
/// The status of the worker ('confirmed', 'late', etc.).
final String status;
/// The time the worker checked in, if applicable.
final String? checkInTime;
/// Returns true if the worker is checked in.
bool get isCheckedIn => status == 'confirmed' && checkInTime != null;
/// Returns true if the worker is en route.
bool get isEnRoute => status == 'confirmed' && checkInTime == null;
/// Returns true if the worker is late.
bool get isLate => status == 'late';
@override
List<Object?> get props => <Object?>[name, status, checkInTime];
}
/// Domain entity representing coverage statistics.
///
/// Aggregates coverage metrics for a specific date.
class CoverageStats extends Equatable {
/// Creates a [CoverageStats].
const CoverageStats({
required this.totalNeeded,
required this.totalConfirmed,
required this.checkedIn,
required this.enRoute,
required this.late,
});
/// The total number of workers needed.
final int totalNeeded;
/// The total number of confirmed workers.
final int totalConfirmed;
/// The number of workers who have checked in.
final int checkedIn;
/// The number of workers en route.
final int enRoute;
/// The number of late workers.
final int late;
/// Calculates the overall coverage percentage.
int get coveragePercent {
if (totalNeeded == 0) return 0;
return ((totalConfirmed / totalNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
totalNeeded,
totalConfirmed,
checkedIn,
enRoute,
late,
];
}

View File

@@ -1,7 +1,8 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_coverage_stats_arguments.dart';
import '../repositories/coverage_repository.dart';
import '../ui_entities/coverage_entities.dart';
/// Use case for fetching coverage statistics for a specific date.
///

View File

@@ -1,7 +1,7 @@
import 'package:krow_core/core.dart';
import '../arguments/get_shifts_for_date_arguments.dart';
import '../repositories/coverage_repository.dart';
import '../ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Use case for fetching shifts for a specific date.
///

View File

@@ -1,7 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/arguments/get_coverage_stats_arguments.dart';
import '../../domain/arguments/get_shifts_for_date_arguments.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_coverage_stats_usecase.dart';
import '../../domain/usecases/get_shifts_for_date_usecase.dart';
import 'coverage_event.dart';

View File

@@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Enum representing the status of coverage data loading.
enum CoverageStatus {

View File

@@ -1,6 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Quick statistics cards showing coverage metrics.
///

View File

@@ -1,7 +1,7 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// List of shifts with their workers.
///
@@ -194,7 +194,8 @@ class _ShiftHeader extends StatelessWidget {
size: UiConstants.space3,
color: UiColors.iconSecondary,
),
Expanded(child: Text(
Expanded(
child: Text(
location,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
@@ -314,36 +315,92 @@ class _WorkerRow extends StatelessWidget {
Color badgeText;
String badgeLabel;
if (worker.isCheckedIn) {
bg = UiColors.textSuccess.withOpacity(0.1);
border = UiColors.textSuccess;
textBg = UiColors.textSuccess.withOpacity(0.2);
textColor = UiColors.textSuccess;
icon = UiIcons.success;
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
badgeBg = UiColors.textSuccess;
badgeText = UiColors.primaryForeground;
badgeLabel = 'On Site';
} else if (worker.isEnRoute) {
bg = UiColors.textWarning.withOpacity(0.1);
border = UiColors.textWarning;
textBg = UiColors.textWarning.withOpacity(0.2);
textColor = UiColors.textWarning;
icon = UiIcons.clock;
statusText = 'En Route - Expected $shiftStartTime';
badgeBg = UiColors.textWarning;
badgeText = UiColors.primaryForeground;
badgeLabel = 'En Route';
} else {
bg = UiColors.destructive.withOpacity(0.1);
border = UiColors.destructive;
textBg = UiColors.destructive.withOpacity(0.2);
textColor = UiColors.destructive;
icon = UiIcons.warning;
statusText = '⚠ Running Late';
badgeBg = UiColors.destructive;
badgeText = UiColors.destructiveForeground;
badgeLabel = 'Late';
switch (worker.status) {
case CoverageWorkerStatus.checkedIn:
bg = UiColors.textSuccess.withOpacity(0.1);
border = UiColors.textSuccess;
textBg = UiColors.textSuccess.withOpacity(0.2);
textColor = UiColors.textSuccess;
icon = UiIcons.success;
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
badgeBg = UiColors.textSuccess;
badgeText = UiColors.primaryForeground;
badgeLabel = 'On Site';
case CoverageWorkerStatus.confirmed:
if (worker.checkInTime == null) {
bg = UiColors.textWarning.withOpacity(0.1);
border = UiColors.textWarning;
textBg = UiColors.textWarning.withOpacity(0.2);
textColor = UiColors.textWarning;
icon = UiIcons.clock;
statusText = 'En Route - Expected $shiftStartTime';
badgeBg = UiColors.textWarning;
badgeText = UiColors.primaryForeground;
badgeLabel = 'En Route';
} else {
bg = UiColors.muted.withOpacity(0.1);
border = UiColors.border;
textBg = UiColors.muted.withOpacity(0.2);
textColor = UiColors.textSecondary;
icon = UiIcons.success;
statusText = 'Confirmed';
badgeBg = UiColors.muted;
badgeText = UiColors.textPrimary;
badgeLabel = 'Confirmed';
}
case CoverageWorkerStatus.late:
bg = UiColors.destructive.withOpacity(0.1);
border = UiColors.destructive;
textBg = UiColors.destructive.withOpacity(0.2);
textColor = UiColors.destructive;
icon = UiIcons.warning;
statusText = '⚠ Running Late';
badgeBg = UiColors.destructive;
badgeText = UiColors.destructiveForeground;
badgeLabel = 'Late';
case CoverageWorkerStatus.checkedOut:
bg = UiColors.muted.withOpacity(0.1);
border = UiColors.border;
textBg = UiColors.muted.withOpacity(0.2);
textColor = UiColors.textSecondary;
icon = UiIcons.success;
statusText = 'Checked Out';
badgeBg = UiColors.muted;
badgeText = UiColors.textPrimary;
badgeLabel = 'Done';
case CoverageWorkerStatus.noShow:
bg = UiColors.destructive.withOpacity(0.1);
border = UiColors.destructive;
textBg = UiColors.destructive.withOpacity(0.2);
textColor = UiColors.destructive;
icon = UiIcons.warning;
statusText = 'No Show';
badgeBg = UiColors.destructive;
badgeText = UiColors.destructiveForeground;
badgeLabel = 'No Show';
case CoverageWorkerStatus.completed:
bg = UiColors.textSuccess.withOpacity(0.1);
border = UiColors.textSuccess;
textBg = UiColors.textSuccess.withOpacity(0.2);
textColor = UiColors.textSuccess;
icon = UiIcons.success;
statusText = 'Completed';
badgeBg = UiColors.textSuccess;
badgeText = UiColors.primaryForeground;
badgeLabel = 'Completed';
case CoverageWorkerStatus.pending:
case CoverageWorkerStatus.accepted:
case CoverageWorkerStatus.rejected:
bg = UiColors.muted.withOpacity(0.1);
border = UiColors.border;
textBg = UiColors.muted.withOpacity(0.2);
textColor = UiColors.textSecondary;
icon = UiIcons.clock;
statusText = worker.status.name.toUpperCase();
badgeBg = UiColors.muted;
badgeText = UiColors.textPrimary;
badgeLabel = worker.status.name[0].toUpperCase() +
worker.status.name.substring(1);
}
return Container(

View File

@@ -43,14 +43,11 @@ class ReorderWidget extends StatelessWidget {
),
if (subtitle != null) ...<Widget>[
const SizedBox(height: UiConstants.space1),
Text(
subtitle!,
style: UiTypography.body2r.textSecondary,
),
Text(subtitle!, style: UiTypography.body2r.textSecondary),
],
const SizedBox(height: UiConstants.space2),
SizedBox(
height: 140,
height: 164,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: recentOrders.length,
@@ -67,13 +64,7 @@ class ReorderWidget extends StatelessWidget {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.02),
blurRadius: 4,
),
],
border: Border.all(color: UiColors.border, width: 0.6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -129,10 +120,7 @@ class ReorderWidget extends StatelessWidget {
style: UiTypography.body1b,
),
Text(
i18n.per_hr(
amount: order.hourlyRate.toString(),
) +
' · ${order.hours}h',
'${i18n.per_hr(amount: order.hourlyRate.toString())} · ${order.hours}h',
style: UiTypography.footnote2r.textSecondary,
),
],
@@ -145,49 +133,37 @@ class ReorderWidget extends StatelessWidget {
_Badge(
icon: UiIcons.success,
text: order.type,
color: const Color(0xFF2563EB),
bg: const Color(0xFF2563EB),
textColor: UiColors.white,
color: UiColors.primary,
bg: UiColors.buttonSecondaryStill,
textColor: UiColors.primary,
),
const SizedBox(width: UiConstants.space2),
_Badge(
icon: UiIcons.building,
text: '${order.workers}',
color: const Color(0xFF334155),
bg: const Color(0xFFF1F5F9),
textColor: const Color(0xFF334155),
color: UiColors.textSecondary,
bg: UiColors.buttonSecondaryStill,
textColor: UiColors.textSecondary,
),
],
),
const Spacer(),
SizedBox(
height: 28,
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => onReorderPressed(<String, dynamic>{
'orderId': order.orderId,
'title': order.title,
'location': order.location,
'hourlyRate': order.hourlyRate,
'hours': order.hours,
'workers': order.workers,
'type': order.type,
}),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: UiColors.white,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
elevation: 0,
),
icon: const Icon(UiIcons.zap, size: 12),
label: Text(
i18n.reorder_button,
style: UiTypography.footnote1m,
),
),
UiButton.secondary(
size: UiButtonSize.small,
text: i18n.reorder_button,
leadingIcon: UiIcons.zap,
iconSize: 12,
fullWidth: true,
onPressed: () => onReorderPressed(<String, dynamic>{
'orderId': order.orderId,
'title': order.title,
'location': order.location,
'hourlyRate': order.hourlyRate,
'hours': order.hours,
'workers': order.workers,
'type': order.type,
}),
),
],
),

View File

@@ -35,7 +35,7 @@ class SettingsProfileHeader extends StatelessWidget {
flexibleSpace: FlexibleSpaceBar(
background: Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
margin: const EdgeInsets.only(top: UiConstants.space16),
margin: const EdgeInsets.only(top: UiConstants.space24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,

View File

@@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -10,6 +9,7 @@ import '../blocs/view_orders_cubit.dart';
import '../blocs/view_orders_state.dart';
import 'package:krow_domain/krow_domain.dart';
import '../widgets/view_order_card.dart';
import '../widgets/view_orders_header.dart';
import '../navigation/view_orders_navigator.dart';
/// The main page for viewing client orders.
@@ -22,6 +22,7 @@ class ViewOrdersPage extends StatelessWidget {
/// Creates a [ViewOrdersPage].
const ViewOrdersPage({super.key, this.initialDate});
/// The initial date to display orders for.
final DateTime? initialDate;
@override
@@ -37,7 +38,8 @@ class ViewOrdersPage extends StatelessWidget {
class ViewOrdersView extends StatefulWidget {
/// Creates a [ViewOrdersView].
const ViewOrdersView({super.key, this.initialDate});
/// The initial date to display orders for.
final DateTime? initialDate;
@override
@@ -88,376 +90,84 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
}
return Scaffold(
body: Stack(
children: <Widget>[
// Background Gradient
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[UiColors.bgSecondary, UiColors.white],
stops: <double>[0.0, 0.3],
),
body: SafeArea(
child: Column(
children: <Widget>[
// Header + Filter + Calendar (Sticky behavior)
ViewOrdersHeader(
state: state,
calendarDays: calendarDays,
),
),
SafeArea(
child: Column(
children: <Widget>[
// Header + Filter + Calendar (Sticky behavior)
_buildHeader(
context: context,
state: state,
calendarDays: calendarDays,
),
// Content List
Expanded(
child: filteredOrders.isEmpty
? _buildEmptyState(context: context, state: state)
: ListView(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space4,
UiConstants.space5,
100,
),
children: <Widget>[
if (filteredOrders.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: Row(
children: <Widget>[
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: dotColor,
shape: BoxShape.circle,
),
),
const SizedBox(
width: UiConstants.space2,
),
Text(
sectionTitle.toUpperCase(),
style: UiTypography.titleUppercase2m
.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(
width: UiConstants.space1,
),
Text(
'(${filteredOrders.length})',
style: UiTypography.footnote1r
.copyWith(
color: UiColors.textSecondary,
),
),
],
),
),
...filteredOrders.map(
(OrderItem order) => Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: ViewOrderCard(order: order),
),
// Content List
Expanded(
child: filteredOrders.isEmpty
? _buildEmptyState(context: context, state: state)
: ListView(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space4,
UiConstants.space5,
100,
),
children: <Widget>[
if (filteredOrders.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
],
child: Row(
children: <Widget>[
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: dotColor,
shape: BoxShape.circle,
),
),
const SizedBox(
width: UiConstants.space2,
),
Text(
sectionTitle.toUpperCase(),
style: UiTypography.titleUppercase2m
.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(
width: UiConstants.space1,
),
Text(
'(${filteredOrders.length})',
style: UiTypography.footnote1r
.copyWith(
color: UiColors.textSecondary,
),
),
],
),
),
...filteredOrders.map(
(OrderItem order) => Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: ViewOrderCard(order: order),
),
),
),
],
],
),
),
),
],
],
),
),
);
},
);
}
/// Builds the sticky header section.
Widget _buildHeader({
required BuildContext context,
required ViewOrdersState state,
required List<DateTime> calendarDays,
}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: const BoxDecoration(
color: Color(0xCCFFFFFF), // White with 0.8 alpha
border: Border(
bottom: BorderSide(color: UiColors.separatorSecondary),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// Top Bar
Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space5,
UiConstants.space5,
UiConstants.space3,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
t.client_view_orders.title,
style: UiTypography.headline3m.copyWith(
color: UiColors.textPrimary,
fontWeight: FontWeight.bold,
),
),
if (state.filteredOrders.isNotEmpty)
UiButton.primary(
text: t.client_view_orders.post_button,
leadingIcon: UiIcons.add,
onPressed: () => Modular.to.navigateToCreateOrder(),
size: UiButtonSize.small,
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
maximumSize: const Size(0, 48),
),
),
],
),
),
// Filter Tabs
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_buildFilterTab(
context,
label: t.client_view_orders.tabs.up_next,
isSelected: state.filterTab == 'all',
tabId: 'all',
count: state.upNextCount,
),
const SizedBox(width: UiConstants.space6),
_buildFilterTab(
context,
label: t.client_view_orders.tabs.active,
isSelected: state.filterTab == 'active',
tabId: 'active',
count: state.activeCount,
),
const SizedBox(width: UiConstants.space6),
_buildFilterTab(
context,
label: t.client_view_orders.tabs.completed,
isSelected: state.filterTab == 'completed',
tabId: 'completed',
count: state.completedCount,
),
],
),
),
// Calendar Header controls
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: const Icon(
UiIcons.chevronLeft,
size: 20,
color: UiColors.iconSecondary,
),
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
context,
).updateWeekOffset(-1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
Text(
DateFormat('MMMM yyyy').format(calendarDays.first),
style: UiTypography.body2m.copyWith(
color: UiColors.textSecondary,
),
),
IconButton(
icon: const Icon(
UiIcons.chevronRight,
size: 20,
color: UiColors.iconSecondary,
),
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
context,
).updateWeekOffset(1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
),
),
// Calendar Grid
SizedBox(
height: 72,
child: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
scrollDirection: Axis.horizontal,
itemCount: 7,
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(width: UiConstants.space2),
itemBuilder: (BuildContext context, int index) {
final DateTime date = calendarDays[index];
final bool isSelected =
state.selectedDate != null &&
date.year == state.selectedDate!.year &&
date.month == state.selectedDate!.month &&
date.day == state.selectedDate!.day;
// Check if this date has any shifts
final String dateStr = DateFormat(
'yyyy-MM-dd',
).format(date);
final bool hasShifts = state.orders.any(
(OrderItem s) => s.date == dateStr,
);
return GestureDetector(
onTap: () => BlocProvider.of<ViewOrdersCubit>(
context,
).selectDate(date),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 48,
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? UiColors.primary
: UiColors.separatorPrimary,
),
boxShadow: isSelected
? <BoxShadow>[
BoxShadow(
color: UiColors.primary.withValues(
alpha: 0.25,
),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
DateFormat('dd').format(date),
style: UiTypography.title2b.copyWith(
fontSize: 18,
color: isSelected
? UiColors.white
: UiColors.textPrimary,
),
),
Text(
DateFormat('E').format(date),
style: UiTypography.footnote2m.copyWith(
color: isSelected
? UiColors.white.withValues(alpha: 0.8)
: UiColors.textSecondary,
),
),
if (hasShifts) ...<Widget>[
const SizedBox(height: UiConstants.space1),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: isSelected
? UiColors.white
: UiColors.primary,
shape: BoxShape.circle,
),
),
],
],
),
),
);
},
),
),
const SizedBox(height: UiConstants.space4),
],
),
),
),
);
}
/// Builds a single filter tab.
Widget _buildFilterTab(
BuildContext context, {
required String label,
required bool isSelected,
required String tabId,
int? count,
}) {
String text = label;
if (count != null) {
text = '$label ($count)';
}
return GestureDetector(
onTap: () =>
BlocProvider.of<ViewOrdersCubit>(context).selectFilterTab(tabId),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Text(
text,
style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.primary : UiColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 2,
width: isSelected ? 40 : 0,
decoration: BoxDecoration(
color: UiColors.primary,
borderRadius: BorderRadius.circular(2),
),
),
if (!isSelected) const SizedBox(height: 2),
],
),
);
}
/// Builds the empty state view.
Widget _buildEmptyState({
required BuildContext context,

View File

@@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import 'package:url_launcher/url_launcher.dart';
import '../blocs/view_orders_cubit.dart';
/// A rich card displaying details of a client order/shift.
@@ -325,15 +326,23 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
children: <Widget>[
Row(
children: <Widget>[
if (order.workersNeeded != 0)
if (coveragePercent != 100)
const Icon(
UiIcons.error,
size: 16,
color: UiColors.textError,
),
if (coveragePercent == 100)
const Icon(
UiIcons.checkCircle,
size: 16,
color: UiColors.textSuccess,
),
const SizedBox(width: 8),
Text(
'${order.workersNeeded} Workers Needed',
coveragePercent == 100
? 'All Workers Confirmed'
: '${order.workersNeeded} Workers Needed',
style: UiTypography.body2m.textPrimary,
),
],

View File

@@ -0,0 +1,68 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/view_orders_cubit.dart';
/// A single filter tab for the View Orders page.
///
/// Displays a label with an optional count and shows a selection indicator
/// when the tab is active.
class ViewOrdersFilterTab extends StatelessWidget {
/// Creates a [ViewOrdersFilterTab].
const ViewOrdersFilterTab({
required this.label,
required this.isSelected,
required this.tabId,
this.count,
super.key,
});
/// The label text to display.
final String label;
/// Whether this tab is currently selected.
final bool isSelected;
/// The unique identifier for this tab.
final String tabId;
/// Optional count to display next to the label.
final int? count;
@override
Widget build(BuildContext context) {
String text = label;
if (count != null) {
text = '$label ($count)';
}
return GestureDetector(
onTap: () =>
BlocProvider.of<ViewOrdersCubit>(context).selectFilterTab(tabId),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Text(
text,
style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.primary : UiColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 2,
width: isSelected ? 40 : 0,
decoration: BoxDecoration(
color: UiColors.primary,
borderRadius: BorderRadius.circular(2),
),
),
if (!isSelected) const SizedBox(height: 2),
],
),
);
}
}

View File

@@ -0,0 +1,268 @@
import 'dart:ui';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:core_localization/core_localization.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/view_orders_cubit.dart';
import '../blocs/view_orders_state.dart';
import '../navigation/view_orders_navigator.dart';
import 'view_orders_filter_tab.dart';
/// The sticky header section for the View Orders page.
///
/// This widget contains:
/// - Top bar with title and post button
/// - Filter tabs (Up Next, Active, Completed)
/// - Calendar navigation controls
/// - Horizontal calendar grid
class ViewOrdersHeader extends StatelessWidget {
/// Creates a [ViewOrdersHeader].
const ViewOrdersHeader({
required this.state,
required this.calendarDays,
super.key,
});
/// The current state of the view orders feature.
final ViewOrdersState state;
/// The list of calendar days to display.
final List<DateTime> calendarDays;
@override
Widget build(BuildContext context) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: const BoxDecoration(
color: Color(0xCCFFFFFF), // White with 0.8 alpha
border: Border(
bottom: BorderSide(color: UiColors.separatorSecondary),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// Top Bar
Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
UiConstants.space5,
UiConstants.space5,
UiConstants.space3,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
t.client_view_orders.title,
style: UiTypography.headline3m.copyWith(
color: UiColors.textPrimary,
fontWeight: FontWeight.bold,
),
),
if (state.filteredOrders.isNotEmpty)
UiButton.primary(
text: t.client_view_orders.post_button,
leadingIcon: UiIcons.add,
onPressed: () => Modular.to.navigateToCreateOrder(),
size: UiButtonSize.small,
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
maximumSize: const Size(0, 48),
),
),
],
),
),
// Filter Tabs
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ViewOrdersFilterTab(
label: t.client_view_orders.tabs.up_next,
isSelected: state.filterTab == 'all',
tabId: 'all',
count: state.upNextCount,
),
const SizedBox(width: UiConstants.space6),
ViewOrdersFilterTab(
label: t.client_view_orders.tabs.active,
isSelected: state.filterTab == 'active',
tabId: 'active',
count: state.activeCount,
),
const SizedBox(width: UiConstants.space6),
ViewOrdersFilterTab(
label: t.client_view_orders.tabs.completed,
isSelected: state.filterTab == 'completed',
tabId: 'completed',
count: state.completedCount,
),
],
),
),
// Calendar Header controls
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: const Icon(
UiIcons.chevronLeft,
size: 20,
color: UiColors.iconSecondary,
),
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
context,
).updateWeekOffset(-1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
Text(
DateFormat('MMMM yyyy').format(calendarDays.first),
style: UiTypography.body2m.copyWith(
color: UiColors.textSecondary,
),
),
IconButton(
icon: const Icon(
UiIcons.chevronRight,
size: 20,
color: UiColors.iconSecondary,
),
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
context,
).updateWeekOffset(1),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
),
),
// Calendar Grid
SizedBox(
height: 72,
child: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
scrollDirection: Axis.horizontal,
itemCount: 7,
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(width: UiConstants.space2),
itemBuilder: (BuildContext context, int index) {
final DateTime date = calendarDays[index];
final bool isSelected =
state.selectedDate != null &&
date.year == state.selectedDate!.year &&
date.month == state.selectedDate!.month &&
date.day == state.selectedDate!.day;
// Check if this date has any shifts
final String dateStr = DateFormat(
'yyyy-MM-dd',
).format(date);
final bool hasShifts = state.orders.any(
(OrderItem s) => s.date == dateStr,
);
// Check if date is in the past
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime checkDate = DateTime(date.year, date.month, date.day);
final bool isPast = checkDate.isBefore(today);
return Opacity(
opacity: isPast && !isSelected ? 0.5 : 1.0,
child: GestureDetector(
onTap: () => BlocProvider.of<ViewOrdersCubit>(
context,
).selectDate(date),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 48,
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? UiColors.primary
: UiColors.separatorPrimary,
),
boxShadow: isSelected
? <BoxShadow>[
BoxShadow(
color: UiColors.primary.withValues(
alpha: 0.25,
),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
DateFormat('dd').format(date),
style: UiTypography.title2b.copyWith(
fontSize: 18,
color: isSelected
? UiColors.white
: UiColors.textPrimary,
),
),
Text(
DateFormat('E').format(date),
style: UiTypography.footnote2m.copyWith(
color: isSelected
? UiColors.white.withValues(alpha: 0.8)
: UiColors.textSecondary,
),
),
if (hasShifts) ...<Widget>[
const SizedBox(height: UiConstants.space1),
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: isSelected
? UiColors.white
: UiColors.primary,
shape: BoxShape.circle,
),
),
],
],
),
),
),
);
},
),
),
const SizedBox(height: UiConstants.space4),
],
),
),
),
);
}
}

View File

@@ -25,10 +25,14 @@ dependencies:
path: ../../../domain
krow_core:
path: ../../../core
krow_data_connect:
path: ../../../data_connect
# UI
lucide_icons: ^0.257.0
intl: ^0.20.1
url_launcher: ^6.3.1
firebase_data_connect: ^0.2.2+2
firebase_auth: ^6.1.4
dev_dependencies:
flutter_test:

View File

@@ -4,6 +4,7 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../utils/test_phone_numbers.dart';
import '../../domain/ui_entities/auth_mode.dart';
import '../../domain/repositories/auth_repository_interface.dart';
@@ -41,7 +42,17 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
await firebaseAuth.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (_) {
verificationCompleted: (PhoneAuthCredential credential) {
// Skip auto-verification for test numbers to allow manual code entry
if (TestPhoneNumbers.isTestNumber(phoneNumber)) {
return;
}
// For real numbers, we can support auto-verification if desired.
// But since this method returns a verificationId for manual OTP entry,
// we might not handle direct sign-in here unless the architecture changes.
// Currently, we just ignore it for the completer flow,
// or we could sign in directly if the credential is provided.
},
verificationFailed: (FirebaseAuthException e) {
if (!completer.isCompleted) {
@@ -168,7 +179,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
avatar: staffRecord.photoUrl,
);
StaffSessionStore.instance.setSession(
StaffSession(user: domainUser, staff: domainStaff),
StaffSession(
user: domainUser,
staff: domainStaff,
ownerId: staffRecord?.ownerId,
),
);
return domainUser;
}

View File

@@ -60,7 +60,7 @@ class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
if (session != null) {
StaffSessionStore.instance.setSession(
StaffSession(user: session.user, staff: staff),
StaffSession(user: session.user, staff: staff, ownerId: session.ownerId),
);
}
}

View File

@@ -1,15 +1,15 @@
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:design_system/design_system.dart';
import 'package:staff_authentication/src/domain/ui_entities/auth_mode.dart';
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
import 'package:staff_authentication/src/presentation/blocs/auth_event.dart';
import 'package:staff_authentication/src/presentation/blocs/auth_state.dart';
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
import '../widgets/phone_verification_page/phone_input.dart';
import '../widgets/phone_verification_page/otp_verification.dart';
import 'package:staff_authentication/staff_authentication.dart';
import '../navigation/auth_navigator.dart'; // Import the extension
import '../widgets/phone_verification_page/otp_verification.dart';
import '../widgets/phone_verification_page/phone_input.dart';
/// A combined page for phone number entry and OTP verification.
///

View File

@@ -7,7 +7,6 @@ class GetStartedBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
children: [
const SizedBox(height: 32),

View File

@@ -9,15 +9,29 @@ import 'phone_input/phone_input_form_field.dart';
import 'phone_input/phone_input_header.dart';
/// A widget that displays the phone number entry UI.
class PhoneInput extends StatelessWidget {
class PhoneInput extends StatefulWidget {
/// Creates a [PhoneInput].
const PhoneInput({super.key, required this.state, required this.onSendCode});
/// The current state of the authentication process.
final AuthState state;
/// Callback for when the "Send Code" action is triggered.
final VoidCallback onSendCode;
/// Creates a [PhoneInput].
const PhoneInput({super.key, required this.state, required this.onSendCode});
@override
State<PhoneInput> createState() => _PhoneInputState();
}
class _PhoneInputState extends State<PhoneInput> {
void _handlePhoneChanged(String value) {
if (!mounted) return;
final AuthBloc bloc = context.read<AuthBloc>();
if (!bloc.isClosed) {
bloc.add(AuthPhoneUpdated(value));
}
}
@override
Widget build(BuildContext context) {
@@ -35,19 +49,18 @@ class PhoneInput extends StatelessWidget {
const PhoneInputHeader(),
const SizedBox(height: UiConstants.space8),
PhoneInputFormField(
initialValue: state.phoneNumber,
error: state.errorMessage ?? '',
onChanged: (String value) {
BlocProvider.of<AuthBloc>(
context,
).add(AuthPhoneUpdated(value));
},
initialValue: widget.state.phoneNumber,
error: widget.state.errorMessage ?? '',
onChanged: _handlePhoneChanged,
),
],
),
),
),
PhoneInputActions(isLoading: state.isLoading, onSendCode: onSendCode),
PhoneInputActions(
isLoading: widget.state.isLoading,
onSendCode: widget.onSendCode,
),
],
);
}

View File

@@ -0,0 +1,9 @@
class TestPhoneNumbers {
static const List<String> values = <String>[
'+15145912311', // Test User 1
];
static bool isTestNumber(String phoneNumber) {
return values.contains(phoneNumber);
}
}

View File

@@ -1,128 +1,197 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/src/session/staff_session_store.dart';
import '../../domain/repositories/clock_in_repository_interface.dart';
/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
final dc.ExampleConnector _dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
final Map<String, String> _shiftToApplicationId = {};
ClockInRepositoryImpl({
required dc.ExampleConnector dataConnect,
required firebase.FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
}) : _dataConnect = dataConnect;
Future<String> _getStaffId() async {
final firebase.User? user = _firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw Exception('Staff profile not found');
final StaffSession? session = StaffSessionStore.instance.session;
final String? staffId = session?.staff?.id;
if (staffId != null && staffId.isNotEmpty) {
return staffId;
}
return result.data.staffs.first.id;
throw Exception('Staff session not found');
}
/// Helper to convert Data Connect Timestamp to DateTime
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
// Attempt to use toJson assuming it matches the generated code's expectation of String
try {
// If t has toDate (e.g. cloud_firestore), usage would be t.toDate()
// But here we rely on toJson or toString
return DateTime.tryParse(t.toJson() as String);
} catch (_) {
DateTime? dt;
if (t is DateTime) {
dt = t;
} else if (t is String) {
dt = DateTime.tryParse(t);
} else {
try {
return DateTime.tryParse(t.toString());
} catch (e) {
return null;
}
if (t is Timestamp) {
dt = t.toDateTime();
}
} catch (_) {}
try {
if (dt == null && t.runtimeType.toString().contains('Timestamp')) {
dt = (t as dynamic).toDate();
}
} catch (_) {}
try {
if (dt == null) {
dt = DateTime.tryParse(t.toString());
}
} catch (_) {}
}
if (dt != null) {
return DateTimeUtils.toDeviceTime(dt);
}
return null;
}
/// Helper to create Timestamp from DateTime
Timestamp _fromDateTime(DateTime d) {
// Assuming Timestamp.fromJson takes an ISO string
return Timestamp.fromJson(d.toIso8601String());
return Timestamp.fromJson(d.toUtc().toIso8601String());
}
/// Helper to find today's active application
Future<dc.GetApplicationsByStaffIdApplications?> _getTodaysApplication(String staffId) async {
({Timestamp start, Timestamp end}) _utcDayRange(DateTime localDay) {
final DateTime dayStartUtc = DateTime.utc(
localDay.year,
localDay.month,
localDay.day,
);
final DateTime dayEndUtc = DateTime.utc(
localDay.year,
localDay.month,
localDay.day,
23,
59,
59,
999,
999,
);
return (
start: _fromDateTime(dayStartUtc),
end: _fromDateTime(dayEndUtc),
);
}
/// Helper to find today's applications ordered with the closest at the end.
Future<List<dc.GetApplicationsByStaffIdApplications>> _getTodaysApplications(
String staffId,
) async {
final DateTime now = DateTime.now();
// Fetch recent applications (assuming meaningful limit)
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result =
await _dataConnect.getApplicationsByStaffId(
staffId: staffId,
).limit(20).execute();
final range = _utcDayRange(now);
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables>
result = await _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.dayStart(range.start)
.dayEnd(range.end)
.execute();
final apps = result.data.applications;
if (apps.isEmpty) return const [];
_shiftToApplicationId
..clear()
..addEntries(apps.map((app) => MapEntry(app.shiftId, app.id)));
apps.sort((a, b) {
final DateTime? aTime =
_toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date);
final DateTime? bTime =
_toDateTime(b.shift.startTime) ?? _toDateTime(b.shift.date);
if (aTime == null && bTime == null) return 0;
if (aTime == null) return -1;
if (bTime == null) return 1;
final Duration aDiff = aTime.difference(now).abs();
final Duration bDiff = bTime.difference(now).abs();
return bDiff.compareTo(aDiff); // closest at the end
});
return apps;
}
dc.GetApplicationsByStaffIdApplications? _getActiveApplication(
List<dc.GetApplicationsByStaffIdApplications> apps,
) {
try {
return result.data.applications.firstWhere((dc.GetApplicationsByStaffIdApplications app) {
final DateTime? shiftTime = _toDateTime(app.shift.startTime);
if (shiftTime == null) return false;
final bool isSameDay = shiftTime.year == now.year &&
shiftTime.month == now.month &&
shiftTime.day == now.day;
if (!isSameDay) return false;
// Check Status
final dynamic status = app.status.stringValue;
return status != 'PENDING' && status != 'REJECTED' && status != 'NO_SHOW' && status != 'CANCELED';
return apps.firstWhere((app) {
final status = app.status.stringValue;
return status == 'CHECKED_IN' || status == 'LATE';
});
} catch (e) {
} catch (_) {
return null;
}
}
@override
Future<Shift?> getTodaysShift() async {
Future<List<Shift>> getTodaysShifts() async {
final String staffId = await _getStaffId();
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
if (apps.isEmpty) return const [];
if (app == null) return null;
final List<Shift> shifts = [];
for (final app in apps) {
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
final DateTime? createdDt = _toDateTime(app.createdAt);
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult =
await _dataConnect.getShiftById(id: shift.id).execute();
if (shiftResult.data.shift == null) return null;
final dc.GetShiftByIdShift fullShift = shiftResult.data.shift!;
return Shift(
id: fullShift.id,
title: fullShift.title,
clientName: fullShift.order.business.businessName,
logoUrl: '', // Not available in GetShiftById
hourlyRate: 0.0,
location: fullShift.location ?? '',
locationAddress: fullShift.locationAddress ?? '',
date: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '',
startTime: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '',
endTime: _toDateTime(fullShift.endTime)?.toIso8601String() ?? '',
createdDate: _toDateTime(fullShift.createdAt)?.toIso8601String() ?? '',
status: fullShift.status?.stringValue,
description: fullShift.description,
);
final String roleName = app.shiftRole.role.name;
final String orderName =
(shift.order.eventName ?? '').trim().isNotEmpty
? shift.order.eventName!
: shift.order.business.businessName;
final String title = '$roleName - $orderName';
shifts.add(
Shift(
id: shift.id,
title: title,
clientName: shift.order.business.businessName,
logoUrl: shift.order.business.companyLogoUrl ?? '',
hourlyRate: app.shiftRole.role.costPerHour,
location: shift.location ?? '',
locationAddress: shift.order.teamHub.hubName,
date: startDt?.toIso8601String() ?? '',
startTime: startDt?.toIso8601String() ?? '',
endTime: endDt?.toIso8601String() ?? '',
createdDate: createdDt?.toIso8601String() ?? '',
status: shift.status?.stringValue,
description: shift.description,
latitude: shift.latitude,
longitude: shift.longitude,
),
);
}
return shifts;
}
@override
Future<AttendanceStatus> getAttendanceStatus() async {
final String staffId = await _getStaffId();
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
if (app == null) {
final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
if (apps.isEmpty) {
return const AttendanceStatus(isCheckedIn: false);
}
final dc.GetApplicationsByStaffIdApplications? activeApp =
_getActiveApplication(apps);
final dc.GetApplicationsByStaffIdApplications app =
activeApp ?? apps.last;
return ClockInAdapter.toAttendanceStatus(
status: app.status.stringValue,
checkInTime: _toDateTime(app.checkInTime),
@@ -134,19 +203,25 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
@override
Future<AttendanceStatus> clockIn({required String shiftId, String? notes}) async {
final String staffId = await _getStaffId();
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResult =
await _dataConnect.getApplicationsByStaffId(staffId: staffId).execute();
final dc.GetApplicationsByStaffIdApplications app = appsResult.data.applications.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId);
await _dataConnect.updateApplicationStatus(
id: app.id,
roleId: app.shiftRole.id,
)
.status(dc.ApplicationStatus.CHECKED_IN)
.checkInTime(_fromDateTime(DateTime.now()))
.execute();
final String? cachedAppId = _shiftToApplicationId[shiftId];
dc.GetApplicationsByStaffIdApplications? app;
if (cachedAppId != null) {
try {
final apps = await _getTodaysApplications(staffId);
app = apps.firstWhere((a) => a.id == cachedAppId);
} catch (_) {}
}
app ??= (await _getTodaysApplications(staffId))
.firstWhere((a) => a.shiftId == shiftId);
await _dataConnect
.updateApplicationStatus(
id: app.id,
roleId: app.shiftRole.id,
)
.checkInTime(_fromDateTime(DateTime.now()))
.execute();
return getAttendanceStatus();
}
@@ -155,7 +230,10 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes}) async {
final String staffId = await _getStaffId();
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
final dc.GetApplicationsByStaffIdApplications? app =
_getActiveApplication(apps);
if (app == null) throw Exception('No active shift found to clock out');
await _dataConnect.updateApplicationStatus(
@@ -168,10 +246,4 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
return getAttendanceStatus();
}
@override
Future<List<Map<String, dynamic>>> getActivityLog() async {
// Placeholder as this wasn't main focus and returns raw maps
return <Map<String, dynamic>>[];
}
}

View File

@@ -3,9 +3,9 @@ import 'package:krow_domain/krow_domain.dart';
/// Repository interface for Clock In/Out functionality
abstract class ClockInRepositoryInterface {
/// Retrieves the shift assigned to the user for the current day.
/// Returns null if no shift is assigned for today.
Future<Shift?> getTodaysShift();
/// Retrieves the shifts assigned to the user for the current day.
/// Returns empty list if no shift is assigned for today.
Future<List<Shift>> getTodaysShifts();
/// Gets the current attendance status (e.g., checked in or not, times).
/// This helps in restoring the UI state if the app was killed.
@@ -18,7 +18,4 @@ abstract class ClockInRepositoryInterface {
/// Checks the user out for the currently active shift.
/// Optionally accepts [breakTimeMinutes] if tracked.
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes});
/// Retrieves a list of recent clock-in/out activities.
Future<List<Map<String, dynamic>>> getActivityLog();
}

View File

@@ -1,14 +0,0 @@
import 'package:krow_core/core.dart';
import '../repositories/clock_in_repository_interface.dart';
/// Use case for retrieving the activity log.
class GetActivityLogUseCase implements NoInputUseCase<List<Map<String, dynamic>>> {
final ClockInRepositoryInterface _repository;
GetActivityLogUseCase(this._repository);
@override
Future<List<Map<String, dynamic>>> call() {
return _repository.getActivityLog();
}
}

View File

@@ -2,14 +2,14 @@ import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/clock_in_repository_interface.dart';
/// Use case for retrieving the user's scheduled shift for today.
class GetTodaysShiftUseCase implements NoInputUseCase<Shift?> {
/// Use case for retrieving the user's scheduled shifts for today.
class GetTodaysShiftUseCase implements NoInputUseCase<List<Shift>> {
final ClockInRepositoryInterface _repository;
GetTodaysShiftUseCase(this._repository);
@override
Future<Shift?> call() {
return _repository.getTodaysShift();
Future<List<Shift>> call() {
return _repository.getTodaysShifts();
}
}

View File

@@ -1,10 +1,10 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:geolocator/geolocator.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_todays_shift_usecase.dart';
import '../../domain/usecases/get_attendance_status_usecase.dart';
import '../../domain/usecases/clock_in_usecase.dart';
import '../../domain/usecases/clock_out_usecase.dart';
import '../../domain/usecases/get_activity_log_usecase.dart';
import '../../domain/arguments/clock_in_arguments.dart';
import '../../domain/arguments/clock_out_arguments.dart';
import 'clock_in_event.dart';
@@ -15,11 +15,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
final GetAttendanceStatusUseCase _getAttendanceStatus;
final ClockInUseCase _clockIn;
final ClockOutUseCase _clockOut;
final GetActivityLogUseCase _getActivityLog;
// Mock Venue Location (e.g., Grand Hotel, NYC)
static const double venueLat = 40.7128;
static const double venueLng = -74.0060;
static const double allowedRadiusMeters = 500;
ClockInBloc({
@@ -27,14 +24,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
required GetAttendanceStatusUseCase getAttendanceStatus,
required ClockInUseCase clockIn,
required ClockOutUseCase clockOut,
required GetActivityLogUseCase getActivityLog,
}) : _getTodaysShift = getTodaysShift,
_getAttendanceStatus = getAttendanceStatus,
_clockIn = clockIn,
_clockOut = clockOut,
_getActivityLog = getActivityLog,
super(ClockInState(selectedDate: DateTime.now())) {
on<ClockInPageLoaded>(_onLoaded);
on<ShiftSelected>(_onShiftSelected);
on<DateSelected>(_onDateSelected);
on<CheckInRequested>(_onCheckIn);
on<CheckOutRequested>(_onCheckOut);
@@ -52,21 +48,30 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
) async {
emit(state.copyWith(status: ClockInStatus.loading));
try {
final shift = await _getTodaysShift();
final shifts = await _getTodaysShift();
final status = await _getAttendanceStatus();
final activity = await _getActivityLog();
// Check permissions silently on load? Maybe better to wait for user interaction or specific event
// However, if shift exists, we might want to check permission state
Shift? selectedShift;
if (shifts.isNotEmpty) {
if (status.activeShiftId != null) {
try {
selectedShift =
shifts.firstWhere((s) => s.id == status.activeShiftId);
} catch (_) {}
}
selectedShift ??= shifts.last;
}
emit(state.copyWith(
status: ClockInStatus.success,
todayShift: shift,
todayShifts: shifts,
selectedShift: selectedShift,
attendance: status,
activityLog: activity,
));
if (shift != null && !status.isCheckedIn) {
if (selectedShift != null && !status.isCheckedIn) {
add(RequestLocationPermission());
}
@@ -103,13 +108,25 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
void _startLocationUpdates() async {
try {
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
venueLat,
venueLng,
);
final isVerified = distance <= allowedRadiusMeters;
double distance = 0;
bool isVerified = false; // Require location match by default if shift has location
if (state.selectedShift != null &&
state.selectedShift!.latitude != null &&
state.selectedShift!.longitude != null) {
distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
state.selectedShift!.latitude!,
state.selectedShift!.longitude!,
);
isVerified = distance <= allowedRadiusMeters;
} else {
// If no shift location, assume verified or don't restrict?
// For strict clock-in, maybe false? but let's default to verified to avoid blocking if data missing
isVerified = true;
}
if (!isClosed) {
add(LocationUpdated(position: position, distance: distance, isVerified: isVerified));
@@ -141,6 +158,16 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
}
}
void _onShiftSelected(
ShiftSelected event,
Emitter<ClockInState> emit,
) {
emit(state.copyWith(selectedShift: event.shift));
if (!state.attendance.isCheckedIn) {
_startLocationUpdates();
}
}
void _onDateSelected(
DateSelected event,
Emitter<ClockInState> emit,

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:geolocator/geolocator.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class ClockInEvent extends Equatable {
const ClockInEvent();
@@ -10,6 +11,14 @@ abstract class ClockInEvent extends Equatable {
class ClockInPageLoaded extends ClockInEvent {}
class ShiftSelected extends ClockInEvent {
final Shift shift;
const ShiftSelected(this.shift);
@override
List<Object?> get props => [shift];
}
class DateSelected extends ClockInEvent {
final DateTime date;

View File

@@ -7,9 +7,9 @@ enum ClockInStatus { initial, loading, success, failure, actionInProgress }
class ClockInState extends Equatable {
final ClockInStatus status;
final Shift? todayShift;
final List<Shift> todayShifts;
final Shift? selectedShift;
final AttendanceStatus attendance;
final List<Map<String, dynamic>> activityLog;
final DateTime selectedDate;
final String checkInMode;
final String? errorMessage;
@@ -23,9 +23,9 @@ class ClockInState extends Equatable {
const ClockInState({
this.status = ClockInStatus.initial,
this.todayShift,
this.todayShifts = const [],
this.selectedShift,
this.attendance = const AttendanceStatus(),
this.activityLog = const [],
required this.selectedDate,
this.checkInMode = 'swipe',
this.errorMessage,
@@ -39,9 +39,9 @@ class ClockInState extends Equatable {
ClockInState copyWith({
ClockInStatus? status,
Shift? todayShift,
List<Shift>? todayShifts,
Shift? selectedShift,
AttendanceStatus? attendance,
List<Map<String, dynamic>>? activityLog,
DateTime? selectedDate,
String? checkInMode,
String? errorMessage,
@@ -54,9 +54,9 @@ class ClockInState extends Equatable {
}) {
return ClockInState(
status: status ?? this.status,
todayShift: todayShift ?? this.todayShift,
todayShifts: todayShifts ?? this.todayShifts,
selectedShift: selectedShift ?? this.selectedShift,
attendance: attendance ?? this.attendance,
activityLog: activityLog ?? this.activityLog,
selectedDate: selectedDate ?? this.selectedDate,
checkInMode: checkInMode ?? this.checkInMode,
errorMessage: errorMessage,
@@ -72,9 +72,9 @@ class ClockInState extends Equatable {
@override
List<Object?> get props => [
status,
todayShift,
todayShifts,
selectedShift,
attendance,
activityLog,
selectedDate,
checkInMode,
errorMessage,

View File

@@ -3,13 +3,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package: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/attendance_card.dart';
import '../widgets/commute_tracker.dart';
import '../widgets/date_selector.dart';
import '../widgets/lunch_break_modal.dart';
@@ -46,16 +46,23 @@ class _ClockInPageState extends State<ClockInPage> {
},
builder: (context, state) {
if (state.status == ClockInStatus.loading &&
state.todayShift == null) {
state.todayShifts.isEmpty) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final todayShift = state.todayShift;
final checkInTime = state.attendance.checkInTime;
final checkOutTime = state.attendance.checkOutTime;
final isCheckedIn = state.attendance.isCheckedIn;
final todayShifts = state.todayShifts;
final selectedShift = state.selectedShift;
final activeShiftId = state.attendance.activeShiftId;
final bool isActiveSelected =
selectedShift != null && selectedShift.id == activeShiftId;
final checkInTime =
isActiveSelected ? state.attendance.checkInTime : null;
final checkOutTime =
isActiveSelected ? state.attendance.checkOutTime : null;
final isCheckedIn =
state.attendance.isCheckedIn && isActiveSelected;
// Format times for display
final checkInStr = checkInTime != null
@@ -89,9 +96,9 @@ class _ClockInPageState extends State<ClockInPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Commute Tracker (shows before date selector when applicable)
if (todayShift != null)
if (selectedShift != null)
CommuteTracker(
shift: todayShift,
shift: selectedShift,
hasLocationConsent: state.hasLocationConsent,
isCommuteModeOn: state.isCommuteModeOn,
distanceMeters: state.distanceFromVenue,
@@ -125,92 +132,113 @@ class _ClockInPageState extends State<ClockInPage> {
const SizedBox(height: 16),
// Selected Shift Info Card
if (todayShift != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFE2E8F0),
), // slate-200
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"TODAY'S SHIFT",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColors.krowBlue,
letterSpacing: 0.5,
if (todayShifts.isNotEmpty)
Column(
children: todayShifts
.map(
(shift) => GestureDetector(
onTap: () =>
_bloc.add(ShiftSelected(shift)),
child: Container(
padding: const EdgeInsets.all(12),
margin:
const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(
12,
),
border: Border.all(
color: shift.id ==
selectedShift?.id
? AppColors.krowBlue
: const Color(0xFFE2E8F0),
width:
shift.id == selectedShift?.id
? 2
: 1,
),
),
const SizedBox(height: 2),
Text(
todayShift.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(
0xFF1E293B,
), // slate-800
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
shift.id ==
selectedShift?.id
? "SELECTED SHIFT"
: "TODAY'S SHIFT",
style: TextStyle(
fontSize: 10,
fontWeight:
FontWeight.w600,
color: shift.id ==
selectedShift?.id
? AppColors.krowBlue
: AppColors
.krowCharcoal,
letterSpacing: 0.5,
),
),
const SizedBox(height: 2),
Text(
shift.title,
style: const TextStyle(
fontSize: 14,
fontWeight:
FontWeight.w600,
color: Color(0xFF1E293B),
),
),
Text(
"${shift.clientName}${shift.location}",
style: const TextStyle(
fontSize: 12,
color: Color(0xFF64748B),
),
),
],
),
),
Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF475569),
),
),
Text(
"\$${shift.hourlyRate}/hr",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.krowBlue,
),
),
],
),
],
),
Text(
"${todayShift.clientName}${todayShift.location}",
style: const TextStyle(
fontSize: 12,
color: Color(
0xFF64748B,
), // slate-500
),
),
],
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${_formatTime(todayShift.startTime)} - ${_formatTime(todayShift.endTime)}",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF475569), // slate-600
),
),
Text(
"\$${todayShift.hourlyRate}/hr",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.krowBlue,
),
),
],
),
],
),
)
.toList(),
),
// Swipe To Check In / Checked Out State / No Shift State
if (todayShift != null && checkOutTime == null) ...[
if (!isCheckedIn && !_isCheckInAllowed(todayShift))
if (selectedShift != null && checkOutTime == null) ...[
if (!isCheckedIn &&
!_isCheckInAllowed(selectedShift))
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
@@ -236,7 +264,7 @@ class _ClockInPageState extends State<ClockInPage> {
),
const SizedBox(height: 4),
Text(
"Check-in available at ${_getCheckInAvailabilityTime(todayShift)}",
"Check-in available at ${_getCheckInAvailabilityTime(selectedShift)}",
style: const TextStyle(
fontSize: 14,
color: Color(0xFF64748B), // slate-500
@@ -259,7 +287,9 @@ class _ClockInPageState extends State<ClockInPage> {
await _showNFCDialog(context);
} else {
_bloc.add(
CheckInRequested(shiftId: todayShift.id),
CheckInRequested(
shiftId: selectedShift.id,
),
);
}
},
@@ -277,7 +307,7 @@ class _ClockInPageState extends State<ClockInPage> {
);
},
),
] else if (todayShift != null &&
] else if (selectedShift != null &&
checkOutTime != null) ...[
// Shift Completed State
Container(
@@ -417,78 +447,7 @@ class _ClockInPageState extends State<ClockInPage> {
const SizedBox(height: 16),
// Recent Activity List
if (state.activityLog.isNotEmpty)
...state.activityLog.map(
(activity) => Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFF1F5F9),
), // slate-100
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(
0.1,
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
LucideIcons.mapPin,
color: AppColors.krowBlue,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
DateFormat('MMM d').format(
activity['date'] as DateTime,
),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(
0xFF0F172A,
), // slate-900
),
),
Text(
"${activity['start']} - ${activity['end']}",
style: const TextStyle(
fontSize: 12,
color: Color(
0xFF64748B,
), // slate-500
),
),
],
),
),
Text(
activity['hours'] as String,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.krowBlue,
),
),
],
),
),
),
// Recent Activity List (Temporarily removed)
const SizedBox(height: 16),
],
),
@@ -552,87 +511,6 @@ class _ClockInPageState extends State<ClockInPage> {
);
}
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.krowBlue.withOpacity(0.2),
width: 2,
),
),
child: CircleAvatar(
backgroundColor: AppColors.krowBlue.withOpacity(0.1),
child: const Text(
'K',
style: TextStyle(
color: AppColors.krowBlue,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Good Morning',
style: TextStyle(color: AppColors.krowMuted, fontSize: 12),
),
Text(
'Krower',
style: TextStyle(
color: AppColors.krowCharcoal,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Warehouse Assistant',
style: TextStyle(color: AppColors.krowMuted, fontSize: 12),
),
],
),
],
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(
20,
), // Rounded full for this page per design
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: const Icon(
LucideIcons.bell,
color: AppColors.krowMuted,
size: 20,
),
),
],
),
);
}
Future<void> _showNFCDialog(BuildContext context) async {
bool scanned = false;
@@ -731,31 +609,40 @@ class _ClockInPageState extends State<ClockInPage> {
// After dialog closes, trigger the event if scan was successful (simulated)
// In real app, we would check the dialog result
if (scanned && _bloc.state.todayShift != null) {
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id));
if (scanned && _bloc.state.selectedShift != null) {
_bloc.add(CheckInRequested(shiftId: _bloc.state.selectedShift!.id));
}
}
// --- Helper Methods ---
String _formatTime(String timeStr) {
// Expecting HH:mm or HH:mm:ss
if (timeStr.isEmpty) return '';
try {
if (timeStr.isEmpty) return '';
final parts = timeStr.split(':');
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
// Try parsing as ISO string first (which contains date)
final dt = DateTime.parse(timeStr);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return timeStr;
} catch (_) {
// Fallback for strict "HH:mm" or "HH:mm:ss" strings
try {
final parts = timeStr.split(':');
if (parts.length >= 2) {
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
return DateFormat('h:mm a').format(dt);
}
return timeStr;
} catch (e) {
return timeStr;
}
}
}
bool _isCheckInAllowed(dynamic shift) {
bool _isCheckInAllowed(Shift shift) {
if (shift == null) return false;
try {
// Parse shift date (e.g. 2024-01-31T09:00:00)
// The Shift entity has 'date' which is the start DateTime string
final shiftStart = DateTime.parse(shift.date);
final shiftStart = DateTime.parse(shift.startTime);
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateTime.now().isAfter(windowStart);
} catch (e) {
@@ -764,10 +651,10 @@ class _ClockInPageState extends State<ClockInPage> {
}
}
String _getCheckInAvailabilityTime(dynamic shift) {
String _getCheckInAvailabilityTime(Shift shift) {
if (shift == null) return '';
try {
final shiftStart = DateTime.parse(shift.date);
final shiftStart = DateTime.parse(shift.startTime.trim());
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateFormat('h:mm a').format(windowStart);
} catch (e) {

View File

@@ -66,9 +66,14 @@ class _CommuteTrackerState extends State<CommuteTracker> {
// For demo purposes, check if we're within 24 hours of shift
final now = DateTime.now();
final shiftStart = DateTime.parse(
'${widget.shift!.date} ${widget.shift!.startTime}',
);
DateTime shiftStart;
try {
shiftStart = DateTime.parse(widget.shift!.startTime);
} catch (_) {
shiftStart = DateTime.parse(
'${widget.shift!.date} ${widget.shift!.startTime}',
);
}
final hoursUntilShift = shiftStart.difference(now).inHours;
final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;

View File

@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
@@ -6,7 +5,6 @@ import 'data/repositories_impl/clock_in_repository_impl.dart';
import 'domain/repositories/clock_in_repository_interface.dart';
import 'domain/usecases/clock_in_usecase.dart';
import 'domain/usecases/clock_out_usecase.dart';
import 'domain/usecases/get_activity_log_usecase.dart';
import 'domain/usecases/get_attendance_status_usecase.dart';
import 'domain/usecases/get_todays_shift_usecase.dart';
import 'presentation/bloc/clock_in_bloc.dart';
@@ -19,7 +17,6 @@ class StaffClockInModule extends Module {
i.add<ClockInRepositoryInterface>(
() => ClockInRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
@@ -28,7 +25,6 @@ class StaffClockInModule extends Module {
i.add<GetAttendanceStatusUseCase>(GetAttendanceStatusUseCase.new);
i.add<ClockInUseCase>(ClockInUseCase.new);
i.add<ClockOutUseCase>(ClockOutUseCase.new);
i.add<GetActivityLogUseCase>(GetActivityLogUseCase.new);
// BLoC
i.add<ClockInBloc>(ClockInBloc.new);

View File

@@ -32,28 +32,40 @@ class HomeRepositoryImpl implements HomeRepository {
Future<List<Shift>> _getShiftsForDate(DateTime date) async {
try {
final staffId = _currentStaffId;
// Create start and end timestamps for the target date
final DateTime start = DateTime(date.year, date.month, date.day);
final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
final response = await ExampleConnector.instance
.getApplicationsByStaffId(staffId: _currentStaffId)
.getApplicationsByStaffId(staffId: staffId)
.dayStart(_toTimestamp(start))
.dayEnd(_toTimestamp(end))
.execute();
final targetYmd = DateFormat('yyyy-MM-dd').format(date);
return response.data.applications
.where((app) {
final shiftDate = app.shift.date?.toDate();
if (shiftDate == null) return false;
final isDateMatch = DateFormat('yyyy-MM-dd').format(shiftDate) == targetYmd;
final isAssigned = app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED;
return isDateMatch && isAssigned;
})
.map((app) => _mapApplicationToShift(app))
.toList();
// Filter for ACCEPTED 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)
);
final List<Shift> shifts = [];
for (final app in apps) {
shifts.add(_mapApplicationToShift(app));
}
return shifts;
} catch (e) {
return [];
}
}
Timestamp _toTimestamp(DateTime dateTime) {
final DateTime utc = dateTime.toUtc();
final int seconds = utc.millisecondsSinceEpoch ~/ 1000;
final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000;
return Timestamp(nanoseconds, seconds);
}
@override
Future<List<Shift>> getRecommendedShifts() async {
@@ -93,21 +105,26 @@ class HomeRepositoryImpl implements HomeRepository {
final s = app.shift;
final r = app.shiftRole;
return Shift(
id: s.id,
title: r.role.name,
clientName: s.order.business.businessName,
hourlyRate: r.role.costPerHour,
location: s.location ?? 'Unknown',
locationAddress: s.location ?? '',
date: s.date?.toDate().toIso8601String() ?? '',
startTime: DateFormat('HH:mm').format(r.startTime?.toDate() ?? DateTime.now()),
endTime: DateFormat('HH:mm').format(r.endTime?.toDate() ?? DateTime.now()),
createdDate: app.createdAt?.toDate().toIso8601String() ?? '',
tipsAvailable: false, // Not in API
mealProvided: false, // Not in API
managers: [], // Not in this query
description: null,
return ShiftAdapter.fromApplicationData(
shiftId: s.id,
roleId: r.roleId,
roleName: r.role.name,
businessName: s.order.business.businessName,
companyLogoUrl: s.order.business.companyLogoUrl,
costPerHour: r.role.costPerHour,
shiftLocation: s.location,
teamHubName: s.order.teamHub.hubName,
shiftDate: s.date?.toDate(),
startTime: r.startTime?.toDate(),
endTime: r.endTime?.toDate(),
createdAt: app.createdAt?.toDate(),
status: 'confirmed',
description: s.description,
durationDays: s.durationDays,
count: r.count,
assigned: r.assigned,
eventName: s.order.eventName,
hasApplied: true,
);
}

View File

@@ -13,7 +13,6 @@ import 'package:staff_home/src/presentation/widgets/home_page/quick_action_item.
import 'package:staff_home/src/presentation/widgets/home_page/recommended_shift_card.dart';
import 'package:staff_home/src/presentation/widgets/home_page/section_header.dart';
import 'package:staff_home/src/presentation/widgets/shift_card.dart';
import 'package:staff_home/src/presentation/widgets/worker/auto_match_toggle.dart';
/// The home page for the staff worker application.
///

View File

@@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart';
import '../navigation/home_navigator.dart';
class ShiftCard extends StatefulWidget {
final Shift shift;
@@ -73,10 +74,7 @@ class _ShiftCardState extends State<ShiftCard> {
? null
: () {
setState(() => isExpanded = !isExpanded);
Modular.to.pushNamed(
'/shift-details/${widget.shift.id}',
arguments: widget.shift,
);
Modular.to.pushShiftDetails(widget.shift);
},
child: Container(
margin: const EdgeInsets.only(bottom: 12),

View File

@@ -3,6 +3,7 @@ import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_data_connect/src/session/staff_session_store.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_core/core.dart';
import '../../domain/repositories/payments_repository.dart';
class PaymentsRepositoryImpl implements PaymentsRepository {
@@ -46,25 +47,34 @@ class PaymentsRepositoryImpl implements PaymentsRepository {
/// Helper to convert Data Connect Timestamp to DateTime
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
if (t is DateTime) return t;
if (t is String) return DateTime.tryParse(t);
DateTime? dt;
if (t is DateTime) {
dt = t;
} else if (t is String) {
dt = DateTime.tryParse(t);
} else {
try {
if (t is Timestamp) {
dt = t.toDateTime();
}
} catch (_) {}
try {
if (t is Timestamp) {
return t.toDateTime();
}
} catch (_) {}
try {
if (t.runtimeType.toString().contains('Timestamp')) {
return (t as dynamic).toDate();
}
} catch (_) {}
try {
return DateTime.tryParse(t.toString());
} catch (_) {}
try {
if (dt == null && t.runtimeType.toString().contains('Timestamp')) {
dt = (t as dynamic).toDate();
}
} catch (_) {}
try {
if (dt == null) {
dt = DateTime.tryParse(t.toString());
}
} catch (_) {}
}
if (dt != null) {
return DateTimeUtils.toDeviceTime(dt);
}
return null;
}

View File

@@ -2,6 +2,7 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart' as domain;
import 'package:krow_core/core.dart';
import '../../domain/repositories/certificates_repository.dart';
@@ -63,7 +64,9 @@ class CertificatesRepositoryImpl implements CertificatesRepository {
description: null, // Description not available in this query response
status: _mapStatus(doc.status),
documentUrl: doc.documentUrl,
expiryDate: doc.expiryDate?.toDateTime(),
expiryDate: doc.expiryDate == null
? null
: DateTimeUtils.toDeviceTime(doc.expiryDate!.toDateTime()),
);
}

View File

@@ -2,6 +2,7 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart' as domain;
import 'package:krow_core/core.dart';
import '../../domain/repositories/documents_repository.dart';
@@ -75,7 +76,9 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
description: null, // Description not available in data source
status: _mapStatus(doc.status),
documentUrl: doc.documentUrl,
expiryDate: doc.expiryDate?.toDateTime(),
expiryDate: doc.expiryDate == null
? null
: DateTimeUtils.toDeviceTime(doc.expiryDate!.toDateTime()),
);
}

View File

@@ -1,4 +1,5 @@
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
@@ -62,14 +63,17 @@ class TaxFormMapper {
status: form.status.stringValue,
staffId: form.staffId,
formData: formData,
updatedAt: form.updatedAt?.toDateTime(),
updatedAt: form.updatedAt == null
? null
: DateTimeUtils.toDeviceTime(form.updatedAt!.toDateTime()),
);
}
static String? _formatDate(Timestamp? timestamp) {
if (timestamp == null) return null;
final DateTime date = timestamp.toDateTime();
final DateTime date =
DateTimeUtils.toDeviceTime(timestamp.toDateTime());
return '${date.month.toString().padLeft(2, '0')}/${date.day.toString().padLeft(2, '0')}/${date.year}';
}

View File

@@ -5,6 +5,7 @@ import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
// ignore: implementation_imports
import 'package:krow_domain/src/adapters/financial/time_card_adapter.dart';
import 'package:krow_core/core.dart';
import '../../domain/repositories/time_card_repository.dart';
/// Implementation of [TimeCardRepository] using Firebase Data Connect.
@@ -40,12 +41,15 @@ class TimeCardRepositoryImpl implements TimeCardRepository {
return result.data.applications
.where((dc.GetApplicationsByStaffIdApplications app) {
final DateTime? shiftDate = app.shift.date?.toDateTime();
final DateTime? shiftDate = app.shift.date == null
? null
: DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime());
if (shiftDate == null) return false;
return shiftDate.year == month.year && shiftDate.month == month.month;
})
.map((dc.GetApplicationsByStaffIdApplications app) {
final DateTime shiftDate = app.shift.date!.toDateTime();
final DateTime shiftDate =
DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime());
final String startTime = _formatTime(app.checkInTime) ?? _formatTime(app.shift.startTime) ?? '';
final String endTime = _formatTime(app.checkOutTime) ?? _formatTime(app.shift.endTime) ?? '';
@@ -73,6 +77,7 @@ class TimeCardRepositoryImpl implements TimeCardRepository {
String? _formatTime(fdc.Timestamp? timestamp) {
if (timestamp == null) return null;
return DateFormat('HH:mm').format(timestamp.toDateTime());
return DateFormat('HH:mm')
.format(DateTimeUtils.toDeviceTime(timestamp.toDateTime()));
}
}

View File

@@ -37,7 +37,9 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
}
try {
final response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
final response = await _dataConnect
.getStaffByUserId(userId: user.uid)
.execute();
if (response.data.staffs.isNotEmpty) {
_cachedStaffId = response.data.staffs.first.id;
return _cachedStaffId!;
@@ -47,9 +49,9 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
}
// 4. Fallback (should ideally not happen if DB is seeded)
return user.uid;
return user.uid;
}
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
DateTime? dt;
@@ -75,62 +77,76 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
return null;
}
@override
Future<List<Shift>> getMyShifts() async {
return _fetchApplications(dc.ApplicationStatus.ACCEPTED);
/// Helper method to map Data Connect application to domain Shift using ShiftAdapter.
Shift _mapApplicationToShift(
dynamic app,
String status, {
bool hasApplied = true,
}) {
return ShiftAdapter.fromApplicationData(
shiftId: app.shift.id,
roleId: app.shiftRole.roleId,
roleName: app.shiftRole.role.name,
businessName: app.shift.order.business.businessName,
companyLogoUrl: app.shift.order.business.companyLogoUrl,
costPerHour: app.shiftRole.role.costPerHour,
shiftLocation: app.shift.location,
teamHubName: app.shift.order.teamHub.hubName,
shiftDate: _toDateTime(app.shift.date),
startTime: _toDateTime(app.shiftRole.startTime),
endTime: _toDateTime(app.shiftRole.endTime),
createdAt: _toDateTime(app.createdAt),
status: status,
description: app.shift.description,
durationDays: app.shift.durationDays,
count: app.shiftRole.count,
assigned: app.shiftRole.assigned,
eventName: app.shift.order.eventName,
hasApplied: hasApplied,
);
}
@override
Future<List<Shift>> getMyShifts({
required DateTime start,
required DateTime end,
}) async {
return _fetchApplications(
[dc.ApplicationStatus.ACCEPTED, dc.ApplicationStatus.CONFIRMED],
start: start,
end: end,
);
}
@override
Future<List<Shift>> getPendingAssignments() async {
return _fetchApplications(dc.ApplicationStatus.PENDING);
return _fetchApplications([dc.ApplicationStatus.PENDING]);
}
@override
Future<List<Shift>> getCancelledShifts() async {
return _fetchApplications(dc.ApplicationStatus.REJECTED);
return _fetchApplications([dc.ApplicationStatus.REJECTED]);
}
@override
Future<List<Shift>> getHistoryShifts() async {
return _fetchApplications(dc.ApplicationStatus.CHECKED_OUT);
}
Future<List<Shift>> _fetchApplications(dc.ApplicationStatus status) async {
try {
final staffId = await _getStaffId();
final response = await _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.listCompletedApplicationsByStaffId(staffId: staffId)
.execute();
final apps = response.data.applications.where((app) => app.status.stringValue == status.name);
final List<Shift> shifts = [];
for (final app in apps) {
_shiftToAppIdMap[app.shift.id] = app.id;
_appToRoleIdMap[app.id] = app.shiftRole.id;
final shift = await _getShiftDetails(app.shift.id);
if (shift != null) {
// Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED)
shifts.add(Shift(
id: shift.id,
title: shift.title,
clientName: shift.clientName,
logoUrl: shift.logoUrl,
hourlyRate: shift.hourlyRate,
location: shift.location,
locationAddress: shift.locationAddress,
date: shift.date,
startTime: shift.startTime,
endTime: shift.endTime,
createdDate: shift.createdDate,
status: _mapStatus(status),
description: shift.description,
durationDays: shift.durationDays,
requiredSlots: shift.requiredSlots,
filledSlots: shift.filledSlots,
));
}
for (final app in response.data.applications) {
_shiftToAppIdMap[app.shift.id] = app.id;
_appToRoleIdMap[app.id] = app.shiftRole.id;
shifts.add(
_mapApplicationToShift(
app,
_mapStatus(dc.ApplicationStatus.CHECKED_OUT),
),
);
}
return shifts;
} catch (e) {
@@ -138,6 +154,51 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
}
}
Future<List<Shift>> _fetchApplications(
List<dc.ApplicationStatus> statuses, {
DateTime? start,
DateTime? end,
}) async {
try {
final staffId = await _getStaffId();
var query = _dataConnect.getApplicationsByStaffId(staffId: staffId);
if (start != null && end != null) {
query = query
.dayStart(_toTimestamp(start))
.dayEnd(_toTimestamp(end));
}
final response = await query.execute();
final statusNames = statuses.map((s) => s.name).toSet();
final apps = response.data.applications.where(
(app) => statusNames.contains(app.status.stringValue),
);
final List<Shift> shifts = [];
for (final app in apps) {
_shiftToAppIdMap[app.shift.id] = app.id;
_appToRoleIdMap[app.id] = app.shiftRole.id;
// Use the first matching status for mapping
final matchingStatus = statuses.firstWhere(
(s) => s.name == app.status.stringValue,
orElse: () => statuses.first,
);
shifts.add(_mapApplicationToShift(app, _mapStatus(matchingStatus)));
}
return shifts;
} catch (e) {
return <Shift>[];
}
}
Timestamp _toTimestamp(DateTime dateTime) {
final DateTime utc = dateTime.toUtc();
final int seconds = utc.millisecondsSinceEpoch ~/ 1000;
final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000;
return Timestamp(nanoseconds, seconds);
}
String _mapStatus(dc.ApplicationStatus status) {
switch (status) {
case dc.ApplicationStatus.ACCEPTED:
@@ -156,133 +217,301 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
@override
Future<List<Shift>> getAvailableShifts(String query, String type) async {
try {
final result = await _dataConnect.listShifts().execute();
final allShifts = result.data.shifts;
final List<Shift> mappedShifts = [];
for (final s in allShifts) {
// For each shift, map to Domain Shift
// Note: date fields in generated code might be specific types
final startDt = _toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt);
mappedShifts.add(Shift(
id: s.id,
title: s.title,
clientName: s.order.business.businessName,
logoUrl: null,
hourlyRate: s.cost ?? 0.0,
location: s.location ?? '',
locationAddress: s.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: s.status?.stringValue.toLowerCase() ?? 'open',
description: s.description,
durationDays: s.durationDays,
requiredSlots: null, // Basic list doesn't fetch detailed role stats yet
filledSlots: null,
));
}
if (query.isNotEmpty) {
return mappedShifts.where((s) =>
s.title.toLowerCase().contains(query.toLowerCase()) ||
s.clientName.toLowerCase().contains(query.toLowerCase())
).toList();
}
return mappedShifts;
} catch (e) {
return <Shift>[];
try {
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
if (vendorId == null || vendorId.isEmpty) {
return <Shift>[];
}
}
@override
Future<Shift?> getShiftDetails(String shiftId) async {
return _getShiftDetails(shiftId);
}
Future<Shift?> _getShiftDetails(String shiftId) async {
try {
final result = await _dataConnect.getShiftById(id: shiftId).execute();
final s = result.data.shift;
if (s == null) return null;
int? required;
int? filled;
try {
final rolesRes = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for(var r in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
}
} catch (_) {}
final result = await _dataConnect
.listShiftRolesByVendorId(vendorId: vendorId)
.execute();
final allShiftRoles = result.data.shiftRoles;
final startDt = _toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt);
return Shift(
id: s.id,
title: s.title,
clientName: s.order.business.businessName,
final List<Shift> mappedShifts = [];
for (final sr in allShiftRoles) {
final DateTime? shiftDate = _toDateTime(sr.shift.date);
final startDt = _toDateTime(sr.startTime);
final endDt = _toDateTime(sr.endTime);
final createdDt = _toDateTime(sr.createdAt);
mappedShifts.add(
Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.role.name,
clientName: sr.shift.order.business.businessName,
logoUrl: null,
hourlyRate: s.cost ?? 0.0,
location: s.location ?? '',
locationAddress: s.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
hourlyRate: sr.role.costPerHour,
location: sr.shift.location ?? '',
locationAddress: sr.shift.locationAddress ?? '',
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null
? DateFormat('HH:mm').format(startDt)
: '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: s.status?.stringValue ?? 'OPEN',
description: s.description,
durationDays: s.durationDays,
requiredSlots: required,
filledSlots: filled,
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
description: sr.shift.description,
durationDays: sr.shift.durationDays,
requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0,
),
);
} catch (e) {
return null;
}
if (query.isNotEmpty) {
return mappedShifts
.where(
(s) =>
s.title.toLowerCase().contains(query.toLowerCase()) ||
s.clientName.toLowerCase().contains(query.toLowerCase()),
)
.toList();
}
return mappedShifts;
} catch (e) {
return <Shift>[];
}
}
@override
Future<void> applyForShift(String shiftId, {bool isInstantBook = false}) async {
final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift');
final role = rolesResult.data.shiftRoles.first;
final staffId = await _getStaffId();
await _dataConnect.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: role.roleId,
status: isInstantBook ? dc.ApplicationStatus.ACCEPTED : dc.ApplicationStatus.PENDING,
origin: dc.ApplicationOrigin.STAFF,
).execute();
Future<Shift?> getShiftDetails(String shiftId, {String? roleId}) async {
return _getShiftDetails(shiftId, roleId: roleId);
}
Future<Shift?> _getShiftDetails(String shiftId, {String? roleId}) async {
try {
if (roleId != null && roleId.isNotEmpty) {
final roleResult = await _dataConnect
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
.execute();
final sr = roleResult.data.shiftRole;
if (sr == null) return null;
final DateTime? startDt = _toDateTime(sr.startTime);
final DateTime? endDt = _toDateTime(sr.endTime);
final DateTime? createdDt = _toDateTime(sr.createdAt);
final String? staffId = _auth.currentUser?.uid;
bool hasApplied = false;
String status = 'open';
if (staffId != null) {
final apps = await _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.execute();
final app = apps.data.applications
.where(
(a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId,
)
.firstOrNull;
if (app != null) {
hasApplied = true;
if (app.status is dc.Known<dc.ApplicationStatus>) {
final dc.ApplicationStatus s =
(app.status as dc.Known<dc.ApplicationStatus>).value;
status = _mapStatus(s);
}
}
}
return Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.shift.order.business.businessName,
clientName: sr.shift.order.business.businessName,
logoUrl: sr.shift.order.business.companyLogoUrl,
hourlyRate: sr.role.costPerHour,
location: sr.shift.location ?? sr.shift.order.teamHub.hubName,
locationAddress: sr.shift.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: status,
description: sr.shift.description,
durationDays: null,
requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0,
hasApplied: hasApplied,
totalValue: sr.totalValue,
);
}
final result = await _dataConnect.getShiftById(id: shiftId).execute();
final s = result.data.shift;
if (s == null) return null;
int? required;
int? filled;
try {
final rolesRes = await _dataConnect
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for (var r in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
}
} catch (_) {}
final startDt = _toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt);
return Shift(
id: s.id,
title: s.title,
clientName: s.order.business.businessName,
logoUrl: null,
hourlyRate: s.cost ?? 0.0,
location: s.location ?? '',
locationAddress: s.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: s.status?.stringValue ?? 'OPEN',
description: s.description,
durationDays: s.durationDays,
requiredSlots: required,
filledSlots: filled,
);
} catch (e) {
return null;
}
}
@override
Future<void> applyForShift(
String shiftId, {
bool isInstantBook = false,
String? roleId,
}) async {
final staffId = await _getStaffId();
String targetRoleId = roleId ?? '';
if (targetRoleId.isEmpty) {
throw Exception('Missing role id.');
}
final roleResult = await _dataConnect
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
.execute();
final role = roleResult.data.shiftRole;
if (role == null) {
throw Exception('Shift role not found');
}
final shiftResult = await _dataConnect.getShiftById(id: shiftId).execute();
final shift = shiftResult.data.shift;
if (shift == null) {
throw Exception('Shift not found');
}
final DateTime? shiftDate = _toDateTime(shift.date);
if (shiftDate != null) {
final DateTime dayStartUtc = DateTime.utc(
shiftDate.year,
shiftDate.month,
shiftDate.day,
);
final DateTime dayEndUtc = DateTime.utc(
shiftDate.year,
shiftDate.month,
shiftDate.day,
23,
59,
59,
999,
999,
);
print(
'Staff applyForShift: dayStartUtc=${_toTimestamp(dayStartUtc).toJson()} '
'dayEndUtc=${_toTimestamp(dayEndUtc).toJson()}',
);
final dayApplications = await _dataConnect
.vaidateDayStaffApplication(staffId: staffId)
.dayStart(_toTimestamp(dayStartUtc))
.dayEnd(_toTimestamp(dayEndUtc))
.execute();
if (dayApplications.data.applications.isNotEmpty) {
throw Exception('The user already has a shift that day.');
}
}
final existingApplicationResult = await _dataConnect
.getApplicationByStaffShiftAndRole(
staffId: staffId,
shiftId: shiftId,
roleId: targetRoleId,
)
.execute();
if (existingApplicationResult.data.applications.isNotEmpty) {
throw Exception('Application already exists.');
}
final int assigned = role.assigned ?? 0;
if (assigned >= role.count) {
throw Exception('This shift is full.');
}
final int filled = shift.filled ?? 0;
String? appId;
bool updatedRole = false;
bool updatedShift = false;
try {
final appResult = await _dataConnect
.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: targetRoleId,
status: dc.ApplicationStatus.CONFIRMED,
origin: dc.ApplicationOrigin.STAFF,
)
// TODO: this should be PENDING so a vendor can accept it.
.execute();
appId = appResult.data.application_insert.id;
await _dataConnect
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
.assigned(assigned + 1)
.execute();
updatedRole = true;
await _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute();
updatedShift = true;
} catch (e) {
if (updatedShift) {
await _dataConnect.updateShift(id: shiftId).filled(filled).execute();
}
if (updatedRole) {
await _dataConnect
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
.assigned(assigned)
.execute();
}
if (appId != null) {
await _dataConnect.deleteApplication(id: appId).execute();
}
rethrow;
}
}
@override
Future<void> acceptShift(String shiftId) async {
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
}
@override
Future<void> declineShift(String shiftId) async {
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.REJECTED);
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED);
}
Future<void> _updateApplicationStatus(String shiftId, dc.ApplicationStatus newStatus) async {
@override
Future<void> declineShift(String shiftId) async {
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.REJECTED);
}
Future<void> _updateApplicationStatus(
String shiftId,
dc.ApplicationStatus newStatus,
) async {
String? appId = _shiftToAppIdMap[shiftId];
String? roleId;
@@ -293,44 +522,49 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
// Re-check map
appId = _shiftToAppIdMap[shiftId];
if (appId != null) {
roleId = _appToRoleIdMap[appId];
roleId = _appToRoleIdMap[appId];
} else {
// Fallback fetch
final staffId = await _getStaffId();
final apps = await _dataConnect.getApplicationsByStaffId(staffId: staffId).execute();
final app = apps.data.applications.where((a) => a.shiftId == shiftId).firstOrNull;
if (app != null) {
appId = app.id;
roleId = app.shiftRole.id;
}
// Fallback fetch
final staffId = await _getStaffId();
final apps = await _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.execute();
final app = apps.data.applications
.where((a) => a.shiftId == shiftId)
.firstOrNull;
if (app != null) {
appId = app.id;
roleId = app.shiftRole.id;
}
}
if (appId == null || roleId == null) {
// If we are rejecting and can't find an application, create one as rejected (declining an available shift)
if (newStatus == dc.ApplicationStatus.REJECTED) {
final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesResult.data.shiftRoles.isNotEmpty) {
final role = rolesResult.data.shiftRoles.first;
final staffId = await _getStaffId();
await _dataConnect.createApplication(
final rolesResult = await _dataConnect
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesResult.data.shiftRoles.isNotEmpty) {
final role = rolesResult.data.shiftRoles.first;
final staffId = await _getStaffId();
await _dataConnect
.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: role.id,
status: dc.ApplicationStatus.REJECTED,
origin: dc.ApplicationOrigin.STAFF,
).execute();
return;
}
)
.execute();
return;
}
}
throw Exception("Application not found for shift $shiftId");
}
await _dataConnect.updateApplicationStatus(
id: appId,
roleId: roleId,
)
.status(newStatus)
.execute();
await _dataConnect
.updateApplicationStatus(id: appId, roleId: roleId)
.status(newStatus)
.execute();
}
}

View File

@@ -0,0 +1,11 @@
import 'package:krow_core/core.dart';
class GetMyShiftsArguments extends UseCaseArgument {
final DateTime start;
final DateTime end;
const GetMyShiftsArguments({
required this.start,
required this.end,
});
}

View File

@@ -0,0 +1,14 @@
import 'package:equatable/equatable.dart';
class GetShiftDetailsArguments extends Equatable {
final String shiftId;
final String? roleId;
const GetShiftDetailsArguments({
required this.shiftId,
this.roleId,
});
@override
List<Object?> get props => [shiftId, roleId];
}

View File

@@ -6,7 +6,10 @@ import 'package:krow_domain/krow_domain.dart';
/// Implementations of this interface should reside in the data layer.
abstract interface class ShiftsRepositoryInterface {
/// Retrieves the list of shifts assigned to the current user.
Future<List<Shift>> getMyShifts();
Future<List<Shift>> getMyShifts({
required DateTime start,
required DateTime end,
});
/// Retrieves available shifts matching the given [query] and [type].
Future<List<Shift>> getAvailableShifts(String query, String type);
@@ -15,12 +18,16 @@ abstract interface class ShiftsRepositoryInterface {
Future<List<Shift>> getPendingAssignments();
/// Retrieves detailed information for a specific shift by [shiftId].
Future<Shift?> getShiftDetails(String shiftId);
Future<Shift?> getShiftDetails(String shiftId, {String? roleId});
/// Applies for a specific open shift.
///
/// [isInstantBook] determines if the application should be immediately accepted.
Future<void> applyForShift(String shiftId, {bool isInstantBook = false});
Future<void> applyForShift(
String shiftId, {
bool isInstantBook = false,
String? roleId,
});
/// Accepts a pending shift assignment.
Future<void> acceptShift(String shiftId);

View File

@@ -5,7 +5,15 @@ class ApplyForShiftUseCase {
ApplyForShiftUseCase(this.repository);
Future<void> call(String shiftId, {bool isInstantBook = false}) async {
return repository.applyForShift(shiftId, isInstantBook: isInstantBook);
Future<void> call(
String shiftId, {
bool isInstantBook = false,
String? roleId,
}) async {
return repository.applyForShift(
shiftId,
isInstantBook: isInstantBook,
roleId: roleId,
);
}
}

View File

@@ -1,18 +1,21 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_my_shifts_arguments.dart';
import '../repositories/shifts_repository_interface.dart';
/// Use case for retrieving the user's assigned shifts.
///
/// This use case delegates to [ShiftsRepositoryInterface].
class GetMyShiftsUseCase extends NoInputUseCase<List<Shift>> {
class GetMyShiftsUseCase extends UseCase<GetMyShiftsArguments, List<Shift>> {
final ShiftsRepositoryInterface repository;
GetMyShiftsUseCase(this.repository);
@override
Future<List<Shift>> call() async {
return repository.getMyShifts();
Future<List<Shift>> call(GetMyShiftsArguments arguments) async {
return repository.getMyShifts(
start: arguments.start,
end: arguments.end,
);
}
}

View File

@@ -1,14 +1,18 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_shift_details_arguments.dart';
import '../repositories/shifts_repository_interface.dart';
class GetShiftDetailsUseCase extends UseCase<String, Shift?> {
class GetShiftDetailsUseCase extends UseCase<GetShiftDetailsArguments, Shift?> {
final ShiftsRepositoryInterface repository;
GetShiftDetailsUseCase(this.repository);
@override
Future<Shift?> call(String params) {
return repository.getShiftDetails(params);
Future<Shift?> call(GetShiftDetailsArguments params) {
return repository.getShiftDetails(
params.shiftId,
roleId: params.roleId,
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart';
import '../../../domain/usecases/apply_for_shift_usecase.dart';
import '../../../domain/usecases/decline_shift_usecase.dart';
import '../../../domain/usecases/get_shift_details_usecase.dart';
import '../../../domain/arguments/get_shift_details_arguments.dart';
import 'shift_details_event.dart';
import 'shift_details_state.dart';
@@ -26,7 +27,9 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
) async {
emit(ShiftDetailsLoading());
try {
final shift = await getShiftDetails(event.shiftId);
final shift = await getShiftDetails(
GetShiftDetailsArguments(shiftId: event.shiftId, roleId: event.roleId),
);
if (shift != null) {
emit(ShiftDetailsLoaded(shift));
} else {
@@ -42,8 +45,14 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
Emitter<ShiftDetailsState> emit,
) async {
try {
await applyForShift(event.shiftId, isInstantBook: true);
emit(const ShiftActionSuccess("Shift successfully booked!"));
await applyForShift(
event.shiftId,
isInstantBook: true,
roleId: event.roleId,
);
emit(
ShiftActionSuccess("Shift successfully booked!", shiftDate: event.date),
);
} catch (e) {
emit(ShiftDetailsError(e.toString()));
}

View File

@@ -9,18 +9,21 @@ abstract class ShiftDetailsEvent extends Equatable {
class LoadShiftDetailsEvent extends ShiftDetailsEvent {
final String shiftId;
const LoadShiftDetailsEvent(this.shiftId);
final String? roleId;
const LoadShiftDetailsEvent(this.shiftId, {this.roleId});
@override
List<Object?> get props => [shiftId];
List<Object?> get props => [shiftId, roleId];
}
class BookShiftDetailsEvent extends ShiftDetailsEvent {
final String shiftId;
const BookShiftDetailsEvent(this.shiftId);
final String? roleId;
final DateTime? date;
const BookShiftDetailsEvent(this.shiftId, {this.roleId, this.date});
@override
List<Object?> get props => [shiftId];
List<Object?> get props => [shiftId, roleId, date];
}
class DeclineShiftDetailsEvent extends ShiftDetailsEvent {

View File

@@ -30,8 +30,9 @@ class ShiftDetailsError extends ShiftDetailsState {
class ShiftActionSuccess extends ShiftDetailsState {
final String message;
const ShiftActionSuccess(this.message);
final DateTime? shiftDate;
const ShiftActionSuccess(this.message, {this.shiftDate});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shiftDate];
}

View File

@@ -4,6 +4,7 @@ import 'package:krow_domain/krow_domain.dart';
import 'package:meta/meta.dart';
import '../../../domain/arguments/get_available_shifts_arguments.dart';
import '../../../domain/arguments/get_my_shifts_arguments.dart';
import '../../../domain/usecases/get_available_shifts_usecase.dart';
import '../../../domain/usecases/get_cancelled_shifts_usecase.dart';
import '../../../domain/usecases/get_history_shifts_usecase.dart';
@@ -28,6 +29,8 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
required this.getHistoryShifts,
}) : super(ShiftsInitial()) {
on<LoadShiftsEvent>(_onLoadShifts);
on<LoadHistoryShiftsEvent>(_onLoadHistoryShifts);
on<LoadShiftsForRangeEvent>(_onLoadShiftsForRange);
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
}
@@ -43,10 +46,12 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
// Or load all for simplicity as per prototype logic which had them all in memory.
try {
final myShiftsResult = await getMyShifts();
final List<DateTime> days = _getCalendarDaysForOffset(0);
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: days.first, end: days.last),
);
final pendingResult = await getPendingAssignments();
final cancelledResult = await getCancelledShifts();
final historyResult = await getHistoryShifts();
// Initial available with defaults
final availableResult = await getAvailableShifts(const GetAvailableShiftsArguments());
@@ -56,7 +61,66 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
pendingShifts: pendingResult,
cancelledShifts: cancelledResult,
availableShifts: _filterPastShifts(availableResult),
historyShifts: const [],
historyLoading: false,
historyLoaded: false,
searchQuery: '',
jobType: 'all',
));
} catch (_) {
emit(const ShiftsError('Failed to load shifts'));
}
}
Future<void> _onLoadHistoryShifts(
LoadHistoryShiftsEvent event,
Emitter<ShiftsState> emit,
) async {
final currentState = state;
if (currentState is! ShiftsLoaded) return;
if (currentState.historyLoading || currentState.historyLoaded) return;
emit(currentState.copyWith(historyLoading: true));
try {
final historyResult = await getHistoryShifts();
emit(currentState.copyWith(
historyShifts: historyResult,
historyLoading: false,
historyLoaded: true,
));
} catch (_) {
emit(currentState.copyWith(historyLoading: false));
}
}
Future<void> _onLoadShiftsForRange(
LoadShiftsForRangeEvent event,
Emitter<ShiftsState> emit,
) async {
try {
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: event.start, end: event.end),
);
if (state is ShiftsLoaded) {
final currentState = state as ShiftsLoaded;
emit(currentState.copyWith(myShifts: myShiftsResult));
return;
}
final pendingResult = await getPendingAssignments();
final cancelledResult = await getCancelledShifts();
final availableResult =
await getAvailableShifts(const GetAvailableShiftsArguments());
emit(ShiftsLoaded(
myShifts: myShiftsResult,
pendingShifts: pendingResult,
cancelledShifts: cancelledResult,
availableShifts: _filterPastShifts(availableResult),
historyShifts: const [],
historyLoading: false,
historyLoaded: false,
searchQuery: '',
jobType: 'all',
));
@@ -91,6 +155,17 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
}
}
List<DateTime> _getCalendarDaysForOffset(int weekOffset) {
final now = DateTime.now();
final int reactDayIndex = now.weekday == 7 ? 0 : now.weekday;
final int daysSinceFriday = (reactDayIndex + 2) % 7;
final start = now
.subtract(Duration(days: daysSinceFriday))
.add(Duration(days: weekOffset * 7));
final startDate = DateTime(start.year, start.month, start.day);
return List.generate(7, (index) => startDate.add(Duration(days: index)));
}
List<Shift> _filterPastShifts(List<Shift> shifts) {
final now = DateTime.now();
return shifts.where((shift) {

View File

@@ -10,6 +10,21 @@ sealed class ShiftsEvent extends Equatable {
class LoadShiftsEvent extends ShiftsEvent {}
class LoadHistoryShiftsEvent extends ShiftsEvent {}
class LoadShiftsForRangeEvent extends ShiftsEvent {
final DateTime start;
final DateTime end;
const LoadShiftsForRangeEvent({
required this.start,
required this.end,
});
@override
List<Object?> get props => [start, end];
}
class FilterAvailableShiftsEvent extends ShiftsEvent {
final String? query;
final String? jobType;

View File

@@ -18,6 +18,8 @@ class ShiftsLoaded extends ShiftsState {
final List<Shift> cancelledShifts;
final List<Shift> availableShifts;
final List<Shift> historyShifts;
final bool historyLoading;
final bool historyLoaded;
final String searchQuery;
final String jobType;
@@ -27,6 +29,8 @@ class ShiftsLoaded extends ShiftsState {
required this.cancelledShifts,
required this.availableShifts,
required this.historyShifts,
required this.historyLoading,
required this.historyLoaded,
required this.searchQuery,
required this.jobType,
});
@@ -37,6 +41,8 @@ class ShiftsLoaded extends ShiftsState {
List<Shift>? cancelledShifts,
List<Shift>? availableShifts,
List<Shift>? historyShifts,
bool? historyLoading,
bool? historyLoaded,
String? searchQuery,
String? jobType,
}) {
@@ -46,6 +52,8 @@ class ShiftsLoaded extends ShiftsState {
cancelledShifts: cancelledShifts ?? this.cancelledShifts,
availableShifts: availableShifts ?? this.availableShifts,
historyShifts: historyShifts ?? this.historyShifts,
historyLoading: historyLoading ?? this.historyLoading,
historyLoaded: historyLoaded ?? this.historyLoaded,
searchQuery: searchQuery ?? this.searchQuery,
jobType: jobType ?? this.jobType,
);
@@ -58,6 +66,8 @@ class ShiftsLoaded extends ShiftsState {
cancelledShifts,
availableShifts,
historyShifts,
historyLoading,
historyLoaded,
searchQuery,
jobType,
];

View File

@@ -2,7 +2,11 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
extension ShiftsNavigator on IModularNavigator {
void navigateToShiftsHome({DateTime? selectedDate}) {
navigate('/worker-main/shifts/', arguments: {'selectedDate': selectedDate});
}
void pushShiftDetails(Shift shift) {
pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift);
navigate('/worker-main/shift-details/${shift.id}', arguments: shift);
}
}

View File

@@ -4,16 +4,25 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic
import 'package:intl/intl.dart';
import 'package:staff_shifts/staff_shifts.dart';
import '../blocs/shift_details/shift_details_bloc.dart';
import '../blocs/shift_details/shift_details_event.dart';
import '../blocs/shift_details/shift_details_state.dart';
class ShiftDetailsPage extends StatelessWidget {
class ShiftDetailsPage extends StatefulWidget {
final String shiftId;
final Shift? shift;
const ShiftDetailsPage({super.key, required this.shiftId, this.shift});
@override
State<ShiftDetailsPage> createState() => _ShiftDetailsPageState();
}
class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
bool _actionDialogOpen = false;
bool _isApplying = false;
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
@@ -122,24 +131,37 @@ class ShiftDetailsPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider<ShiftDetailsBloc>(
create: (_) =>
Modular.get<ShiftDetailsBloc>()..add(LoadShiftDetailsEvent(shiftId)),
Modular.get<ShiftDetailsBloc>()
..add(
LoadShiftDetailsEvent(
widget.shiftId,
roleId: widget.shift?.roleId,
),
),
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
listener: (context, state) {
if (state is ShiftActionSuccess || state is ShiftDetailsError) {
_closeActionDialog(context);
}
if (state is ShiftActionSuccess) {
_isApplying = false;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: const Color(0xFF10B981),
),
);
Modular.to.pop(true); // Return outcome
Modular.to.navigateToShiftsHome(selectedDate: state.shiftDate);
} else if (state is ShiftDetailsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: const Color(0xFFEF4444),
),
);
if (_isApplying || widget.shift == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: const Color(0xFFEF4444),
),
);
}
_isApplying = false;
}
},
child: BlocBuilder<ShiftDetailsBloc, ShiftDetailsState>(
@@ -154,7 +176,7 @@ class ShiftDetailsPage extends StatelessWidget {
if (state is ShiftDetailsLoaded) {
displayShift = state.shift;
} else {
displayShift = shift;
displayShift = widget.shift;
}
if (displayShift == null) {
@@ -164,7 +186,8 @@ class ShiftDetailsPage extends StatelessWidget {
}
final duration = _calculateDuration(displayShift);
final estimatedTotal = (displayShift.hourlyRate) * duration;
final estimatedTotal =
displayShift.totalValue ?? (displayShift.hourlyRate * duration);
final openSlots =
(displayShift.requiredSlots ?? 0) -
(displayShift.filledSlots ?? 0);
@@ -172,8 +195,8 @@ class ShiftDetailsPage extends StatelessWidget {
return Scaffold(
appBar: UiAppBar(
title: displayShift.title,
showBackButton: true,
centerTitle: false,
onLeadingPressed: () => Modular.to.navigateToShiftsHome(),
),
body: Column(
children: [
@@ -396,14 +419,14 @@ class ShiftDetailsPage extends StatelessWidget {
color: UiColors.textPrimary,
),
),
Text(
displayShift.location.isEmpty
? "TBD"
: displayShift.locationAddress,
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
),
Text(
displayShift.location.isEmpty
? "TBD"
: displayShift.locationAddress,
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
),
],
),
],
@@ -438,41 +461,53 @@ class ShiftDetailsPage extends StatelessWidget {
),
],
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () =>
_declineShift(context, displayShift!.id),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFEF4444),
side: const BorderSide(
color: Color(0xFFEF4444),
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,
),
padding: const EdgeInsets.symmetric(
vertical: 16,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFEF4444),
side: const BorderSide(
color: Color(0xFFEF4444),
),
padding: const EdgeInsets.symmetric(
vertical: 16,
),
),
child: const Text("Decline"),
),
child: const Text("Decline"),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () =>
_bookShift(context, displayShift!.id),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => _bookShift(
context,
displayShift!,
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFF10B981,
),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
),
child: const Text("Book Shift"),
),
child: const Text("Book Shift"),
),
),
],
),
],
),
SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
),
@@ -489,7 +524,10 @@ class ShiftDetailsPage extends StatelessWidget {
);
}
void _bookShift(BuildContext context, String id) {
void _bookShift(
BuildContext context,
Shift shift,
) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
@@ -497,15 +535,20 @@ class ShiftDetailsPage extends StatelessWidget {
content: const Text('Do you want to instantly book this shift?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
onPressed: () => Modular.to.pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(ctx).pop();
BlocProvider.of<ShiftDetailsBloc>(
context,
).add(BookShiftDetailsEvent(id));
Modular.to.pop();
_showApplyingDialog(context, shift);
BlocProvider.of<ShiftDetailsBloc>(context).add(
BookShiftDetailsEvent(
shift.id,
roleId: shift.roleId,
date: DateTime.tryParse(shift.date),
),
);
},
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF10B981),
@@ -527,12 +570,11 @@ class ShiftDetailsPage extends StatelessWidget {
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
onPressed: () => Modular.to.pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(ctx).pop();
BlocProvider.of<ShiftDetailsBloc>(
context,
).add(DeclineShiftDetailsEvent(id));
@@ -546,4 +588,62 @@ class ShiftDetailsPage extends StatelessWidget {
),
);
}
void _showApplyingDialog(BuildContext context, Shift shift) {
if (_actionDialogOpen) return;
_actionDialogOpen = true;
_isApplying = true;
showDialog(
context: context,
useRootNavigator: true,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: const Text('Applying'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 36,
width: 36,
child: CircularProgressIndicator(),
),
const SizedBox(height: 16),
Text(
shift.title,
style: UiTypography.body2b.copyWith(
color: UiColors.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,
),
textAlign: TextAlign.center,
),
if (shift.clientName.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
shift.clientName,
style: UiTypography.body3r.copyWith(
color: UiColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
],
),
),
).then((_) {
_actionDialogOpen = false;
});
}
void _closeActionDialog(BuildContext context) {
if (!_actionDialogOpen) return;
Navigator.of(context, rootNavigator: true).pop();
_actionDialogOpen = false;
}
}

View File

@@ -11,7 +11,8 @@ import '../styles/shifts_styles.dart';
class ShiftsPage extends StatefulWidget {
final String? initialTab;
const ShiftsPage({super.key, this.initialTab});
final DateTime? selectedDate;
const ShiftsPage({super.key, this.initialTab, this.selectedDate});
@override
State<ShiftsPage> createState() => _ShiftsPageState();
@@ -19,13 +20,18 @@ class ShiftsPage extends StatefulWidget {
class _ShiftsPageState extends State<ShiftsPage> {
late String _activeTab;
DateTime? _selectedDate;
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
@override
void initState() {
super.initState();
_activeTab = widget.initialTab ?? 'myshifts';
_selectedDate = widget.selectedDate;
_bloc.add(LoadShiftsEvent());
if (_activeTab == 'history') {
_bloc.add(LoadHistoryShiftsEvent());
}
}
@override
@@ -36,6 +42,11 @@ class _ShiftsPageState extends State<ShiftsPage> {
_activeTab = widget.initialTab!;
});
}
if (widget.selectedDate != null && widget.selectedDate != _selectedDate) {
setState(() {
_selectedDate = widget.selectedDate;
});
}
}
@override
@@ -59,6 +70,12 @@ class _ShiftsPageState extends State<ShiftsPage> {
final List<Shift> historyShifts = (state is ShiftsLoaded)
? state.historyShifts
: [];
final bool historyLoading = (state is ShiftsLoaded)
? state.historyLoading
: false;
final bool historyLoaded = (state is ShiftsLoaded)
? state.historyLoaded
: false;
// Note: "filteredJobs" logic moved to FindShiftsTab
// Note: Calendar logic moved to MyShiftsTab
@@ -103,7 +120,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
"find",
"Find Shifts",
UiIcons.search,
availableJobs.length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs.
availableJobs
.length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs.
),
const SizedBox(width: 8),
_buildTab(
@@ -111,6 +129,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
"History",
UiIcons.clock,
historyShifts.length,
showCount: historyLoaded,
),
],
),
@@ -124,11 +143,12 @@ class _ShiftsPageState extends State<ShiftsPage> {
? const Center(child: CircularProgressIndicator())
: _buildTabContent(
myShifts,
pendingAssignments,
cancelledShifts,
availableJobs,
historyShifts,
),
pendingAssignments,
cancelledShifts,
availableJobs,
historyShifts,
historyLoading,
),
),
],
),
@@ -144,6 +164,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
List<Shift> cancelledShifts,
List<Shift> availableJobs,
List<Shift> historyShifts,
bool historyLoading,
) {
switch (_activeTab) {
case 'myshifts':
@@ -151,25 +172,36 @@ class _ShiftsPageState extends State<ShiftsPage> {
myShifts: myShifts,
pendingAssignments: pendingAssignments,
cancelledShifts: cancelledShifts,
initialDate: _selectedDate,
);
case 'find':
return FindShiftsTab(
availableJobs: availableJobs,
);
return FindShiftsTab(availableJobs: availableJobs);
case 'history':
return HistoryShiftsTab(
historyShifts: historyShifts,
);
if (historyLoading) {
return const Center(child: CircularProgressIndicator());
}
return HistoryShiftsTab(historyShifts: historyShifts);
default:
return const SizedBox.shrink();
}
}
Widget _buildTab(String id, String label, IconData icon, int count) {
Widget _buildTab(
String id,
String label,
IconData icon,
int count, {
bool showCount = true,
}) {
final isActive = _activeTab == id;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _activeTab = id),
onTap: () {
setState(() => _activeTab = id);
if (id == 'history') {
_bloc.add(LoadHistoryShiftsEvent());
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
@@ -199,27 +231,32 @@ class _ShiftsPageState extends State<ShiftsPage> {
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, 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),
),
child: Center(
child: Text(
"$count",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: isActive ? AppColors.krowBlue : Colors.white,
if (showCount) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
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),
),
child: Center(
child: Text(
"$count",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: isActive ? AppColors.krowBlue : Colors.white,
),
),
),
),
),
],
],
),
),

View File

@@ -175,7 +175,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
),
),
),
const SizedBox(height: 40),
const SizedBox(height: UiConstants.space32),
],
),
),

View File

@@ -40,7 +40,7 @@ class HistoryShiftsTab extends StatelessWidget {
),
),
),
const SizedBox(height: 40),
const SizedBox(height: UiConstants.space32),
],
),
);

Some files were not shown because too many files have changed in this diff Show More