Merge pull request #423 from Oloodi/408-feature-implement-paidunpaid-breaks---client-app-frontend-development
Update the UI of the shift details page and integrate google maps
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import java.util.Base64
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
@@ -6,6 +8,18 @@ plugins {
|
|||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dartDefinesString = project.findProperty("dart-defines") as? String ?: ""
|
||||||
|
val dartEnvironmentVariables = mutableMapOf<String, String>()
|
||||||
|
dartDefinesString.split(",").forEach {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
val decoded = String(Base64.getDecoder().decode(it))
|
||||||
|
val components = decoded.split("=")
|
||||||
|
if (components.size == 2) {
|
||||||
|
dartEnvironmentVariables[components[0]] = components[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.krowwithus.client"
|
namespace = "com.krowwithus.client"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@@ -29,6 +43,8 @@ android {
|
|||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|
||||||
|
manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="${GOOGLE_MAPS_API_KEY}" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import GoogleMaps
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
@@ -7,7 +8,31 @@ import UIKit
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
if let apiKey = getDartDefine(key: "GOOGLE_MAPS_API_KEY") {
|
||||||
|
GMSServices.provideAPIKey(apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getDartDefine(key: String) -> String? {
|
||||||
|
guard let dartDefines = Bundle.main.infoDictionary?["DART_DEFINES"] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let defines = dartDefines.components(separatedBy: ",")
|
||||||
|
for define in defines {
|
||||||
|
guard let decodedData = Data(base64Encoded: define),
|
||||||
|
let decodedString = String(data: decodedData, encoding: .utf8) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let components = decodedString.components(separatedBy: "=")
|
||||||
|
if components.count == 2 && components[0] == key {
|
||||||
|
return components[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Krow With Us Client</string>
|
<string>Krow With Us Client</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Krow With Us Client</string>
|
<string>Krow With Us Client</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
@@ -45,5 +45,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>DART_DEFINES</key>
|
||||||
|
<string>$(DART_DEFINES)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import java.util.Base64
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
@@ -6,6 +8,18 @@ plugins {
|
|||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dartDefinesString = project.findProperty("dart-defines") as? String ?: ""
|
||||||
|
val dartEnvironmentVariables = mutableMapOf<String, String>()
|
||||||
|
dartDefinesString.split(",").forEach {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
val decoded = String(Base64.getDecoder().decode(it))
|
||||||
|
val components = decoded.split("=")
|
||||||
|
if (components.size == 2) {
|
||||||
|
dartEnvironmentVariables[components[0]] = components[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.krowwithus.staff"
|
namespace = "com.krowwithus.staff"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@@ -29,6 +43,8 @@ android {
|
|||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|
||||||
|
manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="${GOOGLE_MAPS_API_KEY}" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@@ -30,11 +30,21 @@ public final class GeneratedPluginRegistrant {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e);
|
Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin());
|
flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e);
|
Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.googlemaps.GoogleMapsPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin google_maps_flutter_android, io.flutter.plugins.googlemaps.GoogleMapsPlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -50,5 +60,10 @@ public final class GeneratedPluginRegistrant {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import GoogleMaps
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
@@ -7,7 +8,31 @@ import UIKit
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
if let apiKey = getDartDefine(key: "GOOGLE_MAPS_API_KEY") {
|
||||||
|
GMSServices.provideAPIKey(apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getDartDefine(key: String) -> String? {
|
||||||
|
guard let dartDefines = Bundle.main.infoDictionary?["DART_DEFINES"] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let defines = dartDefines.components(separatedBy: ",")
|
||||||
|
for define in defines {
|
||||||
|
guard let decodedData = Data(base64Encoded: define),
|
||||||
|
let decodedString = String(data: decodedData, encoding: .utf8) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let components = decodedString.components(separatedBy: "=")
|
||||||
|
if components.count == 2 && components[0] == key {
|
||||||
|
return components[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@
|
|||||||
@import geolocator_apple;
|
@import geolocator_apple;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<google_maps_flutter_ios/FLTGoogleMapsPlugin.h>)
|
||||||
|
#import <google_maps_flutter_ios/FLTGoogleMapsPlugin.h>
|
||||||
|
#else
|
||||||
|
@import google_maps_flutter_ios;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>)
|
#if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>)
|
||||||
#import <permission_handler_apple/PermissionHandlerPlugin.h>
|
#import <permission_handler_apple/PermissionHandlerPlugin.h>
|
||||||
#else
|
#else
|
||||||
@@ -42,6 +48,12 @@
|
|||||||
@import shared_preferences_foundation;
|
@import shared_preferences_foundation;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<url_launcher_ios/URLLauncherPlugin.h>)
|
||||||
|
#import <url_launcher_ios/URLLauncherPlugin.h>
|
||||||
|
#else
|
||||||
|
@import url_launcher_ios;
|
||||||
|
#endif
|
||||||
|
|
||||||
@implementation GeneratedPluginRegistrant
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
@@ -49,8 +61,10 @@
|
|||||||
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
||||||
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
||||||
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
|
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
|
||||||
|
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
|
||||||
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
||||||
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||||
|
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Krow With Us Staff</string>
|
<string>Krow With Us Staff</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Krow With Us Staff</string>
|
<string>Krow With Us Staff</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
@@ -45,5 +45,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>DART_DEFINES</key>
|
||||||
|
<string>$(DART_DEFINES)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import firebase_auth
|
|||||||
import firebase_core
|
import firebase_core
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
||||||
@@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <geolocator_windows/geolocator_windows.h>
|
#include <geolocator_windows/geolocator_windows.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
||||||
@@ -20,4 +21,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
firebase_core
|
firebase_core
|
||||||
geolocator_windows
|
geolocator_windows
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU",
|
|
||||||
"GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0"
|
"GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0"
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,6 @@
|
|||||||
class AppConfig {
|
class AppConfig {
|
||||||
AppConfig._();
|
AppConfig._();
|
||||||
|
|
||||||
/// The Google Places API key used for address autocomplete functionality.
|
/// The Google Maps API key.
|
||||||
static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY');
|
|
||||||
|
|
||||||
/// The Google Maps Static API key used for location preview images.
|
|
||||||
static const String googleMapsApiKey = String.fromEnvironment('GOOGLE_MAPS_API_KEY');
|
static const String googleMapsApiKey = String.fromEnvironment('GOOGLE_MAPS_API_KEY');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -999,6 +999,14 @@
|
|||||||
"est_total": "Est. Total",
|
"est_total": "Est. Total",
|
||||||
"hours_label": "$count hours",
|
"hours_label": "$count hours",
|
||||||
"location": "LOCATION",
|
"location": "LOCATION",
|
||||||
|
"tbd": "TBD",
|
||||||
|
"get_direction": "Get direction",
|
||||||
|
"break_title": "BREAK",
|
||||||
|
"paid": "Paid",
|
||||||
|
"unpaid": "Unpaid",
|
||||||
|
"min": "min",
|
||||||
|
"hourly_rate": "Hourly Rate",
|
||||||
|
"hours": "Hours",
|
||||||
"open_in_maps": "Open in Maps",
|
"open_in_maps": "Open in Maps",
|
||||||
"job_description": "JOB DESCRIPTION",
|
"job_description": "JOB DESCRIPTION",
|
||||||
"cancel_shift": "CANCEL SHIFT",
|
"cancel_shift": "CANCEL SHIFT",
|
||||||
|
|||||||
@@ -999,6 +999,14 @@
|
|||||||
"est_total": "Total est.",
|
"est_total": "Total est.",
|
||||||
"hours_label": "$count horas",
|
"hours_label": "$count horas",
|
||||||
"location": "UBICACIÓN",
|
"location": "UBICACIÓN",
|
||||||
|
"tbd": "TBD",
|
||||||
|
"get_direction": "Obtener dirección",
|
||||||
|
"break_title": "DESCANSO",
|
||||||
|
"paid": "Pagado",
|
||||||
|
"unpaid": "No pagado",
|
||||||
|
"min": "min",
|
||||||
|
"hourly_rate": "Tarifa por hora",
|
||||||
|
"hours": "Horas",
|
||||||
"open_in_maps": "Abrir en Mapas",
|
"open_in_maps": "Abrir en Mapas",
|
||||||
"job_description": "DESCRIPCIÓN DEL TRABAJO",
|
"job_description": "DESCRIPCIÓN DEL TRABAJO",
|
||||||
"cancel_shift": "CANCELAR TURNO",
|
"cancel_shift": "CANCELAR TURNO",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class UiTheme {
|
|||||||
dividerTheme: const DividerThemeData(
|
dividerTheme: const DividerThemeData(
|
||||||
color: UiColors.separatorPrimary,
|
color: UiColors.separatorPrimary,
|
||||||
space: 1,
|
space: 1,
|
||||||
thickness: 1,
|
thickness: 0.5,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Card Theme
|
// Card Theme
|
||||||
|
|||||||
@@ -173,6 +173,14 @@ class UiTypography {
|
|||||||
color: UiColors.textPrimary,
|
color: UiColors.textPrimary,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Headline 1 Bold - Font: Instrument Sans, Size: 26, Height: 1.5 (#121826)
|
||||||
|
static final TextStyle headline1b = _primaryBase.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 26,
|
||||||
|
height: 1.5,
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
);
|
||||||
|
|
||||||
/// Headline 2 Medium - Font: Instrument Sans, Size: 20, Height: 1.5 (#121826)
|
/// Headline 2 Medium - Font: Instrument Sans, Size: 20, Height: 1.5 (#121826)
|
||||||
static final TextStyle headline2m = _primaryBase.copyWith(
|
static final TextStyle headline2m = _primaryBase.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export 'src/entities/events/work_session.dart';
|
|||||||
// Shifts
|
// Shifts
|
||||||
export 'src/entities/shifts/shift.dart';
|
export 'src/entities/shifts/shift.dart';
|
||||||
export 'src/adapters/shifts/shift_adapter.dart';
|
export 'src/adapters/shifts/shift_adapter.dart';
|
||||||
|
export 'src/entities/shifts/break/break.dart';
|
||||||
|
export 'src/adapters/shifts/break/break_adapter.dart';
|
||||||
|
|
||||||
// Orders & Requests
|
// Orders & Requests
|
||||||
export 'src/entities/orders/order_type.dart';
|
export 'src/entities/orders/order_type.dart';
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import '../../../entities/shifts/break/break.dart';
|
||||||
|
|
||||||
|
/// Adapter for Break related data.
|
||||||
|
class BreakAdapter {
|
||||||
|
/// Maps break data to a Break entity.
|
||||||
|
///
|
||||||
|
/// [isPaid] whether the break is paid.
|
||||||
|
/// [breakTime] the string representation of the break duration (e.g., 'MIN_10', 'MIN_30').
|
||||||
|
static Break fromData({
|
||||||
|
required bool isPaid,
|
||||||
|
required String? breakTime,
|
||||||
|
}) {
|
||||||
|
return Break(
|
||||||
|
isBreakPaid: isPaid,
|
||||||
|
duration: _parseDuration(breakTime),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BreakDuration _parseDuration(String? breakTime) {
|
||||||
|
if (breakTime == null) return BreakDuration.none;
|
||||||
|
|
||||||
|
switch (breakTime.toUpperCase()) {
|
||||||
|
case 'MIN_10':
|
||||||
|
return BreakDuration.ten;
|
||||||
|
case 'MIN_15':
|
||||||
|
return BreakDuration.fifteen;
|
||||||
|
case 'MIN_20':
|
||||||
|
return BreakDuration.twenty;
|
||||||
|
case 'MIN_30':
|
||||||
|
return BreakDuration.thirty;
|
||||||
|
case 'MIN_45':
|
||||||
|
return BreakDuration.fortyFive;
|
||||||
|
case 'MIN_60':
|
||||||
|
return BreakDuration.sixty;
|
||||||
|
default:
|
||||||
|
return BreakDuration.none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Enum representing common break durations in minutes.
|
||||||
|
enum BreakDuration {
|
||||||
|
/// No break.
|
||||||
|
none(0),
|
||||||
|
|
||||||
|
/// 10 minutes break.
|
||||||
|
ten(10),
|
||||||
|
|
||||||
|
/// 15 minutes break.
|
||||||
|
fifteen(15),
|
||||||
|
|
||||||
|
/// 20 minutes break.
|
||||||
|
twenty(20),
|
||||||
|
|
||||||
|
/// 30 minutes break.
|
||||||
|
thirty(30),
|
||||||
|
|
||||||
|
/// 45 minutes break.
|
||||||
|
fortyFive(45),
|
||||||
|
|
||||||
|
/// 60 minutes break.
|
||||||
|
sixty(60);
|
||||||
|
|
||||||
|
/// The duration in minutes.
|
||||||
|
final int minutes;
|
||||||
|
|
||||||
|
const BreakDuration(this.minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a break configuration for a shift.
|
||||||
|
class Break extends Equatable {
|
||||||
|
const Break({
|
||||||
|
required this.duration,
|
||||||
|
required this.isBreakPaid,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The duration of the break.
|
||||||
|
final BreakDuration duration;
|
||||||
|
|
||||||
|
/// Whether the break is paid or unpaid.
|
||||||
|
final bool isBreakPaid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[duration, isBreakPaid];
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/src/entities/shifts/break/break.dart';
|
||||||
|
|
||||||
class Shift extends Equatable {
|
class Shift extends Equatable {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -29,6 +30,7 @@ class Shift extends Equatable {
|
|||||||
final String? roleId;
|
final String? roleId;
|
||||||
final bool? hasApplied;
|
final bool? hasApplied;
|
||||||
final double? totalValue;
|
final double? totalValue;
|
||||||
|
final Break? breakInfo;
|
||||||
|
|
||||||
const Shift({
|
const Shift({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -59,48 +61,49 @@ class Shift extends Equatable {
|
|||||||
this.roleId,
|
this.roleId,
|
||||||
this.hasApplied,
|
this.hasApplied,
|
||||||
this.totalValue,
|
this.totalValue,
|
||||||
|
this.breakInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => <Object?>[
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
clientName,
|
clientName,
|
||||||
logoUrl,
|
logoUrl,
|
||||||
hourlyRate,
|
hourlyRate,
|
||||||
location,
|
location,
|
||||||
locationAddress,
|
locationAddress,
|
||||||
date,
|
date,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
createdDate,
|
createdDate,
|
||||||
tipsAvailable,
|
tipsAvailable,
|
||||||
travelTime,
|
travelTime,
|
||||||
mealProvided,
|
mealProvided,
|
||||||
parkingAvailable,
|
parkingAvailable,
|
||||||
gasCompensation,
|
gasCompensation,
|
||||||
description,
|
description,
|
||||||
instructions,
|
instructions,
|
||||||
managers,
|
managers,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
status,
|
status,
|
||||||
durationDays,
|
durationDays,
|
||||||
requiredSlots,
|
requiredSlots,
|
||||||
filledSlots,
|
filledSlots,
|
||||||
roleId,
|
roleId,
|
||||||
hasApplied,
|
hasApplied,
|
||||||
totalValue,
|
totalValue,
|
||||||
];
|
breakInfo,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShiftManager extends Equatable {
|
class ShiftManager extends Equatable {
|
||||||
|
const ShiftManager({required this.name, required this.phone, this.avatar});
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final String phone;
|
final String phone;
|
||||||
final String? avatar;
|
final String? avatar;
|
||||||
|
|
||||||
const ShiftManager({required this.name, required this.phone, this.avatar});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [name, phone, avatar];
|
List<Object?> get props => <Object?>[name, phone, avatar];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ class HubRepositoryImpl
|
|||||||
<String, String>{
|
<String, String>{
|
||||||
'place_id': placeId,
|
'place_id': placeId,
|
||||||
'fields': 'address_component',
|
'fields': 'address_component',
|
||||||
'key': AppConfig.googlePlacesApiKey,
|
'key': AppConfig.googleMapsApiKey,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class HubAddressAutocomplete extends StatelessWidget {
|
|||||||
return GooglePlaceAutoCompleteTextField(
|
return GooglePlaceAutoCompleteTextField(
|
||||||
textEditingController: controller,
|
textEditingController: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
googleAPIKey: AppConfig.googlePlacesApiKey,
|
googleAPIKey: AppConfig.googleMapsApiKey,
|
||||||
debounceTime: 500,
|
debounceTime: 500,
|
||||||
countries: HubsConstants.supportedCountries,
|
countries: HubsConstants.supportedCountries,
|
||||||
isLatLngRequired: true,
|
isLatLngRequired: true,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../../domain/repositories/place_repository.dart';
|
import '../../domain/repositories/place_repository.dart';
|
||||||
|
|
||||||
class PlaceRepositoryImpl implements PlaceRepository {
|
class PlaceRepositoryImpl implements PlaceRepository {
|
||||||
@@ -18,7 +20,7 @@ class PlaceRepositoryImpl implements PlaceRepository {
|
|||||||
<String, String>{
|
<String, String>{
|
||||||
'input': query,
|
'input': query,
|
||||||
'types': '(cities)',
|
'types': '(cities)',
|
||||||
'key': AppConfig.googlePlacesApiKey,
|
'key': AppConfig.googleMapsApiKey,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -84,13 +84,6 @@ class _ShiftCardState extends State<ShiftCard> {
|
|||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: UiColors.black.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 2,
|
|
||||||
offset: const Offset(0, 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ class ShiftsRepositoryImpl
|
|||||||
requiredSlots: app.shiftRole.count,
|
requiredSlots: app.shiftRole.count,
|
||||||
filledSlots: app.shiftRole.assigned ?? 0,
|
filledSlots: app.shiftRole.assigned ?? 0,
|
||||||
hasApplied: true,
|
hasApplied: true,
|
||||||
|
latitude: app.shift.latitude,
|
||||||
|
longitude: app.shift.longitude,
|
||||||
|
breakInfo: BreakAdapter.fromData(
|
||||||
|
isPaid: app.shiftRole.isBreakPaid ?? false,
|
||||||
|
breakTime: app.shiftRole.breakType?.stringValue,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -208,6 +214,12 @@ class ShiftsRepositoryImpl
|
|||||||
requiredSlots: app.shiftRole.count,
|
requiredSlots: app.shiftRole.count,
|
||||||
filledSlots: app.shiftRole.assigned ?? 0,
|
filledSlots: app.shiftRole.assigned ?? 0,
|
||||||
hasApplied: true,
|
hasApplied: true,
|
||||||
|
latitude: app.shift.latitude,
|
||||||
|
longitude: app.shift.longitude,
|
||||||
|
breakInfo: BreakAdapter.fromData(
|
||||||
|
isPaid: app.shiftRole.isBreakPaid ?? false,
|
||||||
|
breakTime: app.shiftRole.breakType?.stringValue,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -277,6 +289,12 @@ class ShiftsRepositoryImpl
|
|||||||
durationDays: sr.shift.durationDays,
|
durationDays: sr.shift.durationDays,
|
||||||
requiredSlots: sr.count,
|
requiredSlots: sr.count,
|
||||||
filledSlots: sr.assigned ?? 0,
|
filledSlots: sr.assigned ?? 0,
|
||||||
|
latitude: sr.shift.latitude,
|
||||||
|
longitude: sr.shift.longitude,
|
||||||
|
breakInfo: BreakAdapter.fromData(
|
||||||
|
isPaid: sr.isBreakPaid ?? false,
|
||||||
|
breakTime: sr.breakType?.stringValue,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -350,6 +368,12 @@ class ShiftsRepositoryImpl
|
|||||||
filledSlots: sr.assigned ?? 0,
|
filledSlots: sr.assigned ?? 0,
|
||||||
hasApplied: hasApplied,
|
hasApplied: hasApplied,
|
||||||
totalValue: sr.totalValue,
|
totalValue: sr.totalValue,
|
||||||
|
latitude: sr.shift.latitude,
|
||||||
|
longitude: sr.shift.longitude,
|
||||||
|
breakInfo: BreakAdapter.fromData(
|
||||||
|
isPaid: sr.isBreakPaid ?? false,
|
||||||
|
breakTime: sr.breakType?.stringValue,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,6 +384,7 @@ class ShiftsRepositoryImpl
|
|||||||
|
|
||||||
int? required;
|
int? required;
|
||||||
int? filled;
|
int? filled;
|
||||||
|
Break? breakInfo;
|
||||||
try {
|
try {
|
||||||
final rolesRes = await executeProtected(() =>
|
final rolesRes = await executeProtected(() =>
|
||||||
_dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute());
|
_dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute());
|
||||||
@@ -370,6 +395,12 @@ class ShiftsRepositoryImpl
|
|||||||
required = (required ?? 0) + r.count;
|
required = (required ?? 0) + r.count;
|
||||||
filled = (filled ?? 0) + (r.assigned ?? 0);
|
filled = (filled ?? 0) + (r.assigned ?? 0);
|
||||||
}
|
}
|
||||||
|
// Use the first role's break info as a representative
|
||||||
|
final firstRole = rolesRes.data.shiftRoles.first;
|
||||||
|
breakInfo = BreakAdapter.fromData(
|
||||||
|
isPaid: firstRole.isBreakPaid ?? false,
|
||||||
|
breakTime: firstRole.breakType?.stringValue,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
@@ -394,6 +425,9 @@ class ShiftsRepositoryImpl
|
|||||||
durationDays: s.durationDays,
|
durationDays: s.durationDays,
|
||||||
requiredSlots: required,
|
requiredSlots: required,
|
||||||
filledSlots: filled,
|
filledSlots: filled,
|
||||||
|
latitude: s.latitude,
|
||||||
|
longitude: s.longitude,
|
||||||
|
breakInfo: breakInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,23 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../blocs/shift_details/shift_details_bloc.dart';
|
import '../blocs/shift_details/shift_details_bloc.dart';
|
||||||
import '../blocs/shift_details/shift_details_event.dart';
|
import '../blocs/shift_details/shift_details_event.dart';
|
||||||
import '../blocs/shift_details/shift_details_state.dart';
|
import '../blocs/shift_details/shift_details_state.dart';
|
||||||
import '../widgets/shift_location_map.dart';
|
import '../widgets/shift_details/shift_break_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_date_time_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_description_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_details_bottom_bar.dart';
|
||||||
|
import '../widgets/shift_details/shift_details_header.dart';
|
||||||
|
import '../widgets/shift_details/shift_location_section.dart';
|
||||||
|
import '../widgets/shift_details/shift_stats_row.dart';
|
||||||
|
|
||||||
class ShiftDetailsPage extends StatefulWidget {
|
class ShiftDetailsPage extends StatefulWidget {
|
||||||
final String shiftId;
|
final String shiftId;
|
||||||
final Shift? shift;
|
final Shift shift;
|
||||||
|
|
||||||
const ShiftDetailsPage({super.key, required this.shiftId, this.shift});
|
const ShiftDetailsPage({
|
||||||
|
super.key,
|
||||||
|
required this.shiftId,
|
||||||
|
required this.shift,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ShiftDetailsPage> createState() => _ShiftDetailsPageState();
|
State<ShiftDetailsPage> createState() => _ShiftDetailsPageState();
|
||||||
@@ -64,75 +74,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard(IconData icon, String value, String label) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.background,
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
|
||||||
border: Border.all(color: UiColors.border),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: UiColors.white,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(icon, size: 20, color: UiColors.textSecondary),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTimeBox(String label, String time) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.background,
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: UiTypography.footnote2b.copyWith(
|
|
||||||
color: UiColors.textSecondary, letterSpacing: 0.5),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
_formatTime(time),
|
|
||||||
style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ShiftDetailsBloc>(
|
return BlocProvider<ShiftDetailsBloc>(
|
||||||
create: (_) =>
|
create: (_) => Modular.get<ShiftDetailsBloc>()
|
||||||
Modular.get<ShiftDetailsBloc>()
|
..add(
|
||||||
..add(
|
LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId),
|
||||||
LoadShiftDetailsEvent(
|
),
|
||||||
widget.shiftId,
|
child: BlocConsumer<ShiftDetailsBloc, ShiftDetailsState>(
|
||||||
roleId: widget.shift?.roleId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is ShiftActionSuccess || state is ShiftDetailsError) {
|
if (state is ShiftActionSuccess || state is ShiftDetailsError) {
|
||||||
_closeActionDialog(context);
|
_closeActionDialog(context);
|
||||||
@@ -146,7 +95,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
);
|
);
|
||||||
Modular.to.toShifts(selectedDate: state.shiftDate);
|
Modular.to.toShifts(selectedDate: state.shiftDate);
|
||||||
} else if (state is ShiftDetailsError) {
|
} else if (state is ShiftDetailsError) {
|
||||||
if (_isApplying || widget.shift == null) {
|
if (_isApplying) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: translateErrorKey(state.message),
|
message: translateErrorKey(state.message),
|
||||||
@@ -156,299 +105,107 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
_isApplying = false;
|
_isApplying = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: BlocBuilder<ShiftDetailsBloc, ShiftDetailsState>(
|
builder: (context, state) {
|
||||||
builder: (context, state) {
|
if (state is ShiftDetailsLoading) {
|
||||||
if (state is ShiftDetailsLoading) {
|
return const Scaffold(
|
||||||
return const Scaffold(
|
body: Center(child: CircularProgressIndicator()),
|
||||||
body: Center(child: CircularProgressIndicator()),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Shift? displayShift;
|
Shift displayShift = widget.shift;
|
||||||
if (state is ShiftDetailsLoaded) {
|
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||||
displayShift = state.shift;
|
|
||||||
} else {
|
|
||||||
displayShift = widget.shift;
|
|
||||||
}
|
|
||||||
|
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
final duration = _calculateDuration(displayShift);
|
||||||
if (displayShift == null) {
|
final estimatedTotal =
|
||||||
return Scaffold(
|
displayShift.totalValue ?? (displayShift.hourlyRate * duration);
|
||||||
body: Center(child: Text(Translations.of(context).staff_shifts.list.no_shifts)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final duration = _calculateDuration(displayShift);
|
return Scaffold(
|
||||||
final estimatedTotal =
|
appBar: UiAppBar(
|
||||||
displayShift.totalValue ?? (displayShift.hourlyRate * duration);
|
centerTitle: false,
|
||||||
final openSlots =
|
onLeadingPressed: () => Modular.to.toShifts(),
|
||||||
(displayShift.requiredSlots ?? 0) -
|
),
|
||||||
(displayShift.filledSlots ?? 0);
|
body: Column(
|
||||||
|
children: [
|
||||||
return Scaffold(
|
Expanded(
|
||||||
appBar: UiAppBar(
|
child: SingleChildScrollView(
|
||||||
title: displayShift.title,
|
child: Column(
|
||||||
centerTitle: false,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
onLeadingPressed: () => Modular.to.toShifts(),
|
children: [
|
||||||
),
|
ShiftDetailsHeader(shift: displayShift),
|
||||||
body: Column(
|
const Divider(height: 1, thickness: 0.5),
|
||||||
children: [
|
ShiftStatsRow(
|
||||||
Expanded(
|
estimatedTotal: estimatedTotal,
|
||||||
child: SingleChildScrollView(
|
hourlyRate: displayShift.hourlyRate,
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
duration: duration,
|
||||||
child: Column(
|
totalLabel: i18n.est_total,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
hourlyRateLabel: i18n.hourly_rate,
|
||||||
children: [
|
hoursLabel: i18n.hours,
|
||||||
// Vendor Section
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
i18n.vendor,
|
|
||||||
style: UiTypography.titleUppercase4b.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: displayShift.logoUrl != null
|
|
||||||
? ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
UiConstants.radiusMdValue,
|
|
||||||
),
|
|
||||||
child: Image.network(
|
|
||||||
displayShift.logoUrl!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Center(
|
|
||||||
child: Icon(
|
|
||||||
UiIcons.briefcase,
|
|
||||||
color: UiColors.primary,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
displayShift.clientName,
|
|
||||||
style: UiTypography.headline5m.textPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
// Date Section
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
i18n.shift_date,
|
|
||||||
style: UiTypography.titleUppercase4b.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
UiIcons.calendar,
|
|
||||||
size: 20,
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
_formatDate(displayShift.date),
|
|
||||||
style: UiTypography.headline5m.textPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
// Stats Row (New)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.dollar,
|
|
||||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
|
||||||
"Total",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.dollar,
|
|
||||||
"\$${displayShift.hourlyRate.toStringAsFixed(0)}",
|
|
||||||
"Hourly Rate",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.clock,
|
|
||||||
"${duration.toStringAsFixed(1)}",
|
|
||||||
"Hours",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
// Time Section (New)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildTimeBox(
|
|
||||||
"CLOCK IN TIME",
|
|
||||||
displayShift.startTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: _buildTimeBox(
|
|
||||||
"CLOCK OUT TIME",
|
|
||||||
displayShift.endTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space6),
|
|
||||||
|
|
||||||
|
|
||||||
// Location Section (New with Map)
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"LOCATION",
|
|
||||||
style: UiTypography.titleUppercase4b.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
displayShift.location.isEmpty
|
|
||||||
? "TBD"
|
|
||||||
: displayShift.location,
|
|
||||||
style: UiTypography.title1m.textPrimary,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
displayShift!.locationAddress.isNotEmpty
|
|
||||||
? displayShift!.locationAddress
|
|
||||||
: displayShift!.location,
|
|
||||||
),
|
|
||||||
duration: const Duration(
|
|
||||||
seconds: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(
|
|
||||||
UiIcons.navigation,
|
|
||||||
size: UiConstants.iconXs,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
"Get direction",
|
|
||||||
),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor:
|
|
||||||
UiColors.textPrimary,
|
|
||||||
side: const BorderSide(
|
|
||||||
color: UiColors.border,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
UiConstants.radiusBase,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space3,
|
|
||||||
vertical: 0,
|
|
||||||
),
|
|
||||||
minimumSize: const Size(0, 32),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
|
||||||
ShiftLocationMap(
|
|
||||||
shift: displayShift,
|
|
||||||
height: 160,
|
|
||||||
borderRadius: UiConstants.radiusBase,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space8),
|
|
||||||
|
|
||||||
// Description / Instructions
|
|
||||||
if ((displayShift.description ?? '').isNotEmpty) ...[
|
|
||||||
Text(
|
|
||||||
i18n.job_description,
|
|
||||||
style: UiTypography.titleUppercase4b.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
displayShift.description!,
|
|
||||||
style: UiTypography.body2r.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space8),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Bottom Action Bar
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
UiConstants.space5,
|
|
||||||
UiConstants.space4,
|
|
||||||
UiConstants.space5,
|
|
||||||
MediaQuery.of(context).padding.bottom + UiConstants.space4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.white,
|
|
||||||
border: Border(top: BorderSide(color: UiColors.border)),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: UiColors.black.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, -4),
|
|
||||||
),
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
ShiftDateTimeSection(
|
||||||
|
date: displayShift.date,
|
||||||
|
startTime: displayShift.startTime,
|
||||||
|
endTime: displayShift.endTime,
|
||||||
|
shiftDateLabel: i18n.shift_date,
|
||||||
|
clockInLabel: i18n.start_time,
|
||||||
|
clockOutLabel: i18n.end_time,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
if (displayShift.breakInfo != null &&
|
||||||
|
displayShift.breakInfo!.duration !=
|
||||||
|
BreakDuration.none) ...[
|
||||||
|
ShiftBreakSection(
|
||||||
|
breakInfo: displayShift.breakInfo!,
|
||||||
|
breakTitle: i18n.break_title,
|
||||||
|
paidLabel: i18n.paid,
|
||||||
|
unpaidLabel: i18n.unpaid,
|
||||||
|
minLabel: i18n.min,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
],
|
||||||
|
ShiftLocationSection(
|
||||||
|
shift: displayShift,
|
||||||
|
locationLabel: i18n.location,
|
||||||
|
tbdLabel: i18n.tbd,
|
||||||
|
getDirectionLabel: i18n.get_direction,
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
if (displayShift.description != null &&
|
||||||
|
displayShift.description!.isNotEmpty)
|
||||||
|
ShiftDescriptionSection(
|
||||||
|
description: displayShift.description!,
|
||||||
|
descriptionLabel: i18n.job_description,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: _buildBottomButton(displayShift, context),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
ShiftDetailsBottomBar(
|
||||||
);
|
shift: displayShift,
|
||||||
},
|
onApply: () => _bookShift(context, displayShift),
|
||||||
),
|
onDecline: () => BlocProvider.of<ShiftDetailsBloc>(
|
||||||
|
context,
|
||||||
|
).add(DeclineShiftDetailsEvent(displayShift.id)),
|
||||||
|
onAccept: () =>
|
||||||
|
BlocProvider.of<ShiftDetailsBloc>(context).add(
|
||||||
|
BookShiftDetailsEvent(
|
||||||
|
displayShift.id,
|
||||||
|
roleId: displayShift.roleId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _bookShift(
|
void _bookShift(BuildContext context, Shift shift) {
|
||||||
BuildContext context,
|
final i18n = Translations.of(
|
||||||
Shift shift,
|
context,
|
||||||
) {
|
).staff_shifts.shift_details.book_dialog;
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details.book_dialog;
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
@@ -471,38 +228,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(foregroundColor: UiColors.success),
|
||||||
foregroundColor: UiColors.success,
|
child: Text(
|
||||||
|
Translations.of(context).staff_shifts.shift_details.apply_now,
|
||||||
),
|
),
|
||||||
child: Text(Translations.of(context).staff_shifts.shift_details.apply_now),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _declineShift(BuildContext context, String id) {
|
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details.decline_dialog;
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (ctx) => AlertDialog(
|
|
||||||
title: Text(i18n.title),
|
|
||||||
content: Text(i18n.message),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Modular.to.pop(),
|
|
||||||
child: Text(Translations.of(context).common.cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
BlocProvider.of<ShiftDetailsBloc>(
|
|
||||||
context,
|
|
||||||
).add(DeclineShiftDetailsEvent(id));
|
|
||||||
},
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.destructive,
|
|
||||||
),
|
|
||||||
child: Text(Translations.of(context).staff_shifts.shift_details.decline),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -513,7 +242,9 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
if (_actionDialogOpen) return;
|
if (_actionDialogOpen) return;
|
||||||
_actionDialogOpen = true;
|
_actionDialogOpen = true;
|
||||||
_isApplying = true;
|
_isApplying = true;
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details.applying_dialog;
|
final i18n = Translations.of(
|
||||||
|
context,
|
||||||
|
).staff_shifts.shift_details.applying_dialog;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
@@ -561,105 +292,4 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
Navigator.of(context, rootNavigator: true).pop();
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
_actionDialogOpen = false;
|
_actionDialogOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomButton(Shift shift, BuildContext context) {
|
|
||||||
final String status = shift.status ?? 'open';
|
|
||||||
|
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
|
||||||
if (status == 'confirmed') {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => Modular.to.toClockIn(),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: UiColors.success,
|
|
||||||
foregroundColor: UiColors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: Text(i18n.clock_in, style: UiTypography.body2b.white),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == 'pending') {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () => BlocProvider.of<ShiftDetailsBloc>(context).add(DeclineShiftDetailsEvent(shift.id)),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.destructive,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
|
||||||
side: const BorderSide(color: UiColors.destructive),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(i18n.decline, style: UiTypography.body2b.textError),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => BlocProvider.of<ShiftDetailsBloc>(context).add(BookShiftDetailsEvent(shift.id, roleId: shift.roleId)),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: UiColors.primary,
|
|
||||||
foregroundColor: UiColors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: Text(i18n.accept_shift, style: UiTypography.body2b.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == 'open' || status == 'available') {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () => _declineShift(context, shift.id),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.textSecondary,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
|
||||||
side: const BorderSide(color: UiColors.border),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(i18n.decline, style: UiTypography.body2b.textSecondary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => _bookShift(context, shift),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: UiColors.primary,
|
|
||||||
foregroundColor: UiColors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: Text(i18n.apply_now, style: UiTypography.body2b.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_activeTab = widget.initialTab ?? 'myshifts';
|
_activeTab = widget.initialTab ?? 'myshifts';
|
||||||
_selectedDate = widget.selectedDate;
|
_selectedDate = widget.selectedDate;
|
||||||
print('ShiftsPage init: initialTab=$_activeTab');
|
|
||||||
_prioritizeFind = widget.initialTab == 'find';
|
_prioritizeFind = widget.initialTab == 'find';
|
||||||
if (_prioritizeFind) {
|
if (_prioritizeFind) {
|
||||||
_bloc.add(LoadFindFirstEvent());
|
_bloc.add(LoadFindFirstEvent());
|
||||||
@@ -37,11 +36,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
_bloc.add(LoadShiftsEvent());
|
_bloc.add(LoadShiftsEvent());
|
||||||
}
|
}
|
||||||
if (_activeTab == 'history') {
|
if (_activeTab == 'history') {
|
||||||
print('ShiftsPage init: loading history tab');
|
|
||||||
_bloc.add(LoadHistoryShiftsEvent());
|
_bloc.add(LoadHistoryShiftsEvent());
|
||||||
}
|
}
|
||||||
if (_activeTab == 'find') {
|
if (_activeTab == 'find') {
|
||||||
print('ShiftsPage init: entering find tab (not loaded yet)');
|
|
||||||
if (!_prioritizeFind) {
|
if (!_prioritizeFind) {
|
||||||
_bloc.add(LoadAvailableShiftsEvent());
|
_bloc.add(LoadAvailableShiftsEvent());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:design_system/design_system.dart';
|
|
||||||
|
|
||||||
class AppColors {
|
|
||||||
static const Color krowBlue = UiColors.primary;
|
|
||||||
static const Color krowYellow = UiColors.accent;
|
|
||||||
static const Color krowCharcoal = UiColors.textPrimary;
|
|
||||||
static const Color krowMuted = UiColors.textSecondary;
|
|
||||||
static const Color krowBorder = UiColors.border;
|
|
||||||
static const Color krowBackground = UiColors.background;
|
|
||||||
static const Color white = UiColors.white;
|
|
||||||
static const Color black = UiColors.black;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
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 '../../styles/shifts_styles.dart';
|
|
||||||
|
|
||||||
class EmptyStateView extends StatelessWidget {
|
class EmptyStateView extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// A section displaying shift break details (duration and payment status).
|
||||||
|
class ShiftBreakSection extends StatelessWidget {
|
||||||
|
/// The break information.
|
||||||
|
final Break breakInfo;
|
||||||
|
|
||||||
|
/// Localization string for break section title.
|
||||||
|
final String breakTitle;
|
||||||
|
|
||||||
|
/// Localization string for paid status.
|
||||||
|
final String paidLabel;
|
||||||
|
|
||||||
|
/// Localization string for unpaid status.
|
||||||
|
final String unpaidLabel;
|
||||||
|
|
||||||
|
/// Localization string for minutes ("min").
|
||||||
|
final String minLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftBreakSection].
|
||||||
|
const ShiftBreakSection({
|
||||||
|
super.key,
|
||||||
|
required this.breakInfo,
|
||||||
|
required this.breakTitle,
|
||||||
|
required this.paidLabel,
|
||||||
|
required this.unpaidLabel,
|
||||||
|
required this.minLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
breakTitle,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.breakIcon,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
"${breakInfo.duration.minutes} $minLabel (${breakInfo.isBreakPaid ? paidLabel : unpaidLabel})",
|
||||||
|
style: UiTypography.headline5m.textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// A section displaying the date and the shift's start/end times.
|
||||||
|
class ShiftDateTimeSection extends StatelessWidget {
|
||||||
|
/// The ISO string of the date.
|
||||||
|
final String date;
|
||||||
|
|
||||||
|
/// The start time string (HH:mm).
|
||||||
|
final String startTime;
|
||||||
|
|
||||||
|
/// The end time string (HH:mm).
|
||||||
|
final String endTime;
|
||||||
|
|
||||||
|
/// Localization string for shift date.
|
||||||
|
final String shiftDateLabel;
|
||||||
|
|
||||||
|
/// Localization string for clock in time.
|
||||||
|
final String clockInLabel;
|
||||||
|
|
||||||
|
/// Localization string for clock out time.
|
||||||
|
final String clockOutLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDateTimeSection].
|
||||||
|
const ShiftDateTimeSection({
|
||||||
|
super.key,
|
||||||
|
required this.date,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
required this.shiftDateLabel,
|
||||||
|
required this.clockInLabel,
|
||||||
|
required this.clockOutLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
String _formatTime(String time) {
|
||||||
|
if (time.isEmpty) return '';
|
||||||
|
try {
|
||||||
|
final parts = time.split(':');
|
||||||
|
final hour = int.parse(parts[0]);
|
||||||
|
final minute = int.parse(parts[1]);
|
||||||
|
final dt = DateTime(2022, 1, 1, hour, minute);
|
||||||
|
return DateFormat('h:mm a').format(dt);
|
||||||
|
} catch (e) {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(String dateStr) {
|
||||||
|
if (dateStr.isEmpty) return '';
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(dateStr);
|
||||||
|
return DateFormat('EEEE, MMMM d, y').format(date);
|
||||||
|
} catch (e) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
shiftDateLabel,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.calendar,
|
||||||
|
size: 20,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
_formatDate(date),
|
||||||
|
style: UiTypography.headline5m.textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
clockInLabel,
|
||||||
|
startTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
clockOutLabel,
|
||||||
|
endTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeBox(String label, String time) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgThird,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
_formatTime(time),
|
||||||
|
style: UiTypography.title1m
|
||||||
|
.copyWith(fontWeight: FontWeight.w700)
|
||||||
|
.textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A section displaying the job description for the shift.
|
||||||
|
class ShiftDescriptionSection extends StatelessWidget {
|
||||||
|
/// The description text.
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
/// Localization string for description section title.
|
||||||
|
final String descriptionLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDescriptionSection].
|
||||||
|
const ShiftDescriptionSection({
|
||||||
|
super.key,
|
||||||
|
required this.description,
|
||||||
|
required this.descriptionLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (description.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
descriptionLabel,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
/// A bottom action bar containing contextual buttons based on shift status.
|
||||||
|
class ShiftDetailsBottomBar extends StatelessWidget {
|
||||||
|
/// The current shift.
|
||||||
|
final Shift shift;
|
||||||
|
|
||||||
|
/// Callback for applying/booking a shift.
|
||||||
|
final VoidCallback onApply;
|
||||||
|
|
||||||
|
/// Callback for declining a shift.
|
||||||
|
final VoidCallback onDecline;
|
||||||
|
|
||||||
|
/// Callback for accepting a shift.
|
||||||
|
final VoidCallback onAccept;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDetailsBottomBar].
|
||||||
|
const ShiftDetailsBottomBar({
|
||||||
|
super.key,
|
||||||
|
required this.shift,
|
||||||
|
required this.onApply,
|
||||||
|
required this.onDecline,
|
||||||
|
required this.onAccept,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final String status = shift.status ?? 'open';
|
||||||
|
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
UiConstants.space5,
|
||||||
|
UiConstants.space4,
|
||||||
|
UiConstants.space5,
|
||||||
|
MediaQuery.of(context).padding.bottom + UiConstants.space4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
border: Border(top: BorderSide(color: UiColors.border)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.popupShadow.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: _buildButtons(status, i18n, context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButtons(String status, dynamic i18n, BuildContext context) {
|
||||||
|
if (status == 'confirmed') {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Modular.to.toClockIn(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.success,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(i18n.clock_in, style: UiTypography.body2b.white),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == 'pending') {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: onDecline,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: UiColors.destructive,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
),
|
||||||
|
side: const BorderSide(color: UiColors.destructive),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(i18n.decline, style: UiTypography.body2b.textError),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onAccept,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space4,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(i18n.accept_shift, style: UiTypography.body2b.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == 'open' || status == 'available') {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: onApply,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(i18n.apply_now, style: UiTypography.body2b.white),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// A header widget for the shift details page displaying the role, client name, and address.
|
||||||
|
class ShiftDetailsHeader extends StatelessWidget {
|
||||||
|
/// The shift entity containing the header information.
|
||||||
|
final Shift shift;
|
||||||
|
|
||||||
|
/// Creates a [ShiftDetailsHeader].
|
||||||
|
const ShiftDetailsHeader({
|
||||||
|
super.key,
|
||||||
|
required this.shift,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
spacing: UiConstants.space4,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 114,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withAlpha(20),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
border: Border.all(color: UiColors.primary),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.briefcase,
|
||||||
|
color: UiColors.primary,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
shift.title,
|
||||||
|
style: UiTypography.headline1b.textPrimary,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
spacing: UiConstants.space1,
|
||||||
|
children: [
|
||||||
|
// Client name
|
||||||
|
Row(
|
||||||
|
spacing: UiConstants.space1,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.building,
|
||||||
|
size: 16,
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
shift.clientName,
|
||||||
|
style: UiTypography.body1m.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Location address (if available)
|
||||||
|
Row(
|
||||||
|
spacing: UiConstants.space1,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.mapPin,
|
||||||
|
size: 16,
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
shift.locationAddress,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
|
||||||
|
/// A widget that displays the shift location on an interactive Google Map.
|
||||||
|
class ShiftLocationMap extends StatefulWidget {
|
||||||
|
/// The shift entity containing location and coordinates.
|
||||||
|
final Shift shift;
|
||||||
|
|
||||||
|
/// The height of the map widget.
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
/// The border radius for the map container.
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
/// Creates a [ShiftLocationMap].
|
||||||
|
const ShiftLocationMap({
|
||||||
|
super.key,
|
||||||
|
required this.shift,
|
||||||
|
this.height = 120,
|
||||||
|
this.borderRadius = 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShiftLocationMap> createState() => _ShiftLocationMapState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShiftLocationMapState extends State<ShiftLocationMap> {
|
||||||
|
late final CameraPosition _initialPosition;
|
||||||
|
final Set<Marker> _markers = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// Default to a fallback coordinate if latitude/longitude are null.
|
||||||
|
// In a real app, you might want to geocode the address if coordinates are missing.
|
||||||
|
final double lat = widget.shift.latitude ?? 0.0;
|
||||||
|
final double lng = widget.shift.longitude ?? 0.0;
|
||||||
|
|
||||||
|
final LatLng position = LatLng(lat, lng);
|
||||||
|
|
||||||
|
_initialPosition = CameraPosition(
|
||||||
|
target: position,
|
||||||
|
zoom: 15,
|
||||||
|
);
|
||||||
|
|
||||||
|
_markers.add(
|
||||||
|
Marker(
|
||||||
|
markerId: MarkerId(widget.shift.id),
|
||||||
|
position: position,
|
||||||
|
infoWindow: InfoWindow(
|
||||||
|
title: widget.shift.location,
|
||||||
|
snippet: widget.shift.locationAddress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// If coordinates are missing, we show a placeholder.
|
||||||
|
if (widget.shift.latitude == null || widget.shift.longitude == null) {
|
||||||
|
return _buildPlaceholder(context, "Coordinates unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: widget.height * 1.25, // Slightly taller to accommodate map controls
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: GoogleMap(
|
||||||
|
initialCameraPosition: _initialPosition,
|
||||||
|
markers: _markers,
|
||||||
|
liteModeEnabled: true, // Optimized for static-like display in details page
|
||||||
|
scrollGesturesEnabled: false,
|
||||||
|
zoomGesturesEnabled: true,
|
||||||
|
tiltGesturesEnabled: false,
|
||||||
|
rotateGesturesEnabled: false,
|
||||||
|
myLocationButtonEnabled: false,
|
||||||
|
mapToolbarEnabled: false,
|
||||||
|
compassEnabled: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPlaceholder(BuildContext context, String message) {
|
||||||
|
return Container(
|
||||||
|
height: widget.height,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgThird,
|
||||||
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.mapPin,
|
||||||
|
size: 32,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
if (message.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'shift_location_map.dart';
|
||||||
|
|
||||||
|
/// A section displaying the shift's location, address, map, and "Get direction" action.
|
||||||
|
class ShiftLocationSection extends StatelessWidget {
|
||||||
|
/// The shift entity containing location data.
|
||||||
|
final Shift shift;
|
||||||
|
|
||||||
|
/// Localization string for location section title.
|
||||||
|
final String locationLabel;
|
||||||
|
|
||||||
|
/// Localization string for "TBD".
|
||||||
|
final String tbdLabel;
|
||||||
|
|
||||||
|
/// Localization string for "Get direction".
|
||||||
|
final String getDirectionLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftLocationSection].
|
||||||
|
const ShiftLocationSection({
|
||||||
|
super.key,
|
||||||
|
required this.shift,
|
||||||
|
required this.locationLabel,
|
||||||
|
required this.tbdLabel,
|
||||||
|
required this.getDirectionLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: UiConstants.space4,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
locationLabel,
|
||||||
|
style: UiTypography.titleUppercase4b.textSecondary,
|
||||||
|
),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: UiConstants.space4,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
shift.location.isEmpty ? tbdLabel : shift.location,
|
||||||
|
style: UiTypography.title1m.textPrimary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (shift.locationAddress.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
shift.locationAddress,
|
||||||
|
style: UiTypography.body2r.textSecondary,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => _openDirections(context),
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.navigation,
|
||||||
|
size: UiConstants.iconXs,
|
||||||
|
),
|
||||||
|
label: Text(getDirectionLabel),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: UiColors.textPrimary,
|
||||||
|
side: const BorderSide(color: UiColors.border),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space3,
|
||||||
|
vertical: 0,
|
||||||
|
),
|
||||||
|
minimumSize: const Size(0, 32),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
ShiftLocationMap(
|
||||||
|
shift: shift,
|
||||||
|
borderRadius: UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openDirections(BuildContext context) async {
|
||||||
|
final destination = (shift.latitude != null && shift.longitude != null)
|
||||||
|
? '${shift.latitude},${shift.longitude}'
|
||||||
|
: Uri.encodeComponent(
|
||||||
|
shift.locationAddress.isNotEmpty
|
||||||
|
? shift.locationAddress
|
||||||
|
: shift.location,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String url =
|
||||||
|
'https://www.google.com/maps/dir/?api=1&destination=$destination';
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
} else {
|
||||||
|
if (context.mounted) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: 'Could not open maps',
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A row of statistic cards for shift details (Total Pay, Rate, Hours).
|
||||||
|
class ShiftStatsRow extends StatelessWidget {
|
||||||
|
/// Estimated total pay for the shift.
|
||||||
|
final double estimatedTotal;
|
||||||
|
|
||||||
|
/// Hourly rate for the shift.
|
||||||
|
final double hourlyRate;
|
||||||
|
|
||||||
|
/// Total duration of the shift in hours.
|
||||||
|
final double duration;
|
||||||
|
|
||||||
|
/// Localization string for total.
|
||||||
|
final String totalLabel;
|
||||||
|
|
||||||
|
/// Localization string for hourly rate.
|
||||||
|
final String hourlyRateLabel;
|
||||||
|
|
||||||
|
/// Localization string for hours.
|
||||||
|
final String hoursLabel;
|
||||||
|
|
||||||
|
/// Creates a [ShiftStatsRow].
|
||||||
|
const ShiftStatsRow({
|
||||||
|
super.key,
|
||||||
|
required this.estimatedTotal,
|
||||||
|
required this.hourlyRate,
|
||||||
|
required this.duration,
|
||||||
|
required this.totalLabel,
|
||||||
|
required this.hourlyRateLabel,
|
||||||
|
required this.hoursLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.dollar,
|
||||||
|
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||||
|
totalLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.dollar,
|
||||||
|
"\$${hourlyRate.toStringAsFixed(0)}",
|
||||||
|
hourlyRateLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space4),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.clock,
|
||||||
|
duration.toStringAsFixed(1),
|
||||||
|
hoursLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatCard(IconData icon, String value, String label) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgThird,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 20, color: UiColors.textSecondary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: UiTypography.title1m
|
||||||
|
.copyWith(fontWeight: FontWeight.w700)
|
||||||
|
.textPrimary,
|
||||||
|
),
|
||||||
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:design_system/design_system.dart';
|
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
|
||||||
import 'package:krow_core/core.dart'; // Import AppConfig from krow_core
|
|
||||||
|
|
||||||
class ShiftLocationMap extends StatelessWidget {
|
|
||||||
final Shift shift;
|
|
||||||
final double height;
|
|
||||||
final double borderRadius;
|
|
||||||
|
|
||||||
const ShiftLocationMap({
|
|
||||||
super.key,
|
|
||||||
required this.shift,
|
|
||||||
this.height = 120,
|
|
||||||
this.borderRadius = 8,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (AppConfig.googleMapsApiKey.isEmpty) {
|
|
||||||
return _buildPlaceholder(context, "Config Map Key");
|
|
||||||
}
|
|
||||||
|
|
||||||
final String mapUrl = _generateStaticMapUrl();
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
height: height,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.background,
|
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: Image.network(
|
|
||||||
mapUrl,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return _buildPlaceholder(context, "Map unavailable");
|
|
||||||
},
|
|
||||||
loadingBuilder: (context, child, loadingProgress) {
|
|
||||||
if (loadingProgress == null) return child;
|
|
||||||
return Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: loadingProgress.expectedTotalBytes != null
|
|
||||||
? loadingProgress.cumulativeBytesLoaded /
|
|
||||||
loadingProgress.expectedTotalBytes!
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _generateStaticMapUrl() {
|
|
||||||
// Base URL
|
|
||||||
const String baseUrl = "https://maps.googleapis.com/maps/api/staticmap";
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
String center;
|
|
||||||
if (shift.latitude != null && shift.longitude != null) {
|
|
||||||
center = "${shift.latitude},${shift.longitude}";
|
|
||||||
} else {
|
|
||||||
center = Uri.encodeComponent(shift.locationAddress.isNotEmpty
|
|
||||||
? shift.locationAddress
|
|
||||||
: shift.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct URL
|
|
||||||
// scale=2 for retina displays
|
|
||||||
return "$baseUrl?center=$center&zoom=15&size=600x300&maptype=roadmap&markers=color:red%7C$center&key=${AppConfig.googleMapsApiKey}&scale=2";
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPlaceholder(BuildContext context, String message) {
|
|
||||||
return Container(
|
|
||||||
height: height,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.secondary,
|
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
UiIcons.mapPin,
|
|
||||||
size: 32,
|
|
||||||
color: UiColors.iconSecondary,
|
|
||||||
),
|
|
||||||
if (message.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Text(
|
|
||||||
message,
|
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,7 @@ import '../shared/empty_state_view.dart';
|
|||||||
class FindShiftsTab extends StatefulWidget {
|
class FindShiftsTab extends StatefulWidget {
|
||||||
final List<Shift> availableJobs;
|
final List<Shift> availableJobs;
|
||||||
|
|
||||||
const FindShiftsTab({
|
const FindShiftsTab({super.key, required this.availableJobs});
|
||||||
super.key,
|
|
||||||
required this.availableJobs,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
||||||
@@ -42,7 +39,9 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: (isSelected ? UiTypography.footnote2m.white : UiTypography.footnote2m.textSecondary),
|
style: (isSelected
|
||||||
|
? UiTypography.footnote2m.white
|
||||||
|
: UiTypography.footnote2m.textSecondary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -86,13 +85,15 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 48,
|
height: 48,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space3,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.background,
|
color: UiColors.background,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(
|
||||||
border: Border.all(
|
UiConstants.radiusBase,
|
||||||
color: UiColors.border,
|
|
||||||
),
|
),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -123,10 +124,10 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
width: 48,
|
width: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(
|
||||||
border: Border.all(
|
UiConstants.radiusBase,
|
||||||
color: UiColors.border,
|
|
||||||
),
|
),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
UiIcons.filter,
|
UiIcons.filter,
|
||||||
@@ -164,20 +165,27 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
subtitle: "Check back later",
|
subtitle: "Check back later",
|
||||||
)
|
)
|
||||||
: SingleChildScrollView(
|
: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space5,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: UiConstants.space5),
|
const SizedBox(height: UiConstants.space5),
|
||||||
...filteredJobs.map(
|
...filteredJobs.map(
|
||||||
(shift) => Padding(
|
(shift) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: UiConstants.space3,
|
||||||
|
),
|
||||||
child: MyShiftCard(
|
child: MyShiftCard(
|
||||||
shift: shift,
|
shift: shift,
|
||||||
onAccept: () {
|
onAccept: () {
|
||||||
context.read<ShiftsBloc>().add(AcceptShiftEvent(shift.id));
|
context.read<ShiftsBloc>().add(
|
||||||
|
AcceptShiftEvent(shift.id),
|
||||||
|
);
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message: "Shift application submitted!", // Todo: Localization
|
message:
|
||||||
|
"Shift application submitted!", // Todo: Localization
|
||||||
type: UiSnackbarType.success,
|
type: UiSnackbarType.success,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
name: staff_shifts
|
name: staff_shifts
|
||||||
description: A new Flutter package project.
|
description: A new Flutter package project.
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
publish_to: 'none'
|
publish_to: "none"
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.10.0 <4.0.0'
|
sdk: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_modular: ^6.3.2
|
|
||||||
flutter_bloc: ^8.1.3
|
|
||||||
equatable: ^2.0.5
|
|
||||||
intl: ^0.20.2
|
|
||||||
|
|
||||||
# Internal packages
|
# Internal packages
|
||||||
krow_core:
|
krow_core:
|
||||||
path: ../../../core
|
path: ../../../core
|
||||||
@@ -28,6 +24,15 @@ dependencies:
|
|||||||
core_localization:
|
core_localization:
|
||||||
path: ../../../core_localization
|
path: ../../../core_localization
|
||||||
|
|
||||||
|
flutter_modular: ^6.3.2
|
||||||
|
flutter_bloc: ^8.1.3
|
||||||
|
equatable: ^2.0.5
|
||||||
|
intl: ^0.20.2
|
||||||
|
google_maps_flutter: ^2.14.2
|
||||||
|
url_launcher: ^6.3.1
|
||||||
|
firebase_auth: ^6.1.4
|
||||||
|
firebase_data_connect: ^0.2.2+2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -249,6 +249,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
csv:
|
csv:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -467,6 +475,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.1"
|
version: "6.4.1"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.33"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -573,6 +589,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3+1"
|
version: "0.3.3+1"
|
||||||
|
google_maps:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps
|
||||||
|
sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.2.0"
|
||||||
|
google_maps_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter
|
||||||
|
sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.14.2"
|
||||||
|
google_maps_flutter_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_android
|
||||||
|
sha256: "98d7f5354f770f3e993db09fc798d40aeb6a254f04c1c468a94818ec2086e83e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.18.12"
|
||||||
|
google_maps_flutter_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_ios
|
||||||
|
sha256: "38f0a9ee858b0de3a5105e7efe200f154eea8397eb0c36bea6b3810429fbc0e4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.17.3"
|
||||||
|
google_maps_flutter_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_platform_interface
|
||||||
|
sha256: e8b1232419fcdd35c1fdafff96843f5a40238480365599d8ca661dde96d283dd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.14.1"
|
||||||
|
google_maps_flutter_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_maps_flutter_web
|
||||||
|
sha256: d416602944e1859f3cbbaa53e34785c223fa0a11eddb34a913c964c5cbb5d8cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.14+3"
|
||||||
google_places_flutter:
|
google_places_flutter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -613,6 +677,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.5"
|
version: "0.20.5"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1061,6 +1133,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.28.0"
|
||||||
|
sanitize_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sanitize_html
|
||||||
|
sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ query listApplications @auth(level: USER) {
|
|||||||
startTime
|
startTime
|
||||||
endTime
|
endTime
|
||||||
hours
|
hours
|
||||||
|
breakType
|
||||||
|
isBreakPaid
|
||||||
totalValue
|
totalValue
|
||||||
role {
|
role {
|
||||||
id
|
id
|
||||||
@@ -341,6 +343,8 @@ query getApplicationsByStaffId(
|
|||||||
startTime
|
startTime
|
||||||
endTime
|
endTime
|
||||||
hours
|
hours
|
||||||
|
breakType
|
||||||
|
isBreakPaid
|
||||||
totalValue
|
totalValue
|
||||||
role {
|
role {
|
||||||
id
|
id
|
||||||
@@ -352,7 +356,6 @@ query getApplicationsByStaffId(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
query vaidateDayStaffApplication(
|
query vaidateDayStaffApplication(
|
||||||
$staffId: UUID!
|
$staffId: UUID!
|
||||||
$offset: Int
|
$offset: Int
|
||||||
@@ -658,6 +661,8 @@ query listCompletedApplicationsByStaffId(
|
|||||||
status
|
status
|
||||||
description
|
description
|
||||||
durationDays
|
durationDays
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
|
|
||||||
order {
|
order {
|
||||||
id
|
id
|
||||||
@@ -692,6 +697,8 @@ query listCompletedApplicationsByStaffId(
|
|||||||
startTime
|
startTime
|
||||||
endTime
|
endTime
|
||||||
hours
|
hours
|
||||||
|
breakType
|
||||||
|
isBreakPaid
|
||||||
totalValue
|
totalValue
|
||||||
|
|
||||||
role {
|
role {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
query getShiftRoleById(
|
query getShiftRoleById(
|
||||||
$shiftId: UUID!
|
$shiftId: UUID!
|
||||||
$roleId: UUID!
|
$roleId: UUID!
|
||||||
@@ -29,6 +28,8 @@ query getShiftRoleById(
|
|||||||
location
|
location
|
||||||
locationAddress
|
locationAddress
|
||||||
description
|
description
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
orderId
|
orderId
|
||||||
|
|
||||||
order{
|
order{
|
||||||
@@ -91,6 +92,8 @@ query listShiftRolesByShiftId(
|
|||||||
location
|
location
|
||||||
locationAddress
|
locationAddress
|
||||||
description
|
description
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
orderId
|
orderId
|
||||||
|
|
||||||
order{
|
order{
|
||||||
@@ -148,6 +151,8 @@ query listShiftRolesByRoleId(
|
|||||||
location
|
location
|
||||||
locationAddress
|
locationAddress
|
||||||
description
|
description
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
orderId
|
orderId
|
||||||
|
|
||||||
order{
|
order{
|
||||||
@@ -212,6 +217,8 @@ query listShiftRolesByShiftIdAndTimeRange(
|
|||||||
location
|
location
|
||||||
locationAddress
|
locationAddress
|
||||||
description
|
description
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
orderId
|
orderId
|
||||||
|
|
||||||
order{
|
order{
|
||||||
@@ -284,6 +291,8 @@ query listShiftRolesByVendorId(
|
|||||||
location
|
location
|
||||||
locationAddress
|
locationAddress
|
||||||
description
|
description
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
orderId
|
orderId
|
||||||
status
|
status
|
||||||
durationDays
|
durationDays
|
||||||
|
|||||||
Reference in New Issue
Block a user