Merge branch '312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation' into fix_staff_app_bugs
This commit is contained in:
10
BLOCKERS.md
10
BLOCKERS.md
@@ -46,4 +46,12 @@
|
|||||||
- Staff APP:
|
- Staff APP:
|
||||||
- On app launch, check whether there is an active session. If a valid session exists, skip the auth flow and navigate directly to Home, loading Staff account.
|
- On app launch, check whether there is an active session. If a valid session exists, skip the auth flow and navigate directly to Home, loading Staff account.
|
||||||
- Add an expiration time (TTL) to the session (store expiresAt / expiryTimestamp) and invalidate/clear the session when it has expired.
|
- Add an expiration time (TTL) to the session (store expiresAt / expiryTimestamp) and invalidate/clear the session when it has expired.
|
||||||
- For staffs Skills = Roles? thinking in the future for the smart assigned that need to know the roles of staff to assign.
|
- For staffs Skills = Roles? thinking in the future for the smart assigned that need to know the roles of staff to assign.
|
||||||
|
|
||||||
|
## App
|
||||||
|
- Staff Application
|
||||||
|
|
||||||
|
### Github issue
|
||||||
|
- https://github.com/Oloodi/krow-workforce/issues/248
|
||||||
|
### Deveations:
|
||||||
|
- Assumed that a worker can only have one shift per day.
|
||||||
|
|||||||
@@ -5,42 +5,6 @@
|
|||||||
"storage_bucket": "krow-workforce-dev.firebasestorage.app"
|
"storage_bucket": "krow-workforce-dev.firebasestorage.app"
|
||||||
},
|
},
|
||||||
"client": [
|
"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": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db",
|
"mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db",
|
||||||
@@ -67,10 +31,10 @@
|
|||||||
"client_type": 3
|
"client_type": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
|
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
|
||||||
"client_type": 2,
|
"client_type": 2,
|
||||||
"ios_info": {
|
"ios_info": {
|
||||||
"bundle_id": "com.krow.app.staff.dev"
|
"bundle_id": "com.krowwithus.staff"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -103,10 +67,10 @@
|
|||||||
"client_type": 3
|
"client_type": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
|
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
|
||||||
"client_type": 2,
|
"client_type": 2,
|
||||||
"ios_info": {
|
"ios_info": {
|
||||||
"bundle_id": "com.krow.app.staff.dev"
|
"bundle_id": "com.krowwithus.staff"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -139,10 +103,10 @@
|
|||||||
"client_type": 3
|
"client_type": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
|
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
|
||||||
"client_type": 2,
|
"client_type": 2,
|
||||||
"ios_info": {
|
"ios_info": {
|
||||||
"bundle_id": "com.krow.app.staff.dev"
|
"bundle_id": "com.krowwithus.staff"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -151,12 +115,20 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_info": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:933560802882:android:d26bde4ee337b0b17757db",
|
"mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db",
|
||||||
"android_client_info": {
|
"android_client_info": {
|
||||||
"package_name": "com.krowwithus.krow_workforce.dev"
|
"package_name": "com.krowwithus.staff"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"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_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
@@ -175,10 +147,10 @@
|
|||||||
"client_type": 3
|
"client_type": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
|
"client_id": "933560802882-29olj9ku64jbe9h7flinha6hbi8qrluh.apps.googleusercontent.com",
|
||||||
"client_type": 2,
|
"client_type": 2,
|
||||||
"ios_info": {
|
"ios_info": {
|
||||||
"bundle_id": "com.krow.app.staff.dev"
|
"bundle_id": "com.krowwithus.staff"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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 */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
221E00B70DE845BE3D50D0A0 /* GoogleService-Info.plist */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -216,6 +219,7 @@
|
|||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
E8C1A28BFABAEE32FB779C9A /* GoogleService-Info.plist in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
36
apps/mobile/apps/client/ios/Runner/GoogleService-Info.plist
Normal file
36
apps/mobile/apps/client/ios/Runner/GoogleService-Info.plist
Normal 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>
|
||||||
74
apps/mobile/apps/client/lib/firebase_options.dart
Normal file
74
apps/mobile/apps/client/lib/firebase_options.dart
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,11 +12,15 @@ import 'package:client_hubs/client_hubs.dart' as client_hubs;
|
|||||||
import 'package:client_create_order/client_create_order.dart'
|
import 'package:client_create_order/client_create_order.dart'
|
||||||
as client_create_order;
|
as client_create_order;
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'firebase_options.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp(
|
||||||
|
options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null,
|
||||||
|
);
|
||||||
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ dependencies:
|
|||||||
path: ../../packages/features/client/hubs
|
path: ../../packages/features/client/hubs
|
||||||
client_create_order:
|
client_create_order:
|
||||||
path: ../../packages/features/client/create_order
|
path: ../../packages/features/client/create_order
|
||||||
|
krow_core:
|
||||||
|
path: ../../packages/core
|
||||||
|
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_modular: ^6.3.2
|
flutter_modular: ^6.3.2
|
||||||
|
|||||||
@@ -33,6 +33,29 @@
|
|||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,42 +5,6 @@
|
|||||||
"storage_bucket": "krow-workforce-dev.firebasestorage.app"
|
"storage_bucket": "krow-workforce-dev.firebasestorage.app"
|
||||||
},
|
},
|
||||||
"client": [
|
"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": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db",
|
"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": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db",
|
"mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
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 */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -94,6 +96,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
9F0B07DEC91B141354438F79 /* GoogleService-Info.plist */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -216,6 +219,7 @@
|
|||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
1E967D034ADA3A16EF82CB3E /* GoogleService-Info.plist in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
36
apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist
Normal file
36
apps/mobile/apps/staff/ios/Runner/GoogleService-Info.plist
Normal 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>
|
||||||
74
apps/mobile/apps/staff/lib/firebase_options.dart
Normal file
74
apps/mobile/apps/staff/lib/firebase_options.dart
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krowwithus_staff/firebase_options.dart';
|
||||||
import 'package:staff_authentication/staff_authentication.dart'
|
import 'package:staff_authentication/staff_authentication.dart'
|
||||||
as staff_authentication;
|
as staff_authentication;
|
||||||
import 'package:staff_main/staff_main.dart' as staff_main;
|
import 'package:staff_main/staff_main.dart' as staff_main;
|
||||||
@@ -12,7 +13,9 @@ import 'package:krow_core/core.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp(
|
||||||
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
);
|
||||||
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.8
|
# Architecture Packages
|
||||||
flutter_modular: ^6.3.0
|
|
||||||
|
|
||||||
# Architecture Packages
|
|
||||||
design_system:
|
design_system:
|
||||||
path: ../../packages/design_system
|
path: ../../packages/design_system
|
||||||
core_localization:
|
core_localization:
|
||||||
@@ -27,6 +24,14 @@ dependencies:
|
|||||||
path: ../../packages/features/staff/availability
|
path: ../../packages/features/staff/availability
|
||||||
staff_clock_in:
|
staff_clock_in:
|
||||||
path: ../../packages/features/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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -34,5 +34,28 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// A wrapper widget that renders the application inside an iPhone-like frame
|
/// A wrapper widget that renders the application inside an iPhone-like frame
|
||||||
/// specifically for Flutter Web. On other platforms, it simply returns the child.
|
/// specifically for Flutter Web. On other platforms, it simply returns the child.
|
||||||
class WebMobileFrame extends StatelessWidget {
|
class WebMobileFrame extends StatelessWidget {
|
||||||
final Widget child;
|
|
||||||
final Widget logo;
|
|
||||||
final String appName;
|
|
||||||
|
|
||||||
const WebMobileFrame({
|
const WebMobileFrame({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
@@ -15,6 +13,10 @@ class WebMobileFrame extends StatelessWidget {
|
|||||||
required this.appName,
|
required this.appName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final Widget logo;
|
||||||
|
final String appName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!kIsWeb) return child;
|
if (!kIsWeb) return child;
|
||||||
@@ -22,26 +24,22 @@ class WebMobileFrame extends StatelessWidget {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData.dark(),
|
theme: ThemeData.dark(),
|
||||||
home: _WebFrameContent(
|
home: _WebFrameContent(logo: logo, appName: appName, child: child),
|
||||||
logo: logo,
|
|
||||||
appName: appName,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WebFrameContent extends StatefulWidget {
|
class _WebFrameContent extends StatefulWidget {
|
||||||
final Widget child;
|
|
||||||
final Widget logo;
|
|
||||||
final String appName;
|
|
||||||
|
|
||||||
const _WebFrameContent({
|
const _WebFrameContent({
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.logo,
|
required this.logo,
|
||||||
required this.appName,
|
required this.appName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final Widget logo;
|
||||||
|
final String appName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_WebFrameContent> createState() => _WebFrameContentState();
|
State<_WebFrameContent> createState() => _WebFrameContentState();
|
||||||
}
|
}
|
||||||
@@ -61,10 +59,10 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
const double borderThickness = 12.0;
|
const double borderThickness = 12.0;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF121212),
|
backgroundColor: UiColors.foreground,
|
||||||
body: MouseRegion(
|
body: MouseRegion(
|
||||||
cursor: SystemMouseCursors.none,
|
cursor: SystemMouseCursors.none,
|
||||||
onHover: (event) {
|
onHover: (PointerHoverEvent event) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_cursorPosition = event.position;
|
_cursorPosition = event.position;
|
||||||
_isHovering = true;
|
_isHovering = true;
|
||||||
@@ -72,7 +70,7 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
},
|
},
|
||||||
onExit: (_) => setState(() => _isHovering = false),
|
onExit: (_) => setState(() => _isHovering = false),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: <Widget>[
|
||||||
// Logo and Title on the left (Web only)
|
// Logo and Title on the left (Web only)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 60,
|
left: 60,
|
||||||
@@ -84,28 +82,21 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: <Widget>[
|
||||||
SizedBox(
|
SizedBox(width: 140, child: widget.logo),
|
||||||
width: 140,
|
|
||||||
child: widget.logo,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
widget.appName,
|
widget.appName,
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
style: const TextStyle(
|
style: UiTypography.display1b.copyWith(
|
||||||
color: Colors.white,
|
color: UiColors.white,
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: -0.5,
|
|
||||||
fontFamily: 'Instrument Sans', // Fallback if available or system
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Container(
|
Container(
|
||||||
height: 2,
|
height: 2,
|
||||||
width: 40,
|
width: 40,
|
||||||
color: Colors.white.withOpacity(0.3),
|
color: UiColors.white.withOpacity(0.3),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -116,11 +107,11 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
// Frame and Content
|
// Frame and Content
|
||||||
Center(
|
Center(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
// Scale down if screen is too small
|
// Scale down if screen is too small
|
||||||
double scaleX = constraints.maxWidth / (frameWidth + 80);
|
final double scaleX = constraints.maxWidth / (frameWidth - 150);
|
||||||
double scaleY = constraints.maxHeight / (frameHeight + 80);
|
final double scaleY = constraints.maxHeight / (frameHeight - 220);
|
||||||
double scale = (scaleX < 1 || scaleY < 1)
|
final double scale = (scaleX < 1 || scaleY < 1)
|
||||||
? (scaleX < scaleY ? scaleX : scaleY)
|
? (scaleX < scaleY ? scaleX : scaleY)
|
||||||
: 1.0;
|
: 1.0;
|
||||||
|
|
||||||
@@ -130,11 +121,11 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
width: frameWidth,
|
width: frameWidth,
|
||||||
height: frameHeight,
|
height: frameHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black,
|
color: UiColors.black,
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
boxShadow: [
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.6),
|
color: UiColors.black.withOpacity(0.6),
|
||||||
blurRadius: 40,
|
blurRadius: 40,
|
||||||
spreadRadius: 10,
|
spreadRadius: 10,
|
||||||
),
|
),
|
||||||
@@ -149,10 +140,10 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
borderRadius - borderThickness,
|
borderRadius - borderThickness,
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: <Widget>[
|
||||||
// The actual app + status bar
|
// The actual app + status bar
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: <Widget>[
|
||||||
// Mock iOS Status Bar
|
// Mock iOS Status Bar
|
||||||
Container(
|
Container(
|
||||||
height: 48,
|
height: 48,
|
||||||
@@ -160,26 +151,26 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
),
|
),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Color(0xFFF9F6EE),
|
color: UiColors.background,
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: Color(0xFFEEEEEE),
|
color: UiColors.border,
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: const Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceBetween,
|
MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: <Widget>[
|
||||||
// Time side
|
// Time side
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
child: Text(
|
child: Text(
|
||||||
'9:41 PM',
|
'9:41 PM',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black54,
|
color: UiColors.black,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: -0.2,
|
||||||
@@ -187,27 +178,27 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Status Icons side
|
// Status Icons side
|
||||||
const SizedBox(
|
SizedBox(
|
||||||
width: 80,
|
width: 80,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.end,
|
MainAxisAlignment.end,
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Icon(
|
Icon(
|
||||||
Icons.signal_cellular_alt,
|
Icons.signal_cellular_alt,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: Colors.black54,
|
color: UiColors.black,
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.wifi,
|
Icons.wifi,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: Colors.black54,
|
color: UiColors.black,
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.battery_full,
|
Icons.battery_full,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: Colors.black54,
|
color: UiColors.black,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -215,7 +206,7 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// The main app content content
|
// The main app content
|
||||||
Expanded(child: widget.child),
|
Expanded(child: widget.child),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -228,7 +219,7 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
height: 35,
|
height: 35,
|
||||||
margin: const EdgeInsets.only(top: 10),
|
margin: const EdgeInsets.only(top: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black,
|
color: UiColors.black,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -241,6 +232,29 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
design_system:
|
||||||
|
path: ../design_system
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class UiIcons {
|
|||||||
/// Checkmark icon
|
/// Checkmark icon
|
||||||
static const IconData check = _IconLib.check;
|
static const IconData check = _IconLib.check;
|
||||||
|
|
||||||
|
/// Checkmark circle icon
|
||||||
|
static const IconData checkCircle = _IconLib.checkCircle;
|
||||||
|
|
||||||
/// X/Cancel icon
|
/// X/Cancel icon
|
||||||
static const IconData close = _IconLib.x;
|
static const IconData close = _IconLib.x;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'ui_colors.dart';
|
import 'ui_colors.dart';
|
||||||
import 'ui_typography.dart';
|
|
||||||
import 'ui_constants.dart';
|
import 'ui_constants.dart';
|
||||||
|
import 'ui_typography.dart';
|
||||||
|
|
||||||
/// The main entry point for the Staff Design System theme.
|
/// The main entry point for the Staff Design System theme.
|
||||||
/// Assembles colors, typography, and constants into a comprehensive Material 3 theme.
|
/// Assembles colors, typography, and constants into a comprehensive Material 3 theme.
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
@@ -67,7 +67,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
}) : buttonBuilder = _elevatedButtonBuilder,
|
}) : buttonBuilder = _elevatedButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
@@ -85,7 +85,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
}) : buttonBuilder = _outlinedButtonBuilder,
|
}) : buttonBuilder = _outlinedButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
@@ -103,7 +103,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
}) : buttonBuilder = _textButtonBuilder,
|
}) : buttonBuilder = _textButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
@@ -121,7 +121,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
this.style,
|
this.style,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.medium,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
}) : buttonBuilder = _textButtonBuilder,
|
}) : buttonBuilder = _textButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
@@ -132,10 +132,14 @@ class UiButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
/// Builds the button UI.
|
/// Builds the button UI.
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ButtonStyle? mergedStyle = style != null
|
||||||
|
? _getSizeStyle().merge(style)
|
||||||
|
: _getSizeStyle();
|
||||||
|
|
||||||
final Widget button = buttonBuilder(
|
final Widget button = buttonBuilder(
|
||||||
context,
|
context,
|
||||||
onPressed,
|
onPressed,
|
||||||
style,
|
mergedStyle,
|
||||||
_buildButtonContent(),
|
_buildButtonContent(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,6 +150,65 @@ class UiButton extends StatelessWidget {
|
|||||||
return button;
|
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.
|
/// Builds the button content with optional leading and trailing icons.
|
||||||
Widget _buildButtonContent() {
|
Widget _buildButtonContent() {
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
@@ -229,6 +292,9 @@ class UiButton extends StatelessWidget {
|
|||||||
|
|
||||||
/// Defines the size of a [UiButton].
|
/// Defines the size of a [UiButton].
|
||||||
enum UiButtonSize {
|
enum UiButtonSize {
|
||||||
|
/// Extra small button (very compact)
|
||||||
|
extraSmall,
|
||||||
|
|
||||||
/// Small button (compact)
|
/// Small button (compact)
|
||||||
small,
|
small,
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ export 'src/adapters/clock_in/clock_in_adapter.dart';
|
|||||||
export 'src/entities/availability/availability_slot.dart';
|
export 'src/entities/availability/availability_slot.dart';
|
||||||
export 'src/entities/availability/day_availability.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
|
// Adapters
|
||||||
export 'src/adapters/profile/emergency_contact_adapter.dart';
|
export 'src/adapters/profile/emergency_contact_adapter.dart';
|
||||||
export 'src/adapters/profile/experience_adapter.dart';
|
export 'src/adapters/profile/experience_adapter.dart';
|
||||||
|
|||||||
@@ -1,10 +1,59 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
import '../../entities/shifts/shift.dart';
|
import '../../entities/shifts/shift.dart';
|
||||||
|
|
||||||
/// Adapter for Shift related data.
|
/// Adapter for Shift related data.
|
||||||
class ShiftAdapter {
|
class ShiftAdapter {
|
||||||
|
/// Maps application data to a Shift entity.
|
||||||
// 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,
|
/// This method handles the common mapping logic used across different
|
||||||
// we might put the logic in Repo or make this accept dynamic/Map if strictly required.
|
/// repositories when converting application data from Data Connect to
|
||||||
// For now, placeholders or simple status helpers.
|
/// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
@@ -200,6 +200,8 @@ class _BillingViewState extends State<BillingView> {
|
|||||||
const SpendingBreakdownCard(),
|
const SpendingBreakdownCard(),
|
||||||
if (state.invoiceHistory.isEmpty) _buildEmptyState(context)
|
if (state.invoiceHistory.isEmpty) _buildEmptyState(context)
|
||||||
else InvoiceHistorySection(invoices: state.invoiceHistory),
|
else InvoiceHistorySection(invoices: state.invoiceHistory),
|
||||||
|
|
||||||
|
const SizedBox(height: UiConstants.space32),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
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/repositories/coverage_repository.dart';
|
||||||
import '../../domain/ui_entities/coverage_entities.dart';
|
|
||||||
|
|
||||||
/// Implementation of [CoverageRepository] in the Data layer.
|
/// Implementation of [CoverageRepository] in the Data layer.
|
||||||
///
|
///
|
||||||
@@ -25,18 +25,14 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
|||||||
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
|
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
|
||||||
final String? businessId =
|
final String? businessId =
|
||||||
dc.ClientSessionStore.instance.session?.business?.id;
|
dc.ClientSessionStore.instance.session?.business?.id;
|
||||||
print('Coverage: now=${DateTime.now().toIso8601String()}');
|
|
||||||
if (businessId == null || businessId.isEmpty) {
|
if (businessId == null || businessId.isEmpty) {
|
||||||
print('Coverage: missing businessId for date=${date.toIso8601String()}');
|
|
||||||
return <CoverageShift>[];
|
return <CoverageShift>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||||
final DateTime end =
|
final DateTime end =
|
||||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
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<
|
final fdc.QueryResult<
|
||||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
||||||
@@ -58,9 +54,6 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
|||||||
dayEnd: _toTimestamp(end),
|
dayEnd: _toTimestamp(end),
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
print(
|
|
||||||
'Coverage: ${date.toIso8601String()} staffsApplications=${applicationsResult.data.applications.length}',
|
|
||||||
);
|
|
||||||
|
|
||||||
return _mapCoverageShifts(
|
return _mapCoverageShifts(
|
||||||
shiftRolesResult.data.shiftRoles,
|
shiftRolesResult.data.shiftRoles,
|
||||||
@@ -84,11 +77,16 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
|||||||
final List<CoverageWorker> allWorkers =
|
final List<CoverageWorker> allWorkers =
|
||||||
shifts.expand((CoverageShift shift) => shift.workers).toList();
|
shifts.expand((CoverageShift shift) => shift.workers).toList();
|
||||||
final int totalConfirmed = allWorkers.length;
|
final int totalConfirmed = allWorkers.length;
|
||||||
final int checkedIn =
|
final int checkedIn = allWorkers
|
||||||
allWorkers.where((CoverageWorker w) => w.isCheckedIn).length;
|
.where((CoverageWorker w) => w.status == CoverageWorkerStatus.checkedIn)
|
||||||
final int enRoute =
|
.length;
|
||||||
allWorkers.where((CoverageWorker w) => w.isEnRoute).length;
|
final int enRoute = allWorkers
|
||||||
final int late = allWorkers.where((CoverageWorker w) => w.isLate).length;
|
.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(
|
return CoverageStats(
|
||||||
totalNeeded: totalNeeded,
|
totalNeeded: totalNeeded,
|
||||||
@@ -172,25 +170,32 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _mapWorkerStatus(
|
CoverageWorkerStatus _mapWorkerStatus(
|
||||||
dc.EnumValue<dc.ApplicationStatus> status,
|
dc.EnumValue<dc.ApplicationStatus> status,
|
||||||
) {
|
) {
|
||||||
if (status is dc.Known<dc.ApplicationStatus>) {
|
if (status is dc.Known<dc.ApplicationStatus>) {
|
||||||
switch (status.value) {
|
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:
|
case dc.ApplicationStatus.PENDING:
|
||||||
|
return CoverageWorkerStatus.pending;
|
||||||
|
case dc.ApplicationStatus.ACCEPTED:
|
||||||
|
return CoverageWorkerStatus.confirmed;
|
||||||
case dc.ApplicationStatus.REJECTED:
|
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:
|
case dc.ApplicationStatus.NO_SHOW:
|
||||||
|
return CoverageWorkerStatus.noShow;
|
||||||
case dc.ApplicationStatus.COMPLETED:
|
case dc.ApplicationStatus.COMPLETED:
|
||||||
return 'confirmed';
|
return CoverageWorkerStatus.completed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 'confirmed';
|
return CoverageWorkerStatus.pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _formatTime(fdc.Timestamp? timestamp) {
|
String? _formatTime(fdc.Timestamp? timestamp) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import '../ui_entities/coverage_entities.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Repository interface for coverage-related operations.
|
/// Repository interface for coverage-related operations.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
|
||||||
|
|
||||||
/// Domain entity representing a shift in the coverage view.
|
|
||||||
///
|
|
||||||
/// This is a feature-specific domain entity that encapsulates shift information
|
|
||||||
/// including scheduling details and assigned workers.
|
|
||||||
class CoverageShift extends Equatable {
|
|
||||||
/// Creates a [CoverageShift].
|
|
||||||
const CoverageShift({
|
|
||||||
required this.id,
|
|
||||||
required this.title,
|
|
||||||
required this.location,
|
|
||||||
required this.startTime,
|
|
||||||
required this.workersNeeded,
|
|
||||||
required this.date,
|
|
||||||
required this.workers,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The unique identifier for the shift.
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
/// The title or role of the shift.
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
/// The location where the shift takes place.
|
|
||||||
final String location;
|
|
||||||
|
|
||||||
/// The start time of the shift (e.g., "16:00").
|
|
||||||
final String startTime;
|
|
||||||
|
|
||||||
/// The number of workers needed for this shift.
|
|
||||||
final int workersNeeded;
|
|
||||||
|
|
||||||
/// The date of the shift.
|
|
||||||
final DateTime date;
|
|
||||||
|
|
||||||
/// The list of workers assigned to this shift.
|
|
||||||
final List<CoverageWorker> workers;
|
|
||||||
|
|
||||||
/// Calculates the coverage percentage for this shift.
|
|
||||||
int get coveragePercent {
|
|
||||||
if (workersNeeded == 0) return 0;
|
|
||||||
return ((workers.length / workersNeeded) * 100).round();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
location,
|
|
||||||
startTime,
|
|
||||||
workersNeeded,
|
|
||||||
date,
|
|
||||||
workers,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Domain entity representing a worker in the coverage view.
|
|
||||||
///
|
|
||||||
/// This entity tracks worker status including check-in information.
|
|
||||||
class CoverageWorker extends Equatable {
|
|
||||||
/// Creates a [CoverageWorker].
|
|
||||||
const CoverageWorker({
|
|
||||||
required this.name,
|
|
||||||
required this.status,
|
|
||||||
this.checkInTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The name of the worker.
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
/// The status of the worker ('confirmed', 'late', etc.).
|
|
||||||
final String status;
|
|
||||||
|
|
||||||
/// The time the worker checked in, if applicable.
|
|
||||||
final String? checkInTime;
|
|
||||||
|
|
||||||
/// Returns true if the worker is checked in.
|
|
||||||
bool get isCheckedIn => status == 'confirmed' && checkInTime != null;
|
|
||||||
|
|
||||||
/// Returns true if the worker is en route.
|
|
||||||
bool get isEnRoute => status == 'confirmed' && checkInTime == null;
|
|
||||||
|
|
||||||
/// Returns true if the worker is late.
|
|
||||||
bool get isLate => status == 'late';
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[name, status, checkInTime];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Domain entity representing coverage statistics.
|
|
||||||
///
|
|
||||||
/// Aggregates coverage metrics for a specific date.
|
|
||||||
class CoverageStats extends Equatable {
|
|
||||||
/// Creates a [CoverageStats].
|
|
||||||
const CoverageStats({
|
|
||||||
required this.totalNeeded,
|
|
||||||
required this.totalConfirmed,
|
|
||||||
required this.checkedIn,
|
|
||||||
required this.enRoute,
|
|
||||||
required this.late,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The total number of workers needed.
|
|
||||||
final int totalNeeded;
|
|
||||||
|
|
||||||
/// The total number of confirmed workers.
|
|
||||||
final int totalConfirmed;
|
|
||||||
|
|
||||||
/// The number of workers who have checked in.
|
|
||||||
final int checkedIn;
|
|
||||||
|
|
||||||
/// The number of workers en route.
|
|
||||||
final int enRoute;
|
|
||||||
|
|
||||||
/// The number of late workers.
|
|
||||||
final int late;
|
|
||||||
|
|
||||||
/// Calculates the overall coverage percentage.
|
|
||||||
int get coveragePercent {
|
|
||||||
if (totalNeeded == 0) return 0;
|
|
||||||
return ((totalConfirmed / totalNeeded) * 100).round();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[
|
|
||||||
totalNeeded,
|
|
||||||
totalConfirmed,
|
|
||||||
checkedIn,
|
|
||||||
enRoute,
|
|
||||||
late,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../arguments/get_coverage_stats_arguments.dart';
|
import '../arguments/get_coverage_stats_arguments.dart';
|
||||||
import '../repositories/coverage_repository.dart';
|
import '../repositories/coverage_repository.dart';
|
||||||
import '../ui_entities/coverage_entities.dart';
|
|
||||||
|
|
||||||
/// Use case for fetching coverage statistics for a specific date.
|
/// Use case for fetching coverage statistics for a specific date.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import '../arguments/get_shifts_for_date_arguments.dart';
|
import '../arguments/get_shifts_for_date_arguments.dart';
|
||||||
import '../repositories/coverage_repository.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.
|
/// Use case for fetching shifts for a specific date.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../domain/arguments/get_coverage_stats_arguments.dart';
|
import '../../domain/arguments/get_coverage_stats_arguments.dart';
|
||||||
import '../../domain/arguments/get_shifts_for_date_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_coverage_stats_usecase.dart';
|
||||||
import '../../domain/usecases/get_shifts_for_date_usecase.dart';
|
import '../../domain/usecases/get_shifts_for_date_usecase.dart';
|
||||||
import 'coverage_event.dart';
|
import 'coverage_event.dart';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
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 representing the status of coverage data loading.
|
||||||
enum CoverageStatus {
|
enum CoverageStatus {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.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.
|
/// Quick statistics cards showing coverage metrics.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.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.
|
/// List of shifts with their workers.
|
||||||
///
|
///
|
||||||
@@ -194,7 +194,8 @@ class _ShiftHeader extends StatelessWidget {
|
|||||||
size: UiConstants.space3,
|
size: UiConstants.space3,
|
||||||
color: UiColors.iconSecondary,
|
color: UiColors.iconSecondary,
|
||||||
),
|
),
|
||||||
Expanded(child: Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
location,
|
location,
|
||||||
style: UiTypography.body3r.textSecondary,
|
style: UiTypography.body3r.textSecondary,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -314,36 +315,92 @@ class _WorkerRow extends StatelessWidget {
|
|||||||
Color badgeText;
|
Color badgeText;
|
||||||
String badgeLabel;
|
String badgeLabel;
|
||||||
|
|
||||||
if (worker.isCheckedIn) {
|
switch (worker.status) {
|
||||||
bg = UiColors.textSuccess.withOpacity(0.1);
|
case CoverageWorkerStatus.checkedIn:
|
||||||
border = UiColors.textSuccess;
|
bg = UiColors.textSuccess.withOpacity(0.1);
|
||||||
textBg = UiColors.textSuccess.withOpacity(0.2);
|
border = UiColors.textSuccess;
|
||||||
textColor = UiColors.textSuccess;
|
textBg = UiColors.textSuccess.withOpacity(0.2);
|
||||||
icon = UiIcons.success;
|
textColor = UiColors.textSuccess;
|
||||||
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
|
icon = UiIcons.success;
|
||||||
badgeBg = UiColors.textSuccess;
|
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
|
||||||
badgeText = UiColors.primaryForeground;
|
badgeBg = UiColors.textSuccess;
|
||||||
badgeLabel = 'On Site';
|
badgeText = UiColors.primaryForeground;
|
||||||
} else if (worker.isEnRoute) {
|
badgeLabel = 'On Site';
|
||||||
bg = UiColors.textWarning.withOpacity(0.1);
|
case CoverageWorkerStatus.confirmed:
|
||||||
border = UiColors.textWarning;
|
if (worker.checkInTime == null) {
|
||||||
textBg = UiColors.textWarning.withOpacity(0.2);
|
bg = UiColors.textWarning.withOpacity(0.1);
|
||||||
textColor = UiColors.textWarning;
|
border = UiColors.textWarning;
|
||||||
icon = UiIcons.clock;
|
textBg = UiColors.textWarning.withOpacity(0.2);
|
||||||
statusText = 'En Route - Expected $shiftStartTime';
|
textColor = UiColors.textWarning;
|
||||||
badgeBg = UiColors.textWarning;
|
icon = UiIcons.clock;
|
||||||
badgeText = UiColors.primaryForeground;
|
statusText = 'En Route - Expected $shiftStartTime';
|
||||||
badgeLabel = 'En Route';
|
badgeBg = UiColors.textWarning;
|
||||||
} else {
|
badgeText = UiColors.primaryForeground;
|
||||||
bg = UiColors.destructive.withOpacity(0.1);
|
badgeLabel = 'En Route';
|
||||||
border = UiColors.destructive;
|
} else {
|
||||||
textBg = UiColors.destructive.withOpacity(0.2);
|
bg = UiColors.muted.withOpacity(0.1);
|
||||||
textColor = UiColors.destructive;
|
border = UiColors.border;
|
||||||
icon = UiIcons.warning;
|
textBg = UiColors.muted.withOpacity(0.2);
|
||||||
statusText = '⚠ Running Late';
|
textColor = UiColors.textSecondary;
|
||||||
badgeBg = UiColors.destructive;
|
icon = UiIcons.success;
|
||||||
badgeText = UiColors.destructiveForeground;
|
statusText = 'Confirmed';
|
||||||
badgeLabel = 'Late';
|
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(
|
return Container(
|
||||||
|
|||||||
@@ -43,14 +43,11 @@ class ReorderWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (subtitle != null) ...<Widget>[
|
if (subtitle != null) ...<Widget>[
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Text(
|
Text(subtitle!, style: UiTypography.body2r.textSecondary),
|
||||||
subtitle!,
|
|
||||||
style: UiTypography.body2r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 140,
|
height: 164,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: recentOrders.length,
|
itemCount: recentOrders.length,
|
||||||
@@ -67,13 +64,7 @@ class ReorderWidget extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border, width: 0.6),
|
||||||
boxShadow: <BoxShadow>[
|
|
||||||
BoxShadow(
|
|
||||||
color: UiColors.black.withValues(alpha: 0.02),
|
|
||||||
blurRadius: 4,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -129,10 +120,7 @@ class ReorderWidget extends StatelessWidget {
|
|||||||
style: UiTypography.body1b,
|
style: UiTypography.body1b,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
i18n.per_hr(
|
'${i18n.per_hr(amount: order.hourlyRate.toString())} · ${order.hours}h',
|
||||||
amount: order.hourlyRate.toString(),
|
|
||||||
) +
|
|
||||||
' · ${order.hours}h',
|
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -145,49 +133,37 @@ class ReorderWidget extends StatelessWidget {
|
|||||||
_Badge(
|
_Badge(
|
||||||
icon: UiIcons.success,
|
icon: UiIcons.success,
|
||||||
text: order.type,
|
text: order.type,
|
||||||
color: const Color(0xFF2563EB),
|
color: UiColors.primary,
|
||||||
bg: const Color(0xFF2563EB),
|
bg: UiColors.buttonSecondaryStill,
|
||||||
textColor: UiColors.white,
|
textColor: UiColors.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
_Badge(
|
_Badge(
|
||||||
icon: UiIcons.building,
|
icon: UiIcons.building,
|
||||||
text: '${order.workers}',
|
text: '${order.workers}',
|
||||||
color: const Color(0xFF334155),
|
color: UiColors.textSecondary,
|
||||||
bg: const Color(0xFFF1F5F9),
|
bg: UiColors.buttonSecondaryStill,
|
||||||
textColor: const Color(0xFF334155),
|
textColor: UiColors.textSecondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
SizedBox(
|
|
||||||
height: 28,
|
UiButton.secondary(
|
||||||
width: double.infinity,
|
size: UiButtonSize.small,
|
||||||
child: ElevatedButton.icon(
|
text: i18n.reorder_button,
|
||||||
onPressed: () => onReorderPressed(<String, dynamic>{
|
leadingIcon: UiIcons.zap,
|
||||||
'orderId': order.orderId,
|
iconSize: 12,
|
||||||
'title': order.title,
|
fullWidth: true,
|
||||||
'location': order.location,
|
onPressed: () => onReorderPressed(<String, dynamic>{
|
||||||
'hourlyRate': order.hourlyRate,
|
'orderId': order.orderId,
|
||||||
'hours': order.hours,
|
'title': order.title,
|
||||||
'workers': order.workers,
|
'location': order.location,
|
||||||
'type': order.type,
|
'hourlyRate': order.hourlyRate,
|
||||||
}),
|
'hours': order.hours,
|
||||||
style: ElevatedButton.styleFrom(
|
'workers': order.workers,
|
||||||
backgroundColor: UiColors.primary,
|
'type': order.type,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class SettingsProfileHeader extends StatelessWidget {
|
|||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
background: Container(
|
background: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
|
||||||
margin: const EdgeInsets.only(top: UiConstants.space16),
|
margin: const EdgeInsets.only(top: UiConstants.space24),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:ui';
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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 '../blocs/view_orders_state.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../widgets/view_order_card.dart';
|
import '../widgets/view_order_card.dart';
|
||||||
|
import '../widgets/view_orders_header.dart';
|
||||||
import '../navigation/view_orders_navigator.dart';
|
import '../navigation/view_orders_navigator.dart';
|
||||||
|
|
||||||
/// The main page for viewing client orders.
|
/// The main page for viewing client orders.
|
||||||
@@ -22,6 +22,7 @@ class ViewOrdersPage extends StatelessWidget {
|
|||||||
/// Creates a [ViewOrdersPage].
|
/// Creates a [ViewOrdersPage].
|
||||||
const ViewOrdersPage({super.key, this.initialDate});
|
const ViewOrdersPage({super.key, this.initialDate});
|
||||||
|
|
||||||
|
/// The initial date to display orders for.
|
||||||
final DateTime? initialDate;
|
final DateTime? initialDate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -37,7 +38,8 @@ class ViewOrdersPage extends StatelessWidget {
|
|||||||
class ViewOrdersView extends StatefulWidget {
|
class ViewOrdersView extends StatefulWidget {
|
||||||
/// Creates a [ViewOrdersView].
|
/// Creates a [ViewOrdersView].
|
||||||
const ViewOrdersView({super.key, this.initialDate});
|
const ViewOrdersView({super.key, this.initialDate});
|
||||||
|
|
||||||
|
/// The initial date to display orders for.
|
||||||
final DateTime? initialDate;
|
final DateTime? initialDate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -88,376 +90,84 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: SafeArea(
|
||||||
children: <Widget>[
|
child: Column(
|
||||||
// Background Gradient
|
children: <Widget>[
|
||||||
Container(
|
// Header + Filter + Calendar (Sticky behavior)
|
||||||
decoration: const BoxDecoration(
|
ViewOrdersHeader(
|
||||||
gradient: LinearGradient(
|
state: state,
|
||||||
begin: Alignment.topCenter,
|
calendarDays: calendarDays,
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: <Color>[UiColors.bgSecondary, UiColors.white],
|
|
||||||
stops: <double>[0.0, 0.3],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
// Content List
|
||||||
SafeArea(
|
Expanded(
|
||||||
child: Column(
|
child: filteredOrders.isEmpty
|
||||||
children: <Widget>[
|
? _buildEmptyState(context: context, state: state)
|
||||||
// Header + Filter + Calendar (Sticky behavior)
|
: ListView(
|
||||||
_buildHeader(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
context: context,
|
UiConstants.space5,
|
||||||
state: state,
|
UiConstants.space4,
|
||||||
calendarDays: calendarDays,
|
UiConstants.space5,
|
||||||
),
|
100,
|
||||||
|
),
|
||||||
// Content List
|
children: <Widget>[
|
||||||
Expanded(
|
if (filteredOrders.isNotEmpty)
|
||||||
child: filteredOrders.isEmpty
|
Padding(
|
||||||
? _buildEmptyState(context: context, state: state)
|
padding: const EdgeInsets.only(
|
||||||
: ListView(
|
bottom: UiConstants.space3,
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
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.
|
/// Builds the empty state view.
|
||||||
Widget _buildEmptyState({
|
Widget _buildEmptyState({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../blocs/view_orders_cubit.dart';
|
import '../blocs/view_orders_cubit.dart';
|
||||||
|
|
||||||
/// A rich card displaying details of a client order/shift.
|
/// A rich card displaying details of a client order/shift.
|
||||||
@@ -325,15 +326,23 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (order.workersNeeded != 0)
|
if (coveragePercent != 100)
|
||||||
const Icon(
|
const Icon(
|
||||||
UiIcons.error,
|
UiIcons.error,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: UiColors.textError,
|
color: UiColors.textError,
|
||||||
),
|
),
|
||||||
|
if (coveragePercent == 100)
|
||||||
|
const Icon(
|
||||||
|
UiIcons.checkCircle,
|
||||||
|
size: 16,
|
||||||
|
color: UiColors.textSuccess,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'${order.workersNeeded} Workers Needed',
|
coveragePercent == 100
|
||||||
|
? 'All Workers Confirmed'
|
||||||
|
: '${order.workersNeeded} Workers Needed',
|
||||||
style: UiTypography.body2m.textPrimary,
|
style: UiTypography.body2m.textPrimary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../blocs/view_orders_cubit.dart';
|
||||||
|
|
||||||
|
/// A single filter tab for the View Orders page.
|
||||||
|
///
|
||||||
|
/// Displays a label with an optional count and shows a selection indicator
|
||||||
|
/// when the tab is active.
|
||||||
|
class ViewOrdersFilterTab extends StatelessWidget {
|
||||||
|
/// Creates a [ViewOrdersFilterTab].
|
||||||
|
const ViewOrdersFilterTab({
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.tabId,
|
||||||
|
this.count,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label text to display.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// Whether this tab is currently selected.
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
/// The unique identifier for this tab.
|
||||||
|
final String tabId;
|
||||||
|
|
||||||
|
/// Optional count to display next to the label.
|
||||||
|
final int? count;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
String text = label;
|
||||||
|
if (count != null) {
|
||||||
|
text = '$label ($count)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () =>
|
||||||
|
BlocProvider.of<ViewOrdersCubit>(context).selectFilterTab(tabId),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.textSecondary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
height: 2,
|
||||||
|
width: isSelected ? 40 : 0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isSelected) const SizedBox(height: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../blocs/view_orders_cubit.dart';
|
||||||
|
import '../blocs/view_orders_state.dart';
|
||||||
|
import '../navigation/view_orders_navigator.dart';
|
||||||
|
import 'view_orders_filter_tab.dart';
|
||||||
|
|
||||||
|
/// The sticky header section for the View Orders page.
|
||||||
|
///
|
||||||
|
/// This widget contains:
|
||||||
|
/// - Top bar with title and post button
|
||||||
|
/// - Filter tabs (Up Next, Active, Completed)
|
||||||
|
/// - Calendar navigation controls
|
||||||
|
/// - Horizontal calendar grid
|
||||||
|
class ViewOrdersHeader extends StatelessWidget {
|
||||||
|
/// Creates a [ViewOrdersHeader].
|
||||||
|
const ViewOrdersHeader({
|
||||||
|
required this.state,
|
||||||
|
required this.calendarDays,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The current state of the view orders feature.
|
||||||
|
final ViewOrdersState state;
|
||||||
|
|
||||||
|
/// The list of calendar days to display.
|
||||||
|
final List<DateTime> calendarDays;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xCCFFFFFF), // White with 0.8 alpha
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(color: UiColors.separatorSecondary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
// Top Bar
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space3,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.client_view_orders.title,
|
||||||
|
style: UiTypography.headline3m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.filteredOrders.isNotEmpty)
|
||||||
|
UiButton.primary(
|
||||||
|
text: t.client_view_orders.post_button,
|
||||||
|
leadingIcon: UiIcons.add,
|
||||||
|
onPressed: () => Modular.to.navigateToCreateOrder(),
|
||||||
|
size: UiButtonSize.small,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(0, 48),
|
||||||
|
maximumSize: const Size(0, 48),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Filter Tabs
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ViewOrdersFilterTab(
|
||||||
|
label: t.client_view_orders.tabs.up_next,
|
||||||
|
isSelected: state.filterTab == 'all',
|
||||||
|
tabId: 'all',
|
||||||
|
count: state.upNextCount,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space6),
|
||||||
|
ViewOrdersFilterTab(
|
||||||
|
label: t.client_view_orders.tabs.active,
|
||||||
|
isSelected: state.filterTab == 'active',
|
||||||
|
tabId: 'active',
|
||||||
|
count: state.activeCount,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space6),
|
||||||
|
ViewOrdersFilterTab(
|
||||||
|
label: t.client_view_orders.tabs.completed,
|
||||||
|
isSelected: state.filterTab == 'completed',
|
||||||
|
tabId: 'completed',
|
||||||
|
count: state.completedCount,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Calendar Header controls
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
vertical: UiConstants.space2,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronLeft,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
|
||||||
|
context,
|
||||||
|
).updateWeekOffset(-1),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
splashRadius: 20,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat('MMMM yyyy').format(calendarDays.first),
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronRight,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onPressed: () => BlocProvider.of<ViewOrdersCubit>(
|
||||||
|
context,
|
||||||
|
).updateWeekOffset(1),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
splashRadius: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Calendar Grid
|
||||||
|
SizedBox(
|
||||||
|
height: 72,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: 7,
|
||||||
|
separatorBuilder: (BuildContext context, int index) =>
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final DateTime date = calendarDays[index];
|
||||||
|
final bool isSelected =
|
||||||
|
state.selectedDate != null &&
|
||||||
|
date.year == state.selectedDate!.year &&
|
||||||
|
date.month == state.selectedDate!.month &&
|
||||||
|
date.day == state.selectedDate!.day;
|
||||||
|
|
||||||
|
// Check if this date has any shifts
|
||||||
|
final String dateStr = DateFormat(
|
||||||
|
'yyyy-MM-dd',
|
||||||
|
).format(date);
|
||||||
|
final bool hasShifts = state.orders.any(
|
||||||
|
(OrderItem s) => s.date == dateStr,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if date is in the past
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
final DateTime today = DateTime(now.year, now.month, now.day);
|
||||||
|
final DateTime checkDate = DateTime(date.year, date.month, date.day);
|
||||||
|
final bool isPast = checkDate.isBefore(today);
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: isPast && !isSelected ? 0.5 : 1.0,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => BlocProvider.of<ViewOrdersCubit>(
|
||||||
|
context,
|
||||||
|
).selectDate(date),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
width: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? UiColors.primary
|
||||||
|
: UiColors.separatorPrimary,
|
||||||
|
),
|
||||||
|
boxShadow: isSelected
|
||||||
|
? <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.primary.withValues(
|
||||||
|
alpha: 0.25,
|
||||||
|
),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
DateFormat('dd').format(date),
|
||||||
|
style: UiTypography.title2b.copyWith(
|
||||||
|
fontSize: 18,
|
||||||
|
color: isSelected
|
||||||
|
? UiColors.white
|
||||||
|
: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat('E').format(date),
|
||||||
|
style: UiTypography.footnote2m.copyWith(
|
||||||
|
color: isSelected
|
||||||
|
? UiColors.white.withValues(alpha: 0.8)
|
||||||
|
: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasShifts) ...<Widget>[
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? UiColors.white
|
||||||
|
: UiColors.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,10 +25,14 @@ dependencies:
|
|||||||
path: ../../../domain
|
path: ../../../domain
|
||||||
krow_core:
|
krow_core:
|
||||||
path: ../../../core
|
path: ../../../core
|
||||||
|
krow_data_connect:
|
||||||
|
path: ../../../data_connect
|
||||||
# UI
|
# UI
|
||||||
lucide_icons: ^0.257.0
|
lucide_icons: ^0.257.0
|
||||||
intl: ^0.20.1
|
intl: ^0.20.1
|
||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
|
firebase_data_connect: ^0.2.2+2
|
||||||
|
firebase_auth: ^6.1.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
|
||||||
import 'package:staff_authentication/src/domain/ui_entities/auth_mode.dart';
|
|
||||||
import 'package:staff_authentication/src/presentation/blocs/auth_event.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_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 'package:staff_authentication/staff_authentication.dart';
|
||||||
|
|
||||||
import '../navigation/auth_navigator.dart'; // Import the extension
|
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.
|
/// A combined page for phone number entry and OTP verification.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -9,15 +9,29 @@ import 'phone_input/phone_input_form_field.dart';
|
|||||||
import 'phone_input/phone_input_header.dart';
|
import 'phone_input/phone_input_header.dart';
|
||||||
|
|
||||||
/// A widget that displays the phone number entry UI.
|
/// 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.
|
/// The current state of the authentication process.
|
||||||
final AuthState state;
|
final AuthState state;
|
||||||
|
|
||||||
/// Callback for when the "Send Code" action is triggered.
|
/// Callback for when the "Send Code" action is triggered.
|
||||||
final VoidCallback onSendCode;
|
final VoidCallback onSendCode;
|
||||||
|
|
||||||
/// Creates a [PhoneInput].
|
@override
|
||||||
const PhoneInput({super.key, required this.state, required this.onSendCode});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -35,19 +49,18 @@ class PhoneInput extends StatelessWidget {
|
|||||||
const PhoneInputHeader(),
|
const PhoneInputHeader(),
|
||||||
const SizedBox(height: UiConstants.space8),
|
const SizedBox(height: UiConstants.space8),
|
||||||
PhoneInputFormField(
|
PhoneInputFormField(
|
||||||
initialValue: state.phoneNumber,
|
initialValue: widget.state.phoneNumber,
|
||||||
error: state.errorMessage ?? '',
|
error: widget.state.errorMessage ?? '',
|
||||||
onChanged: (String value) {
|
onChanged: _handlePhoneChanged,
|
||||||
BlocProvider.of<AuthBloc>(
|
|
||||||
context,
|
|
||||||
).add(AuthPhoneUpdated(value));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PhoneInputActions(isLoading: state.isLoading, onSendCode: onSendCode),
|
PhoneInputActions(
|
||||||
|
isLoading: widget.state.isLoading,
|
||||||
|
onSendCode: widget.onSendCode,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,28 +32,40 @@ class HomeRepositoryImpl implements HomeRepository {
|
|||||||
|
|
||||||
Future<List<Shift>> _getShiftsForDate(DateTime date) async {
|
Future<List<Shift>> _getShiftsForDate(DateTime date) async {
|
||||||
try {
|
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
|
final response = await ExampleConnector.instance
|
||||||
.getApplicationsByStaffId(staffId: _currentStaffId)
|
.getApplicationsByStaffId(staffId: staffId)
|
||||||
|
.dayStart(_toTimestamp(start))
|
||||||
|
.dayEnd(_toTimestamp(end))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final targetYmd = DateFormat('yyyy-MM-dd').format(date);
|
// Filter for ACCEPTED applications (same logic as shifts_repository_impl)
|
||||||
|
final apps = response.data.applications.where(
|
||||||
return response.data.applications
|
(app) => (app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED) || (app.status is Known && (app.status as Known).value == ApplicationStatus.CONFIRMED)
|
||||||
.where((app) {
|
);
|
||||||
final shiftDate = app.shift.date?.toDate();
|
|
||||||
if (shiftDate == null) return false;
|
final List<Shift> shifts = [];
|
||||||
|
for (final app in apps) {
|
||||||
final isDateMatch = DateFormat('yyyy-MM-dd').format(shiftDate) == targetYmd;
|
shifts.add(_mapApplicationToShift(app));
|
||||||
final isAssigned = app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED;
|
}
|
||||||
|
|
||||||
return isDateMatch && isAssigned;
|
return shifts;
|
||||||
})
|
|
||||||
.map((app) => _mapApplicationToShift(app))
|
|
||||||
.toList();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
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
|
@override
|
||||||
Future<List<Shift>> getRecommendedShifts() async {
|
Future<List<Shift>> getRecommendedShifts() async {
|
||||||
@@ -93,21 +105,26 @@ class HomeRepositoryImpl implements HomeRepository {
|
|||||||
final s = app.shift;
|
final s = app.shift;
|
||||||
final r = app.shiftRole;
|
final r = app.shiftRole;
|
||||||
|
|
||||||
return Shift(
|
return ShiftAdapter.fromApplicationData(
|
||||||
id: s.id,
|
shiftId: s.id,
|
||||||
title: r.role.name,
|
roleId: r.roleId,
|
||||||
clientName: s.order.business.businessName,
|
roleName: r.role.name,
|
||||||
hourlyRate: r.role.costPerHour,
|
businessName: s.order.business.businessName,
|
||||||
location: s.location ?? 'Unknown',
|
companyLogoUrl: s.order.business.companyLogoUrl,
|
||||||
locationAddress: s.location ?? '',
|
costPerHour: r.role.costPerHour,
|
||||||
date: s.date?.toDate().toIso8601String() ?? '',
|
shiftLocation: s.location,
|
||||||
startTime: DateFormat('HH:mm').format(r.startTime?.toDate() ?? DateTime.now()),
|
teamHubName: s.order.teamHub.hubName,
|
||||||
endTime: DateFormat('HH:mm').format(r.endTime?.toDate() ?? DateTime.now()),
|
shiftDate: s.date?.toDate(),
|
||||||
createdDate: app.createdAt?.toDate().toIso8601String() ?? '',
|
startTime: r.startTime?.toDate(),
|
||||||
tipsAvailable: false, // Not in API
|
endTime: r.endTime?.toDate(),
|
||||||
mealProvided: false, // Not in API
|
createdAt: app.createdAt?.toDate(),
|
||||||
managers: [], // Not in this query
|
status: 'confirmed',
|
||||||
description: null,
|
description: s.description,
|
||||||
|
durationDays: s.durationDays,
|
||||||
|
count: r.count,
|
||||||
|
assigned: r.assigned,
|
||||||
|
eventName: s.order.eventName,
|
||||||
|
hasApplied: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/recommended_shift_card.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/section_header.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/shift_card.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/worker/auto_match_toggle.dart';
|
|
||||||
|
|
||||||
/// The home page for the staff worker application.
|
/// The home page for the staff worker application.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
|
|||||||
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../navigation/home_navigator.dart';
|
||||||
|
|
||||||
class ShiftCard extends StatefulWidget {
|
class ShiftCard extends StatefulWidget {
|
||||||
final Shift shift;
|
final Shift shift;
|
||||||
@@ -73,10 +74,7 @@ class _ShiftCardState extends State<ShiftCard> {
|
|||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
setState(() => isExpanded = !isExpanded);
|
setState(() => isExpanded = !isExpanded);
|
||||||
Modular.to.pushNamed(
|
Modular.to.pushShiftDetails(widget.shift);
|
||||||
'/shift-details/${widget.shift.id}',
|
|
||||||
arguments: widget.shift,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
|||||||
@@ -83,13 +83,42 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
@override
|
||||||
Future<List<Shift>> getMyShifts({
|
Future<List<Shift>> getMyShifts({
|
||||||
required DateTime start,
|
required DateTime start,
|
||||||
required DateTime end,
|
required DateTime end,
|
||||||
}) async {
|
}) async {
|
||||||
return _fetchApplications(
|
return _fetchApplications(
|
||||||
dc.ApplicationStatus.ACCEPTED,
|
[dc.ApplicationStatus.ACCEPTED, dc.ApplicationStatus.CONFIRMED],
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
);
|
);
|
||||||
@@ -97,12 +126,12 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Shift>> getPendingAssignments() async {
|
Future<List<Shift>> getPendingAssignments() async {
|
||||||
return _fetchApplications(dc.ApplicationStatus.PENDING);
|
return _fetchApplications([dc.ApplicationStatus.PENDING]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Shift>> getCancelledShifts() async {
|
Future<List<Shift>> getCancelledShifts() async {
|
||||||
return _fetchApplications(dc.ApplicationStatus.REJECTED);
|
return _fetchApplications([dc.ApplicationStatus.REJECTED]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -118,37 +147,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||||
|
|
||||||
final String roleName = app.shiftRole.role.name;
|
|
||||||
final String orderName =
|
|
||||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
|
||||||
? app.shift.order.eventName!
|
|
||||||
: app.shift.order.business.businessName;
|
|
||||||
final String title = '$roleName - $orderName';
|
|
||||||
final DateTime? shiftDate = _toDateTime(app.shift.date);
|
|
||||||
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
|
|
||||||
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
|
|
||||||
final DateTime? createdDt = _toDateTime(app.createdAt);
|
|
||||||
|
|
||||||
shifts.add(
|
shifts.add(
|
||||||
Shift(
|
_mapApplicationToShift(
|
||||||
id: app.shift.id,
|
app,
|
||||||
roleId: app.shiftRole.roleId,
|
_mapStatus(dc.ApplicationStatus.CHECKED_OUT),
|
||||||
title: title,
|
|
||||||
clientName: app.shift.order.business.businessName,
|
|
||||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
|
||||||
hourlyRate: app.shiftRole.role.costPerHour,
|
|
||||||
location: app.shift.location ?? '',
|
|
||||||
locationAddress: app.shift.order.teamHub.hubName,
|
|
||||||
date: shiftDate?.toIso8601String() ?? '',
|
|
||||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
|
||||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
|
||||||
createdDate: createdDt?.toIso8601String() ?? '',
|
|
||||||
status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT),
|
|
||||||
description: app.shift.description,
|
|
||||||
durationDays: app.shift.durationDays,
|
|
||||||
requiredSlots: app.shiftRole.count,
|
|
||||||
filledSlots: app.shiftRole.assigned ?? 0,
|
|
||||||
hasApplied: true,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -159,7 +161,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Shift>> _fetchApplications(
|
Future<List<Shift>> _fetchApplications(
|
||||||
dc.ApplicationStatus status, {
|
List<dc.ApplicationStatus> statuses, {
|
||||||
DateTime? start,
|
DateTime? start,
|
||||||
DateTime? end,
|
DateTime? end,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -173,8 +175,9 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
}
|
}
|
||||||
final response = await query.execute();
|
final response = await query.execute();
|
||||||
|
|
||||||
|
final statusNames = statuses.map((s) => s.name).toSet();
|
||||||
final apps = response.data.applications.where(
|
final apps = response.data.applications.where(
|
||||||
(app) => app.status.stringValue == status.name,
|
(app) => statusNames.contains(app.status.stringValue),
|
||||||
);
|
);
|
||||||
final List<Shift> shifts = [];
|
final List<Shift> shifts = [];
|
||||||
|
|
||||||
@@ -513,7 +516,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
shiftId: shiftId,
|
shiftId: shiftId,
|
||||||
staffId: staffId,
|
staffId: staffId,
|
||||||
roleId: targetRoleId,
|
roleId: targetRoleId,
|
||||||
status: dc.ApplicationStatus.ACCEPTED,
|
status: dc.ApplicationStatus.CONFIRMED,
|
||||||
origin: dc.ApplicationOrigin.STAFF,
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
)
|
)
|
||||||
// TODO: this should be PENDING so a vendor can accept it.
|
// TODO: this should be PENDING so a vendor can accept it.
|
||||||
@@ -547,7 +550,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> acceptShift(String shiftId) async {
|
Future<void> acceptShift(String shiftId) async {
|
||||||
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
|
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
908
docs/QA_TESTING_CHECKLIST.md
Normal file
908
docs/QA_TESTING_CHECKLIST.md
Normal file
@@ -0,0 +1,908 @@
|
|||||||
|
# 🧪 KROW Workforce Platform - QA Testing Checklist
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
**Date:** February 1, 2026
|
||||||
|
**Coverage:** Client App + Staff App
|
||||||
|
**Purpose:** Manual QA and Regression Testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 TABLE OF CONTENTS
|
||||||
|
|
||||||
|
1. [Feature-Level QA Checklist](#1️⃣-feature-level-qa-checklist)
|
||||||
|
- [Client App Features](#client-app-features)
|
||||||
|
- [Staff App Features](#staff-app-features)
|
||||||
|
2. [Cross-Application Test Scenarios](#2️⃣-cross-application-test-scenarios)
|
||||||
|
3. [Shared Infrastructure Validation](#3️⃣-shared-infrastructure-validation)
|
||||||
|
4. [Regression & Release Checklist](#4️⃣-regression--release-checklist)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ FEATURE-LEVEL QA CHECKLIST
|
||||||
|
|
||||||
|
### CLIENT APP FEATURES
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-001: Authentication
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Launch app → Get Started → Sign In
|
||||||
|
- Launch app → Get Started → Sign Up
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Sign in with valid email and password displays home dashboard
|
||||||
|
- [ ] Sign up with business details creates account and navigates to home
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Invalid email format shows validation error
|
||||||
|
- [ ] Incorrect password shows authentication error
|
||||||
|
- [ ] Weak password in sign-up shows strength requirements
|
||||||
|
- [ ] Duplicate email in sign-up shows "already registered" error
|
||||||
|
- [ ] Empty fields show required field errors
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays during authentication
|
||||||
|
- [ ] OAuth redirect shows appropriate loading state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-002: Home Dashboard
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Home tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Dashboard displays current day coverage widget
|
||||||
|
- [ ] Spending analytics widget shows correct totals
|
||||||
|
- [ ] Recent reorders display completed shift roles
|
||||||
|
- [ ] Quick action buttons navigate to correct features
|
||||||
|
- [ ] Drag-and-drop widget reordering works correctly
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty state shows "No data available" when no orders exist
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Empty coverage shows "No shifts today"
|
||||||
|
- [ ] Empty reorders shows "No recent orders"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-003: Create Order
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Home → Create Order button
|
||||||
|
- Orders tab → + FAB button
|
||||||
|
- Order type → One-Time
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Order type selection displays.
|
||||||
|
- [ ] Hub selection shows list of business hubs
|
||||||
|
- [ ] Role selection displays vendor roles
|
||||||
|
- [ ] Position quantity can be incremented/decremented (min 1)
|
||||||
|
- [ ] Date picker displays correct calendar
|
||||||
|
- [ ] Time pickers show valid time ranges
|
||||||
|
- [ ] Break duration affects total hours calculation
|
||||||
|
- [ ] Cost preview calculates correctly (rate × positions × hours)
|
||||||
|
- [ ] Order submission creates order, shift, and shift roles
|
||||||
|
- [ ] Success confirmation displays after submission
|
||||||
|
- [ ] New order appears in View Orders list
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty hub field shows validation error
|
||||||
|
- [ ] Empty role field shows validation error
|
||||||
|
- [ ] Zero positions shows validation error
|
||||||
|
- [ ] Invalid date (past) shows validation error
|
||||||
|
- [ ] Start time after end time shows validation error
|
||||||
|
- [ ] Missing required fields prevent submission
|
||||||
|
- [ ] Backend validation errors display appropriately
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Hub list shows "No hubs" if none exist
|
||||||
|
- [ ] Role list shows "No roles" if none configured
|
||||||
|
- [ ] Loading spinner displays during submission
|
||||||
|
- [ ] Submission progress indicator updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-004: View Orders
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Orders tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Orders list displays orders for selected date
|
||||||
|
- [ ] Calendar date selection updates order list
|
||||||
|
- [ ] Each order card shows hub name and address
|
||||||
|
- [ ] Each order card shows shift time range
|
||||||
|
- [ ] Each order card shows role positions (filled/total)
|
||||||
|
- [ ] Each order card shows hourly rate and total cost
|
||||||
|
- [ ] Accepted applications section displays confirmed staff
|
||||||
|
- [ ] Staff names and photos display correctly
|
||||||
|
- [ ] Order list scrolls smoothly with many orders
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Invalid date selection shows error
|
||||||
|
|
||||||
|
- [ ] Missing staff data shows placeholder
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Empty date shows "No orders for this date"
|
||||||
|
- [ ] Empty accepted applications shows "No confirmed staff"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-005: Coverage Monitoring
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Coverage tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Coverage overview displays current date
|
||||||
|
- [ ] Coverage stats show needed/confirmed/checked-in counts
|
||||||
|
- [ ] Shift cards display hub name and time range
|
||||||
|
- [ ] Worker cards show staff name and photo
|
||||||
|
- [ ] Check-in status indicators update correctly (late, en-route, checked-in)
|
||||||
|
- [ ] Late workers display with warning indicator
|
||||||
|
- [ ] Coverage progress bar updates correctly
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Missing worker photo shows default avatar
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
|
||||||
|
- [ ] Empty coverage shows "No shifts today"
|
||||||
|
- [ ] No workers show "No staff assigned"
|
||||||
|
|
||||||
|
**State Persistence:**
|
||||||
|
- [ ] Coverage data refreshes automatically every X minutes
|
||||||
|
- [ ] Manual refresh via pull-to-refresh gesture
|
||||||
|
|
||||||
|
**Backend Dependency Validation:**
|
||||||
|
- [ ] `listShiftRolesByBusinessAndDateRange` returns shift requirements
|
||||||
|
- [ ] `listStaffsApplicationsByBusinessForDay` returns staff status
|
||||||
|
- [ ] Attendance status correctly mapped from backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-006: Billing & Invoices
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Billing tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Current bill amount displays correctly
|
||||||
|
- [ ] Pending invoices list shows open invoices
|
||||||
|
- [ ] Invoice history shows paid invoices
|
||||||
|
- [ ] Savings amount displays correctly
|
||||||
|
- [ ] Spending breakdown shows costs by role
|
||||||
|
- [ ] Period filter (weekly/monthly) updates data
|
||||||
|
- [ ] Invoice detail view shows line items
|
||||||
|
- [ ] Invoice PDF download works (if implemented)
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Zero billing shows $0.00 (not error)
|
||||||
|
- [ ] Negative savings shows correctly
|
||||||
|
- [ ] Missing invoice data shows placeholder
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Empty pending invoices shows "No pending invoices"
|
||||||
|
- [ ] Empty history shows "No invoice history"
|
||||||
|
- [ ] Empty spending breakdown shows "No spending data"
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-007: Hub Management
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Settings → Hubs
|
||||||
|
- Create Order → Add Hub button
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Hubs list displays all business hubs
|
||||||
|
- [ ] Hub cards show name and full address
|
||||||
|
- [ ] Add hub button opens creation form
|
||||||
|
- [ ] Google Places autocomplete suggests addresses
|
||||||
|
- [ ] Address selection auto-fills all address fields
|
||||||
|
- [ ] Hub name can be customized
|
||||||
|
- [ ] Hub creation adds to list immediately
|
||||||
|
- [ ] Hub deletion removes from list (with confirmation)
|
||||||
|
- [ ] Team entity auto-created for business if missing
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty hub name shows validation error
|
||||||
|
- [ ] Empty address shows validation error
|
||||||
|
- [ ] Invalid address format shows error
|
||||||
|
- [ ] Duplicate hub name shows warning
|
||||||
|
- [ ] Hub with active orders prevents deletion (validation error)
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Empty hubs list shows "No hubs configured"
|
||||||
|
- [ ] Hub creation shows loading spinner
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-008: Settings
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Settings (navigation menu)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] User profile displays name and email
|
||||||
|
- [ ] Business name displays correctly
|
||||||
|
- [ ] Hubs link navigates to hub management
|
||||||
|
- [ ] Sign out logs out user and returns to auth screen
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Missing profile photo shows default avatar
|
||||||
|
- [ ] Sign out error shows retry option
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Profile data loads on page mount
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 CLIENT-009: Client Main Navigation
|
||||||
|
|
||||||
|
**Applications:** Client
|
||||||
|
**Entry Points:**
|
||||||
|
- Main app shell after authentication
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Bottom navigation displays 5 tabs (Home, Coverage, Billing, Orders, Reports)
|
||||||
|
- [ ] Tab selection updates active indicator
|
||||||
|
- [ ] Tab selection navigates to correct feature
|
||||||
|
- [ ] Deep links navigate to correct tab
|
||||||
|
- [ ] Back button navigates correctly within nested routes
|
||||||
|
- [ ] Tab state persists after device rotation
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Invalid route shows 404 or redirects to home
|
||||||
|
- [ ] Reports tab shows placeholder (not yet implemented)
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Navigation bar displays immediately
|
||||||
|
- [ ] Initial tab loads first
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STAFF APP FEATURES
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-001: Authentication
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Launch app → Get Started → Phone Verification
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Phone number entry accepts valid formats
|
||||||
|
- [ ] OTP sent confirmation displays
|
||||||
|
- [ ] OTP verification succeeds with valid code
|
||||||
|
- [ ] Profile setup wizard displays for new users
|
||||||
|
- [ ] Authenticated users bypass auth and show home
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Invalid phone format shows validation error
|
||||||
|
- [ ] Incorrect OTP shows verification error
|
||||||
|
- [ ] Expired OTP shows re-send option
|
||||||
|
- [ ] Empty fields show required field errors
|
||||||
|
- [ ] Network error displays retry option
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays during phone verification
|
||||||
|
- [ ] OTP input shows countdown timer
|
||||||
|
- [ ] Profile setup shows progress indicator
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-002: Home Dashboard
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Home tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Today's shifts display with time and location
|
||||||
|
- [ ] Tomorrow's shifts display correctly
|
||||||
|
- [ ] Recommended shifts show available opportunities
|
||||||
|
- [ ] Shift cards show role, location, and pay rate
|
||||||
|
- [ ] Quick actions navigate to correct features
|
||||||
|
- [ ] Dashboard refreshes on pull-to-refresh
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Missing shift data shows placeholder
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Empty today's shifts shows "No shifts today"
|
||||||
|
- [ ] Empty recommended shows "No available shifts"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-003: Profile
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Profile tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Profile displays name, email, phone, and photo
|
||||||
|
- [ ] Statistics show total shifts, ratings, reliability score
|
||||||
|
- [ ] Profile sections list displays all sections
|
||||||
|
- [ ] Section navigation works correctly
|
||||||
|
- [ ] Sign out logs out user and returns to auth screen
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Missing profile photo shows default avatar
|
||||||
|
- [ ] Missing statistics show 0 or default values
|
||||||
|
- [ ] Sign out error shows retry option
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Profile data loads on page mount
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-004: Shifts Management
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Shifts tab (bottom navigation)
|
||||||
|
- Tab navigation: My Shifts / Available / Pending / Cancelled / History
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] My Shifts tab displays assigned shifts
|
||||||
|
- [ ] Available Shifts tab shows open positions
|
||||||
|
- [ ] Pending tab shows applications awaiting approval
|
||||||
|
- [ ] Cancelled tab shows cancelled shifts
|
||||||
|
- [ ] History tab shows past shifts
|
||||||
|
- [ ] Shift detail view displays full information
|
||||||
|
- [ ] Accept shift updates status to confirmed
|
||||||
|
- [ ] Decline shift updates status to declined
|
||||||
|
- [ ] Apply for shift creates application
|
||||||
|
- [ ] Shift cards show time, location, role, and pay
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty tabs show appropriate empty state messages
|
||||||
|
- [ ] Already applied shift prevents duplicate application
|
||||||
|
- [ ] Past shifts cannot be applied to
|
||||||
|
- [ ] Cancelled shifts show cancellation reason
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-005: Availability Management
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Worker Main → Availability
|
||||||
|
- Profile → Availability section
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Weekly grid displays Monday-Sunday
|
||||||
|
- [ ] Time slots (Morning/Afternoon/Evening) toggle correctly
|
||||||
|
- [ ] Quick-set buttons work (Weekdays/Weekends/All Week)
|
||||||
|
- [ ] Individual day/slot updates save correctly
|
||||||
|
- [ ] Green checkmarks indicate availability
|
||||||
|
- [ ] Gray states indicate unavailability
|
||||||
|
- [ ] Changes save automatically
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
|
||||||
|
- [ ] Save failure shows error message
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays while fetching availability
|
||||||
|
- [ ] Default state shows all unavailable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-006: Clock In/Out
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Clock In tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Today's shift displays with clock in button
|
||||||
|
- [ ] Clock in button creates attendance record
|
||||||
|
- [ ] Clock in time displays correctly
|
||||||
|
- [ ] Clock out button appears after clocking in
|
||||||
|
- [ ] Clock out creates end time record
|
||||||
|
- [ ] Total hours calculated correctly
|
||||||
|
- [ ] Attendance status updates immediately
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] No shift today shows "No shifts to clock in"
|
||||||
|
- [ ] Already clocked in prevents duplicate clock in
|
||||||
|
|
||||||
|
- [ ] Clock in outside shift time shows warning
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays while fetching shift
|
||||||
|
- [ ] Empty state shows "No shifts scheduled"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-007: Payments
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Payments tab (bottom navigation)
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Payment summary displays total earnings
|
||||||
|
- [ ] Payment history lists all transactions
|
||||||
|
- [ ] Payment cards show amount, date, and status
|
||||||
|
- [ ] Payment detail view shows breakdown
|
||||||
|
- [ ] Filter by date range works correctly
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Zero earnings show $0.00 (not error)
|
||||||
|
- [ ] Missing payment data shows placeholder
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Empty history shows "No payment history"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-008: Personal Info (Onboarding)
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Profile → Personal Info
|
||||||
|
- Onboarding wizard
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Form displays current profile data
|
||||||
|
- [ ] Name field allows text input
|
||||||
|
- [ ] Email field validates email format
|
||||||
|
- [ ] Phone field validates phone format
|
||||||
|
- [ ] Photo upload works correctly
|
||||||
|
- [ ] Preferred locations multi-select works
|
||||||
|
- [ ] Save button updates profile
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty required fields show validation errors
|
||||||
|
- [ ] Invalid email format shows error
|
||||||
|
- [ ] Invalid phone format shows error
|
||||||
|
|
||||||
|
- [ ] Photo upload failure shows error
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Form loads with skeleton placeholders
|
||||||
|
- [ ] Photo upload shows progress indicator
|
||||||
|
- [ ] Save button shows loading spinner
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-009: Emergency Contact (Onboarding)
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Profile → Emergency Contact
|
||||||
|
- Onboarding wizard
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Contact list displays all contacts
|
||||||
|
- [ ] Add contact button opens form
|
||||||
|
- [ ] Contact form validates name and phone
|
||||||
|
- [ ] Relationship dropdown shows options (Family/Spouse/Friend/Other)
|
||||||
|
- [ ] Remove contact deletes from list
|
||||||
|
- [ ] Save updates all contacts
|
||||||
|
- [ ] Multiple contacts supported
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty name shows validation error
|
||||||
|
- [ ] Invalid phone format shows error
|
||||||
|
- [ ] At least one contact required (if applicable)
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays while fetching contacts
|
||||||
|
- [ ] Empty state shows "No emergency contacts"
|
||||||
|
- [ ] Save button shows loading spinner
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-010: Experience & Skills (Onboarding)
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Profile → Experience
|
||||||
|
- Onboarding wizard
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Industries multi-select displays options
|
||||||
|
- [ ] Skills multi-select displays options
|
||||||
|
- [ ] Selected items show checkmarks
|
||||||
|
- [ ] Deselection removes items
|
||||||
|
- [ ] Save updates profile
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] At least one industry required (if applicable)
|
||||||
|
- [ ] At least one skill required (if applicable)
|
||||||
|
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays while fetching data
|
||||||
|
- [ ] Save button shows loading spinner
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-012: Bank Account (Finances)
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Profile → Bank Account
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Account list displays all accounts
|
||||||
|
- [ ] Add account button opens form
|
||||||
|
- [ ] Form validates routing and account numbers
|
||||||
|
- [ ] Account type dropdown shows options (Checking/Savings)
|
||||||
|
- [ ] First account auto-sets as primary
|
||||||
|
- [ ] Save adds account to list
|
||||||
|
- [ ] Primary account indicator displays
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty routing number shows validation error
|
||||||
|
- [ ] Invalid routing number format shows error
|
||||||
|
- [ ] Empty account number shows validation error
|
||||||
|
- [ ] Invalid account number format shows error
|
||||||
|
- [ ] Duplicate account shows warning
|
||||||
|
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays while fetching accounts
|
||||||
|
- [ ] Empty state shows "No bank accounts"
|
||||||
|
- [ ] Save button shows loading spinner
|
||||||
|
|
||||||
|
**State Persistence:**
|
||||||
|
- [ ] Accounts persist after save
|
||||||
|
- [ ] Account list refreshes after addition
|
||||||
|
|
||||||
|
**Backend Dependency Validation:**
|
||||||
|
- [ ] `getAccountsByOwnerId` fetches staff accounts
|
||||||
|
- [ ] `createAccount` creates new account
|
||||||
|
- [ ] First account auto-flagged as primary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-013: Time Card History (Finances)
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Profile → Time Card
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Time card list displays all records
|
||||||
|
- [ ] Each card shows shift details (date, time, location)
|
||||||
|
- [ ] Each card shows clock in/out times
|
||||||
|
- [ ] Each card shows total hours worked
|
||||||
|
- [ ] Scrolling loads more records (pagination)
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Missing attendance data shows "Not recorded"
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Empty state shows "No time card history"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-014: Tax Forms (Compliance)
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Profile → Tax Forms
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Forms list displays required forms (I-9, W-4)
|
||||||
|
- [ ] Form status shows completed/incomplete
|
||||||
|
- [ ] I-9 form opens editor
|
||||||
|
- [ ] I-9 form validates all fields
|
||||||
|
- [ ] W-4 form opens editor
|
||||||
|
- [ ] W-4 form validates all fields
|
||||||
|
- [ ] Form submission updates status to completed
|
||||||
|
- [ ] Completed forms show edit option
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Empty required fields show validation errors
|
||||||
|
- [ ] Invalid SSN format shows error
|
||||||
|
- [ ] Invalid date format shows error
|
||||||
|
- [ ] Signature required validation
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Loading spinner displays while fetching forms
|
||||||
|
- [ ] Form editor loads with skeleton placeholders
|
||||||
|
- [ ] Save button shows loading spinner
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 📱 STAFF-017: Staff Main Navigation
|
||||||
|
|
||||||
|
**Applications:** Staff
|
||||||
|
**Entry Points:**
|
||||||
|
- Main app shell after authentication
|
||||||
|
|
||||||
|
**Happy Path Test Cases:**
|
||||||
|
- [ ] Bottom navigation displays 5 tabs (Shifts, Payments, Home, Clock In, Profile)
|
||||||
|
- [ ] Tab selection updates active indicator
|
||||||
|
- [ ] Tab selection navigates to correct feature
|
||||||
|
- [ ] Deep links navigate to correct tab and nested route
|
||||||
|
- [ ] Back button navigates correctly within nested routes
|
||||||
|
- [ ] Tab state persists after device rotation
|
||||||
|
- [ ] Nested routes (onboarding, emergency-contact, etc.) accessible
|
||||||
|
|
||||||
|
**Validation & Error States:**
|
||||||
|
- [ ] Invalid route shows 404 or redirects to home
|
||||||
|
- [ ] Navigation errors log appropriately
|
||||||
|
|
||||||
|
**Loading & Empty States:**
|
||||||
|
- [ ] Navigation bar displays immediately
|
||||||
|
- [ ] Initial tab loads first
|
||||||
|
|
||||||
|
**State Persistence:**
|
||||||
|
- [ ] Active tab persists after app background → foreground
|
||||||
|
- [ ] Tab state resets to home on app restart
|
||||||
|
|
||||||
|
**Backend Dependency Validation:**
|
||||||
|
- [ ] No direct backend calls (navigation only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ CROSS-APPLICATION TEST SCENARIOS
|
||||||
|
|
||||||
|
### Scenario 1: Order Creation → Staff Application Flow
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Client user authenticated
|
||||||
|
- Staff user authenticated
|
||||||
|
- At least one hub configured
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **CLIENT APP:**
|
||||||
|
- [ ] Create one-time order with specific hub, role, date, and time
|
||||||
|
- [ ] Verify order appears in View Orders list
|
||||||
|
- [ ] Verify shift shows as unfilled (0/X positions)
|
||||||
|
|
||||||
|
2. **STAFF APP:**
|
||||||
|
- [ ] Open Shifts tab → Available tab
|
||||||
|
- [ ] Verify new shift appears in available list
|
||||||
|
- [ ] Verify shift details match order (hub, role, time, pay)
|
||||||
|
- [ ] Apply for shift position
|
||||||
|
|
||||||
|
3. **CLIENT APP:**
|
||||||
|
- [ ] Refresh View Orders
|
||||||
|
- [ ] Verify shift shows pending application (0/X filled, pending)
|
||||||
|
|
||||||
|
4. **STAFF APP:**
|
||||||
|
- [ ] Verify application appears in Pending tab
|
||||||
|
- [ ] Verify shift removed from Available tab
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ Order created in Client appears in Staff Available Shifts
|
||||||
|
- ✅ Application in Staff shows pending in both apps
|
||||||
|
- ✅ Shift counts update correctly in real-time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 2: Shift Acceptance → Coverage Tracking
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Scenario 1 completed (pending application exists)
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **STAFF APP:**
|
||||||
|
- [ ] Go to Shifts → Pending tab
|
||||||
|
- [ ] Accept pending shift assignment
|
||||||
|
|
||||||
|
2. **CLIENT APP:**
|
||||||
|
- [ ] Refresh View Orders
|
||||||
|
- [ ] Verify shift shows as filled (1/X positions)
|
||||||
|
- [ ] Verify staff name and photo appear in accepted applications
|
||||||
|
- [ ] Navigate to Coverage tab
|
||||||
|
- [ ] Verify shift appears with assigned staff
|
||||||
|
|
||||||
|
3. **STAFF APP:**
|
||||||
|
- [ ] Verify shift moved from Pending to My Shifts tab
|
||||||
|
- [ ] Verify shift appears on Home dashboard
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ Accepted shift reflects in Client orders immediately
|
||||||
|
- ✅ Staff appears in Coverage monitoring
|
||||||
|
- ✅ Shift moves to My Shifts in Staff app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 3: Clock In → Real-Time Coverage Update
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Scenario 2 completed (staff has accepted shift)
|
||||||
|
- Current date/time is during shift window
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **STAFF APP:**
|
||||||
|
- [ ] Navigate to Clock In tab
|
||||||
|
- [ ] Verify today's shift displays
|
||||||
|
- [ ] Click Clock In button
|
||||||
|
- [ ] Verify clock in time recorded
|
||||||
|
|
||||||
|
2. **CLIENT APP:**
|
||||||
|
- [ ] Navigate to Coverage tab
|
||||||
|
- [ ] Verify staff status changed to "Checked In"
|
||||||
|
- [ ] Verify check-in time displays
|
||||||
|
- [ ] Verify coverage stats updated (checked-in count incremented)
|
||||||
|
|
||||||
|
3. **STAFF APP:**
|
||||||
|
- [ ] Wait until shift end time
|
||||||
|
- [ ] Click Clock Out button
|
||||||
|
- [ ] Verify clock out time recorded
|
||||||
|
|
||||||
|
4. **CLIENT APP:**
|
||||||
|
- [ ] Refresh Coverage tab
|
||||||
|
- [ ] Verify staff status changed to "Completed"
|
||||||
|
|
||||||
|
5. **STAFF APP:**
|
||||||
|
- [ ] Navigate to Time Card
|
||||||
|
- [ ] Verify attendance record appears with correct times and hours
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ Clock in updates Coverage status in Client
|
||||||
|
- ✅ Clock out completes attendance record
|
||||||
|
- ✅ Time card displays correct hours in Staff app
|
||||||
|
- ✅ Coverage monitoring reflects real-time status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 4: Hub Creation → Order Placement
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Client user authenticated
|
||||||
|
- No existing hubs
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **CLIENT APP:**
|
||||||
|
- [ ] Navigate to Settings → Hubs
|
||||||
|
- [ ] Verify empty state "No hubs configured"
|
||||||
|
- [ ] Click Add Hub button
|
||||||
|
- [ ] Enter hub name and use Google Places autocomplete
|
||||||
|
- [ ] Select address from suggestions
|
||||||
|
- [ ] Verify address fields auto-filled
|
||||||
|
- [ ] Save hub
|
||||||
|
|
||||||
|
2. **CLIENT APP:**
|
||||||
|
- [ ] Navigate to Create Order
|
||||||
|
- [ ] Verify new hub appears in hub selection list
|
||||||
|
- [ ] Select new hub and complete order creation
|
||||||
|
|
||||||
|
3. **STAFF APP:**
|
||||||
|
- [ ] Navigate to Shifts → Available
|
||||||
|
- [ ] Verify shift shows correct hub name and address
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ Hub created in Settings appears in order creation
|
||||||
|
- ✅ Hub address propagates to shift details in Staff app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 5: Shift Cancellation → Staff Notification
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Staff has accepted shift assignment
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **CLIENT APP:**
|
||||||
|
- [ ] Navigate to View Orders
|
||||||
|
- [ ] Select order with assigned staff
|
||||||
|
- [ ] Cancel shift (if feature exists) or delete order
|
||||||
|
|
||||||
|
2. **STAFF APP:**
|
||||||
|
- [ ] Refresh Shifts tab
|
||||||
|
- [ ] Verify shift moved to Cancelled tab
|
||||||
|
- [ ] Verify shift removed from My Shifts
|
||||||
|
- [ ] Verify cancellation reason displays
|
||||||
|
|
||||||
|
3. **STAFF APP:**
|
||||||
|
- [ ] Verify shift removed from Home dashboard
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ Cancelled shift moves to Cancelled tab
|
||||||
|
- ✅ Shift removed from active assignments
|
||||||
|
- ⚠️ **Requires clarification:** Cancellation feature may not be fully implemented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 6: Authentication State Sharing
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Neither app authenticated
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **CLIENT APP:**
|
||||||
|
- [ ] Sign in with email/password
|
||||||
|
- [ ] Verify Firebase Auth token generated
|
||||||
|
|
||||||
|
2. **STAFF APP:**
|
||||||
|
- [ ] Launch app
|
||||||
|
- [ ] Verify Staff app requires separate authentication
|
||||||
|
- [ ] Verify Client session does not carry over
|
||||||
|
|
||||||
|
3. **CLIENT APP:**
|
||||||
|
- [ ] Sign out
|
||||||
|
|
||||||
|
4. **STAFF APP:**
|
||||||
|
- [ ] Verify Staff app session persists (independent)
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ Client and Staff apps maintain independent auth sessions
|
||||||
|
- ✅ Signing out of one app does not affect the other
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 7: Data Created in Client → Visible in Staff
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Client creates multiple orders
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **CLIENT APP:**
|
||||||
|
- [ ] Create 5 orders on different dates
|
||||||
|
- [ ] Create 3 orders on same date with different hubs
|
||||||
|
|
||||||
|
2. **STAFF APP:**
|
||||||
|
- [ ] Navigate to Shifts → Available
|
||||||
|
- [ ] Verify all 8 shifts appear
|
||||||
|
- [ ] Verify date grouping correct
|
||||||
|
- [ ] Verify hub addresses correct
|
||||||
|
- [ ] Apply for 2 shifts
|
||||||
|
|
||||||
|
3. **CLIENT APP:**
|
||||||
|
- [ ] Navigate to View Orders
|
||||||
|
- [ ] Verify 2 shifts show pending applications
|
||||||
|
- [ ] Navigate to Coverage
|
||||||
|
- [ ] Verify 0 checked-in (pending acceptance)
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ All orders visible in both apps
|
||||||
|
- ✅ Application states sync correctly
|
||||||
|
- ✅ Data consistency maintained across apps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenario 8: Role-Based Access Differences
|
||||||
|
|
||||||
|
**Preconditions:**
|
||||||
|
- Client user authenticated
|
||||||
|
- Staff user authenticated
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. **CLIENT APP:**
|
||||||
|
- [ ] Navigate to Billing
|
||||||
|
- [ ] Verify billing data displays (Client-only feature)
|
||||||
|
- [ ] Navigate to Create Order
|
||||||
|
- [ ] Verify order creation available (Client-only feature)
|
||||||
|
|
||||||
|
2. **STAFF APP:**
|
||||||
|
- [ ] Verify no Billing tab exists
|
||||||
|
- [ ] Verify no Create Order feature
|
||||||
|
- [ ] Navigate to Availability
|
||||||
|
- [ ] Verify availability editing available (Staff-only feature)
|
||||||
|
|
||||||
|
3. **CLIENT APP:**
|
||||||
|
- [ ] Verify no Availability feature exists
|
||||||
|
- [ ] Verify no Clock In feature exists
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
- ✅ Client app has business management features (orders, billing, hubs)
|
||||||
|
- ✅ Staff app has worker features (availability, clock in, payments)
|
||||||
|
- ✅ No feature overlap or unauthorized access
|
||||||
|
|
||||||
|
---
|
||||||
@@ -146,6 +146,17 @@
|
|||||||
.markdown-content th { background-color: #f9fafb; font-weight: 600; }
|
.markdown-content th { background-color: #f9fafb; font-weight: 600; }
|
||||||
.markdown-content img { max-width: 100%; height: auto; border-radius: 0.5em; margin: 1em 0; }
|
.markdown-content img { max-width: 100%; height: auto; border-radius: 0.5em; margin: 1em 0; }
|
||||||
.markdown-content hr { border: none; border-top: 2px solid #e5e7eb; margin: 2em 0; }
|
.markdown-content hr { border: none; border-top: 2px solid #e5e7eb; margin: 2em 0; }
|
||||||
|
|
||||||
|
/* Mermaid diagram styling */
|
||||||
|
.mermaid-diagram-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.mermaid-diagram-wrapper svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading Overlay */
|
/* Loading Overlay */
|
||||||
#auth-loading {
|
#auth-loading {
|
||||||
@@ -821,6 +832,28 @@
|
|||||||
const markdownText = await response.text();
|
const markdownText = await response.text();
|
||||||
const htmlContent = marked.parse(markdownText);
|
const htmlContent = marked.parse(markdownText);
|
||||||
documentContainer.innerHTML = htmlContent;
|
documentContainer.innerHTML = htmlContent;
|
||||||
|
|
||||||
|
// Render Mermaid diagrams embedded in the markdown
|
||||||
|
const mermaidBlocks = documentContainer.querySelectorAll('code.language-mermaid');
|
||||||
|
for (let i = 0; i < mermaidBlocks.length; i++) {
|
||||||
|
const block = mermaidBlocks[i];
|
||||||
|
const mermaidCode = block.textContent;
|
||||||
|
const pre = block.parentElement;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { svg } = await mermaid.render(`mermaid-doc-${Date.now()}-${i}`, mermaidCode);
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'mermaid-diagram-wrapper bg-white p-4 rounded-lg border border-gray-200 my-4 overflow-x-auto';
|
||||||
|
wrapper.innerHTML = svg;
|
||||||
|
pre.replaceWith(wrapper);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Mermaid rendering error:', err);
|
||||||
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'bg-red-50 border border-red-200 rounded-lg p-4 my-4';
|
||||||
|
errorDiv.innerHTML = `<p class="text-red-700 text-sm"><strong>Mermaid Error:</strong> ${err.message}</p>`;
|
||||||
|
pre.replaceWith(errorDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading document:', error);
|
console.error('Error loading document:', error);
|
||||||
documentContainer.innerHTML = `
|
documentContainer.innerHTML = `
|
||||||
|
|||||||
Reference in New Issue
Block a user