Merge pull request #579 from Oloodi/493-implement-rapid-order-creation-voice-text-in-client-mobile-app

Prepare & Handover M4 Mobile Applications for Client Testing
This commit is contained in:
Achintha Isuru
2026-03-05 17:09:33 -05:00
committed by GitHub
37 changed files with 307 additions and 462 deletions

View File

@@ -43,7 +43,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.krowwithus.client"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
@@ -51,18 +50,17 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: ""
manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: ""
}
signingConfigs {
create("release") {
if (System.getenv()["CI"] == "true") {
// CodeMagic CI environment
storeFile = file(System.getenv()["CM_KEYSTORE_PATH_CLIENT"] ?: "")
storePassword = System.getenv()["CM_KEYSTORE_PASSWORD_CLIENT"]
keyAlias = System.getenv()["CM_KEY_ALIAS_CLIENT"]
keyPassword = System.getenv()["CM_KEY_PASSWORD_CLIENT"]
storeFile = file(System.getenv()["CM_KEYSTORE_PATH"] ?: "")
storePassword = System.getenv()["CM_KEYSTORE_PASSWORD"]
keyAlias = System.getenv()["CM_KEY_ALIAS"]
keyPassword = System.getenv()["CM_KEY_PASSWORD"]
} else {
// Local development environment
keyAlias = keystoreProperties["keyAlias"] as String?

View File

@@ -104,7 +104,7 @@ class _SessionListenerState extends State<SessionListener> {
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
Modular.to.popSafe();
_proceedToLogin();
},
child: const Text('Log In'),
@@ -134,7 +134,7 @@ class _SessionListenerState extends State<SessionListener> {
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
Modular.to.popSafe();;
_proceedToLogin();
},
child: const Text('Log Out'),

View File

@@ -43,7 +43,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.krowwithus.staff"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
@@ -59,10 +58,10 @@ android {
create("release") {
if (System.getenv()["CI"] == "true") {
// CodeMagic CI environment
storeFile = file(System.getenv()["CM_KEYSTORE_PATH_STAFF"] ?: "")
storePassword = System.getenv()["CM_KEYSTORE_PASSWORD_STAFF"]
keyAlias = System.getenv()["CM_KEY_ALIAS_STAFF"]
keyPassword = System.getenv()["CM_KEY_PASSWORD_STAFF"]
storeFile = file(System.getenv()["CM_KEYSTORE_PATH"] ?: "")
storePassword = System.getenv()["CM_KEYSTORE_PASSWORD"]
keyAlias = System.getenv()["CM_KEY_ALIAS"]
keyPassword = System.getenv()["CM_KEY_PASSWORD"]
} else {
// Local development environment
keyAlias = keystoreProperties["keyAlias"] as String?

View File

@@ -1,162 +0,0 @@
{
"project_info": {
"project_number": "933560802882",
"project_id": "krow-workforce-dev",
"storage_bucket": "krow-workforce-dev.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db",
"android_client_info": {
"package_name": "com.krow.app.business.dev"
}
},
"oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:d49b8c0f4d19e95e7757db",
"android_client_info": {
"package_name": "com.krow.app.staff.dev"
}
},
"oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:da13569105659ead7757db",
"android_client_info": {
"package_name": "com.krowwithus.client"
}
},
"oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db",
"android_client_info": {
"package_name": "com.krowwithus.staff"
}
},
"oauth_client": [
{
"client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.krowwithus.staff",
"certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d"
}
},
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.krow.app.staff.dev"
}
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -104,7 +104,7 @@ class _SessionListenerState extends State<SessionListener> {
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
Modular.to.popSafe();;
_proceedToLogin();
},
child: const Text('Log In'),
@@ -134,7 +134,7 @@ class _SessionListenerState extends State<SessionListener> {
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
Modular.to.popSafe();;
_proceedToLogin();
},
child: const Text('Log Out'),

View File

@@ -1,4 +1,5 @@
import 'dart:ui';
import 'package:core_localization/src/l10n/strings.g.dart';
import '../../domain/repositories/locale_repository_interface.dart';
@@ -31,12 +32,10 @@ class LocaleRepositoryImpl implements LocaleRepositoryInterface {
return getDefaultLocale();
}
/// We can hardcode this to english based on customer requirements,
/// but in a more dynamic app this should be the device locale or a fallback to english.
@override
Locale getDefaultLocale() {
final Locale deviceLocale = AppLocaleUtils.findDeviceLocale().flutterLocale;
if (getSupportedLocales().contains(deviceLocale)) {
return deviceLocale;
}
return const Locale('en');
}

View File

@@ -20,7 +20,6 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
@override
Future<bool> getProfileCompletion() async {
return true;
return _service.run(() async {
final String staffId = await _service.getStaffId();

View File

@@ -28,6 +28,12 @@ class StaffPayment extends Equatable {
required this.amount,
required this.status,
this.paidAt,
this.shiftTitle,
this.shiftLocation,
this.locationAddress,
this.hoursWorked,
this.hourlyRate,
this.workedTime,
});
/// Unique identifier.
final String id;
@@ -47,6 +53,24 @@ class StaffPayment extends Equatable {
/// When the payment was successfully processed.
final DateTime? paidAt;
/// Title of the shift worked.
final String? shiftTitle;
/// Location/hub name of the shift.
final String? shiftLocation;
/// Address of the shift location.
final String? locationAddress;
/// Number of hours worked.
final double? hoursWorked;
/// Hourly rate for the shift.
final double? hourlyRate;
/// Work session duration or status.
final String? workedTime;
@override
List<Object?> get props => <Object?>[id, staffId, assignmentId, amount, status, paidAt];
List<Object?> get props => <Object?>[id, staffId, assignmentId, amount, status, paidAt, shiftTitle, shiftLocation, locationAddress, hoursWorked, hourlyRate, workedTime];
}

View File

@@ -1,14 +1,13 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:core_localization/core_localization.dart';
import '../blocs/coverage_bloc.dart';
import '../blocs/coverage_event.dart';
import '../blocs/coverage_state.dart';
import '../widgets/coverage_calendar_selector.dart';
import '../widgets/coverage_quick_stats.dart';
import '../widgets/coverage_shift_list.dart';
@@ -78,21 +77,6 @@ class _CoveragePageState extends State<CoveragePage> {
pinned: true,
expandedHeight: 300.0,
backgroundColor: UiColors.primary,
leading: IconButton(
onPressed: () => Modular.to.toClientHome(),
icon: Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(
color: UiColors.primaryForeground.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
UiIcons.arrowLeft,
color: UiColors.primaryForeground,
size: UiConstants.space4,
),
),
),
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(

View File

@@ -506,21 +506,21 @@ class _WorkerRow extends StatelessWidget {
),
),
),
if (worker.status == CoverageWorkerStatus.checkedIn)
UiButton.primary(
text: context.t.client_coverage.worker_row.verify,
size: UiButtonSize.small,
onPressed: () {
UiSnackbar.show(
context,
message:
context.t.client_coverage.worker_row.verified_message(
name: worker.name,
),
type: UiSnackbarType.success,
);
},
),
// if (worker.status == CoverageWorkerStatus.checkedIn)
// UiButton.primary(
// text: context.t.client_coverage.worker_row.verify,
// size: UiButtonSize.small,
// onPressed: () {
// UiSnackbar.show(
// context,
// message:
// context.t.client_coverage.worker_row.verified_message(
// name: worker.name,
// ),
// type: UiSnackbarType.success,
// );
// },
// ),
],
),
],

View File

@@ -161,14 +161,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
border: Border.all(color: UiColors.border, width: 0.5),
),
child: Column(
children: <Widget>[
@@ -249,9 +242,12 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
size: 14,
color: UiColors.iconSecondary,
),
Text(
order.eventName,
style: UiTypography.headline5m.textSecondary,
Expanded(
child: Text(
order.eventName,
style: UiTypography.headline5m.textSecondary,
overflow: TextOverflow.ellipsis,
),
),
],
),
@@ -313,7 +309,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
Expanded(
child: Text(
order.hubManagerName!,
style: UiTypography.footnote2r.textSecondary,
style:
UiTypography.footnote2r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -335,7 +332,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
bgColor: UiColors.primary.withValues(alpha: 0.08),
onTap: () => _openEditSheet(order: order),
),
if (_canEditOrder(order)) const SizedBox(width: UiConstants.space2),
if (_canEditOrder(order))
const SizedBox(width: UiConstants.space2),
if (order.confirmedApps.isNotEmpty)
_buildHeaderIconButton(
icon: _expanded

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/src/entities/reports/daily_ops_report.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/repositories/reports_repository.dart';
import 'daily_ops_event.dart';
import 'daily_ops_state.dart';

View File

@@ -1,14 +1,14 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:client_reports/src/presentation/blocs/coverage/coverage_bloc.dart';
import 'package:client_reports/src/presentation/blocs/coverage/coverage_bloc.dart';
import 'package:client_reports/src/presentation/blocs/coverage/coverage_event.dart';
import 'package:client_reports/src/presentation/blocs/coverage/coverage_state.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
class CoverageReportPage extends StatefulWidget {
const CoverageReportPage({super.key});
@@ -23,11 +23,10 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
@override
Widget build(BuildContext context) {
return BlocProvider(
return BlocProvider<CoverageBloc>(
create: (BuildContext context) => Modular.get<CoverageBloc>()
..add(LoadCoverageReport(startDate: _startDate, endDate: _endDate)),
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: BlocBuilder<CoverageBloc, CoverageState>(
builder: (BuildContext context, CoverageState state) {
if (state is CoverageLoading) {
@@ -64,7 +63,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Navigator.of(context).pop(),
onTap: () => Modular.to.popSafe(),
child: Container(
width: 40,
height: 40,

View File

@@ -1,5 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
import 'package:client_reports/src/presentation/blocs/daily_ops/daily_ops_bloc.dart';
import 'package:client_reports/src/presentation/blocs/daily_ops/daily_ops_bloc.dart';
import 'package:client_reports/src/presentation/blocs/daily_ops/daily_ops_event.dart';
import 'package:client_reports/src/presentation/blocs/daily_ops/daily_ops_state.dart';
import 'package:core_localization/core_localization.dart';
@@ -8,7 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/src/entities/reports/daily_ops_report.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
class DailyOpsReportPage extends StatefulWidget {
const DailyOpsReportPage({super.key});
@@ -50,11 +50,10 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
@override
Widget build(BuildContext context) {
return BlocProvider(
return BlocProvider<DailyOpsBloc>(
create: (BuildContext context) => Modular.get<DailyOpsBloc>()
..add(LoadDailyOpsReport(date: _selectedDate)),
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: BlocBuilder<DailyOpsBloc, DailyOpsState>(
builder: (BuildContext context, DailyOpsState state) {
if (state is DailyOpsLoading) {
@@ -94,7 +93,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Navigator.of(context).pop(),
onTap: () => Modular.to.popSafe(),
child: Container(
width: 40,
height: 40,
@@ -245,6 +244,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.2,
@@ -318,7 +318,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
],
),
const SizedBox(height: 8),
const SizedBox(height: UiConstants.space8),
Text(
context.t.client_reports.daily_ops_report
.all_shifts_title
@@ -398,14 +398,8 @@ class _OpsStatCard extends StatelessWidget {
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withOpacity(0.06),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.border, width: 0.5),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -440,7 +434,8 @@ class _OpsStatCard extends StatelessWidget {
color: UiColors.textPrimary,
),
),
const SizedBox(height: 6),
//UiChip(label: subValue),
// Colored pill badge (matches prototype)
Container(
padding: const EdgeInsets.symmetric(
@@ -449,12 +444,12 @@ class _OpsStatCard extends StatelessWidget {
),
decoration: BoxDecoration(
color: color.withOpacity(0.12),
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(4),
),
child: Text(
subValue,
style: TextStyle(
fontSize: 10,
fontSize: 12,
fontWeight: FontWeight.bold,
color: color,
),

View File

@@ -2,6 +2,7 @@
import 'package:client_reports/src/presentation/blocs/forecast/forecast_bloc.dart';
import 'package:client_reports/src/presentation/blocs/forecast/forecast_event.dart';
import 'package:client_reports/src/presentation/blocs/forecast/forecast_state.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
@@ -28,7 +29,6 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
create: (BuildContext context) => Modular.get<ForecastBloc>()
..add(LoadForecastReport(startDate: _startDate, endDate: _endDate)),
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: BlocBuilder<ForecastBloc, ForecastState>(
builder: (BuildContext context, ForecastState state) {
if (state is ForecastLoading) {
@@ -86,7 +86,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
(ForecastWeek week) => _WeeklyBreakdownItem(week: week),
),
const SizedBox(height: 40),
const SizedBox(height: UiConstants.space24),
],
),
),
@@ -124,7 +124,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Navigator.of(context).pop(),
onTap: () => Modular.to.popSafe(),
child: Container(
width: 40,
height: 40,
@@ -184,6 +184,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
final TranslationsClientReportsForecastReportEn t = context.t.client_reports.forecast_report;
return GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,

View File

@@ -1,4 +1,5 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:client_reports/src/presentation/blocs/no_show/no_show_bloc.dart';
import 'package:client_reports/src/presentation/blocs/no_show/no_show_event.dart';
@@ -27,7 +28,6 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
create: (BuildContext context) => Modular.get<NoShowBloc>()
..add(LoadNoShowReport(startDate: _startDate, endDate: _endDate)),
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: BlocBuilder<NoShowBloc, NoShowState>(
builder: (BuildContext context, NoShowState state) {
if (state is NoShowLoading) {
@@ -67,7 +67,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Navigator.of(context).pop(),
onTap: () => Modular.to.popSafe(),
child: Container(
width: 40,
height: 40,

View File

@@ -1,5 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
import 'package:client_reports/src/presentation/blocs/performance/performance_bloc.dart';
import 'package:client_reports/src/presentation/blocs/performance/performance_bloc.dart';
import 'package:client_reports/src/presentation/blocs/performance/performance_event.dart';
import 'package:client_reports/src/presentation/blocs/performance/performance_state.dart';
import 'package:core_localization/core_localization.dart';
@@ -7,7 +6,8 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/src/entities/reports/performance_report.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
class PerformanceReportPage extends StatefulWidget {
const PerformanceReportPage({super.key});
@@ -26,7 +26,6 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
create: (BuildContext context) => Modular.get<PerformanceBloc>()
..add(LoadPerformanceReport(startDate: _startDate, endDate: _endDate)),
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: BlocBuilder<PerformanceBloc, PerformanceState>(
builder: (BuildContext context, PerformanceState state) {
if (state is PerformanceLoading) {
@@ -143,7 +142,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Navigator.of(context).pop(),
onTap: () => Modular.to.popSafe(),
child: Container(
width: 40,
height: 40,

View File

@@ -1,7 +1,5 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart';
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart';
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_event.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
@@ -80,10 +78,9 @@ class _ReportsPageState extends State<ReportsPage>
@override
Widget build(BuildContext context) {
return BlocProvider.value(
return BlocProvider<ReportsSummaryBloc>.value(
value: _summaryBloc,
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: SingleChildScrollView(
child: Column(
children: <Widget>[

View File

@@ -1,5 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:client_reports/src/presentation/blocs/spend/spend_bloc.dart';
import 'package:client_reports/src/presentation/blocs/spend/spend_bloc.dart';
import 'package:client_reports/src/presentation/blocs/spend/spend_event.dart';
import 'package:client_reports/src/presentation/blocs/spend/spend_state.dart';
import 'package:core_localization/core_localization.dart';
@@ -9,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
class SpendReportPage extends StatefulWidget {
@@ -35,11 +35,10 @@ class _SpendReportPageState extends State<SpendReportPage> {
@override
Widget build(BuildContext context) {
return BlocProvider(
return BlocProvider<SpendBloc>(
create: (BuildContext context) => Modular.get<SpendBloc>()
..add(LoadSpendReport(startDate: _startDate, endDate: _endDate)),
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: BlocBuilder<SpendBloc, SpendState>(
builder: (BuildContext context, SpendState state) {
if (state is SpendLoading) {
@@ -72,7 +71,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Navigator.of(context).pop(),
onTap: () => Modular.to.popSafe(),
child: Container(
width: 40,
height: 40,

View File

@@ -155,7 +155,7 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
BlocProvider.of<AuthBloc>(
context,
).add(AuthResetRequested(mode: widget.mode));
Navigator.of(context).pop();
Modular.to.popSafe();;
},
),
body: SafeArea(

View File

@@ -11,16 +11,16 @@ class FullWidthDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
//final screenWidth = MediaQuery.of(context).size.width;
return Column(
children: [
const SizedBox(height: UiConstants.space10),
Transform.translate(
offset: const Offset(-UiConstants.space4, 0),
child: SizedBox(width: screenWidth, child: const Divider()),
),
const SizedBox(height: UiConstants.space10),
// Transform.translate(
// offset: const Offset(-UiConstants.space4, 0),
// child: SizedBox(width: screenWidth, child: const Divider()),
// ),
// const SizedBox(height: UiConstants.space10),
],
);
}

View File

@@ -67,6 +67,16 @@ class PaymentsRepositoryImpl
.execute();
return response.data.recentPayments.map((dc.ListRecentPaymentsByStaffIdRecentPayments payment) {
// Extract shift details from nested application structure
final String? shiftTitle = payment.application.shiftRole.shift.title;
final String? locationAddress = payment.application.shiftRole.shift.locationAddress;
final double? hoursWorked = payment.application.shiftRole.hours;
final double? hourlyRate = payment.application.shiftRole.role.costPerHour;
// Extract hub details from order
final String? locationHub = payment.invoice.order.teamHub.hubName;
final String? hubAddress = payment.invoice.order.teamHub.address;
final String? shiftLocation = locationAddress ?? hubAddress;
return StaffPayment(
id: payment.id,
staffId: payment.staffId,
@@ -74,6 +84,12 @@ class PaymentsRepositoryImpl
amount: payment.invoice.amount,
status: PaymentAdapter.toPaymentStatus(payment.status?.stringValue ?? 'UNKNOWN'),
paidAt: _service.toDateTime(payment.invoice.issueDate),
shiftTitle: shiftTitle,
shiftLocation: locationHub,
locationAddress: shiftLocation,
hoursWorked: hoursWorked,
hourlyRate: hourlyRate,
workedTime: payment.workedTime,
);
}).toList();
});

View File

@@ -1,5 +1,4 @@
import 'package:design_system/design_system.dart';
import 'package:krow_core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
@@ -10,7 +9,6 @@ import '../blocs/payments/payments_bloc.dart';
import '../blocs/payments/payments_event.dart';
import '../blocs/payments/payments_state.dart';
import '../widgets/payment_stats_card.dart';
import '../widgets/pending_pay_card.dart';
import '../widgets/payment_history_item.dart';
import '../widgets/earnings_graph.dart';
@@ -172,17 +170,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
),
],
),
const SizedBox(height: UiConstants.space4),
// Pending Pay
if (state.summary.pendingEarnings > 0)
PendingPayCard(
amount: state.summary.pendingEarnings,
onCashOut: () {
Modular.to.pushNamed('${StaffPaths.payments}early-pay');
},
),
const SizedBox(height: UiConstants.space6),
const SizedBox(height: UiConstants.space8),
// Recent Payments
if (state.history.isNotEmpty)
@@ -191,7 +179,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
children: <Widget>[
Text(
"Recent Payments",
style: UiTypography.body2m.textPrimary,
style: UiTypography.body1b,
),
const SizedBox(height: UiConstants.space3),
Column(
@@ -201,16 +189,16 @@ class _PaymentsPageState extends State<PaymentsPage> {
bottom: UiConstants.space2),
child: PaymentHistoryItem(
amount: payment.amount,
title: "Shift Payment",
location: "Varies",
address: "Payment ID: ${payment.id}",
title: payment.shiftTitle ?? "Shift Payment",
location: payment.shiftLocation ?? "Varies",
address: payment.locationAddress ?? payment.id,
date: payment.paidAt != null
? DateFormat('E, MMM d')
.format(payment.paidAt!)
: 'Pending',
workedTime: "Completed",
hours: 0,
rate: 0.0,
workedTime: payment.workedTime ?? "Completed",
hours: (payment.hoursWorked ?? 0).toInt(),
rate: payment.hourlyRate ?? 0.0,
status: payment.status.name.toUpperCase(),
),
);

View File

@@ -32,13 +32,7 @@ class PaymentHistoryItem extends StatelessWidget {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
border: Border.all(color: UiColors.border, width: 0.5),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -77,7 +71,7 @@ class PaymentHistoryItem extends StatelessWidget {
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: const Icon(
UiIcons.chart,
UiIcons.dollar,
color: UiColors.mutedForeground,
size: 24,
),
@@ -98,7 +92,7 @@ class PaymentHistoryItem extends StatelessWidget {
children: <Widget>[
Text(
title,
style: UiTypography.body2b.textPrimary,
style: UiTypography.body2m,
),
Text(
location,
@@ -112,7 +106,7 @@ class PaymentHistoryItem extends StatelessWidget {
children: <Widget>[
Text(
"\$${amount.toStringAsFixed(0)}",
style: UiTypography.headline4m.textPrimary,
style: UiTypography.headline4b,
),
Text(
"\$${rate.toStringAsFixed(0)}/hr · ${hours}h",

View File

@@ -1,5 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import '../../../domain/usecases/apply_for_shift_usecase.dart';
import '../../../domain/usecases/decline_shift_usecase.dart';
import '../../../domain/usecases/get_shift_details_usecase.dart';
@@ -12,11 +13,13 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
final GetShiftDetailsUseCase getShiftDetails;
final ApplyForShiftUseCase applyForShift;
final DeclineShiftUseCase declineShift;
final GetProfileCompletionUseCase getProfileCompletion;
ShiftDetailsBloc({
required this.getShiftDetails,
required this.applyForShift,
required this.declineShift,
required this.getProfileCompletion,
}) : super(ShiftDetailsInitial()) {
on<LoadShiftDetailsEvent>(_onLoadDetails);
on<BookShiftDetailsEvent>(_onBookShift);
@@ -34,8 +37,9 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
final shift = await getShiftDetails(
GetShiftDetailsArguments(shiftId: event.shiftId, roleId: event.roleId),
);
final isProfileComplete = await getProfileCompletion();
if (shift != null) {
emit(ShiftDetailsLoaded(shift));
emit(ShiftDetailsLoaded(shift, isProfileComplete: isProfileComplete));
} else {
emit(const ShiftDetailsError("Shift not found"));
}

View File

@@ -14,10 +14,11 @@ class ShiftDetailsLoading extends ShiftDetailsState {}
class ShiftDetailsLoaded extends ShiftDetailsState {
final Shift shift;
const ShiftDetailsLoaded(this.shift);
final bool isProfileComplete;
const ShiftDetailsLoaded(this.shift, {this.isProfileComplete = false});
@override
List<Object?> get props => [shift];
List<Object?> get props => [shift, isProfileComplete];
}
class ShiftDetailsError extends ShiftDetailsState {

View File

@@ -125,6 +125,9 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
final Shift displayShift = widget.shift;
final i18n = Translations.of(context).staff_shifts.shift_details;
final isProfileComplete = state is ShiftDetailsLoaded
? state.isProfileComplete
: false;
final duration = _calculateDuration(displayShift);
final estimatedTotal =
@@ -142,6 +145,16 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isProfileComplete)
Padding(
padding: const EdgeInsets.all(UiConstants.space6),
child: UiNoticeBanner(
title: 'Complete Your Account',
description:
'Complete your account to book this shift and start earning',
icon: UiIcons.sparkles,
),
),
ShiftDetailsHeader(shift: displayShift),
const Divider(height: 1, thickness: 0.5),
ShiftStatsRow(
@@ -194,20 +207,21 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
),
),
),
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,
if (isProfileComplete)
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,
),
),
),
),
),
],
),
);

View File

@@ -1,4 +1,5 @@
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'domain/repositories/shifts_repository_interface.dart';
import 'data/repositories_impl/shifts_repository_impl.dart';
import 'domain/usecases/get_shift_details_usecase.dart';
@@ -14,11 +15,21 @@ class ShiftDetailsModule extends Module {
// Repository
i.add<ShiftsRepositoryInterface>(ShiftsRepositoryImpl.new);
// StaffConnectorRepository for profile completion
i.addLazySingleton<StaffConnectorRepository>(
() => StaffConnectorRepositoryImpl(),
);
// UseCases
i.add(GetShiftDetailsUseCase.new);
i.add(AcceptShiftUseCase.new);
i.add(DeclineShiftUseCase.new);
i.add(ApplyForShiftUseCase.new);
i.addLazySingleton<GetProfileCompletionUseCase>(
() => GetProfileCompletionUseCase(
repository: i.get<StaffConnectorRepository>(),
),
);
// Bloc
i.add(ShiftDetailsBloc.new);

View File

@@ -32,18 +32,18 @@ class StaffShiftsModule extends Module {
);
// Repository
i.add<ShiftsRepositoryInterface>(ShiftsRepositoryImpl.new);
i.addLazySingleton<ShiftsRepositoryInterface>(ShiftsRepositoryImpl.new);
// UseCases
i.add(GetMyShiftsUseCase.new);
i.add(GetAvailableShiftsUseCase.new);
i.add(GetPendingAssignmentsUseCase.new);
i.add(GetCancelledShiftsUseCase.new);
i.add(GetHistoryShiftsUseCase.new);
i.add(AcceptShiftUseCase.new);
i.add(DeclineShiftUseCase.new);
i.add(ApplyForShiftUseCase.new);
i.add(GetShiftDetailsUseCase.new);
i.addLazySingleton(GetMyShiftsUseCase.new);
i.addLazySingleton(GetAvailableShiftsUseCase.new);
i.addLazySingleton(GetPendingAssignmentsUseCase.new);
i.addLazySingleton(GetCancelledShiftsUseCase.new);
i.addLazySingleton(GetHistoryShiftsUseCase.new);
i.addLazySingleton(AcceptShiftUseCase.new);
i.addLazySingleton(DeclineShiftUseCase.new);
i.addLazySingleton(ApplyForShiftUseCase.new);
i.addLazySingleton(GetShiftDetailsUseCase.new);
// Bloc
i.add(

View File

@@ -4,9 +4,6 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
'@dataconnect/generated': link:src/dataconnect-generated
importers:
.:

View File

@@ -1,2 +1,5 @@
packages:
- '.'
overrides:
'@dataconnect/generated': link:src/dataconnect-generated

View File

@@ -62,12 +62,6 @@ export const NAV_CONFIG: NavGroup[] = [
icon: LayoutDashboard,
allowedRoles: ['Vendor'],
},
{
label: 'Savings Engine',
path: '/savings',
icon: PiggyBank,
allowedRoles: ALL_ROLES,
},
{
label: 'Vendor Performance',
path: '/performance',
@@ -117,23 +111,6 @@ export const NAV_CONFIG: NavGroup[] = [
},
],
},
{
title: 'Marketplace',
items: [
{
label: 'Discovery',
path: '/marketplace',
icon: ShoppingBag,
allowedRoles: ['Client', 'Admin'],
},
{
label: 'Compare Rates',
path: '/marketplace/compare',
icon: Scale,
allowedRoles: ['Client', 'Admin'],
},
],
},
{
title: 'Workforce',
items: [
@@ -143,18 +120,6 @@ export const NAV_CONFIG: NavGroup[] = [
icon: Users,
allowedRoles: ALL_ROLES,
},
{
label: 'Onboarding',
path: '/onboarding',
icon: UserPlus,
allowedRoles: ALL_ROLES,
},
{
label: 'Teams',
path: '/teams',
icon: Users2,
allowedRoles: ALL_ROLES,
},
{
label: 'Compliance',
path: '/compliance',
@@ -197,44 +162,4 @@ export const NAV_CONFIG: NavGroup[] = [
},
],
},
{
title: 'Analytics & Comm',
items: [
{
label: 'Reports',
path: '/reports',
icon: PieChart,
allowedRoles: ALL_ROLES,
},
{
label: 'Activity Log',
path: '/activity',
icon: History,
allowedRoles: ['Vendor', 'Admin'],
},
{
label: 'Messages',
path: '/messages',
icon: MessageSquare,
allowedRoles: ALL_ROLES,
},
{
label: 'Tutorials',
path: '/tutorials',
icon: BookOpen,
allowedRoles: ['Client', 'Admin'],
},
],
},
{
title: 'Support',
items: [
{
label: 'Help Center',
path: '/support',
icon: HelpCircle,
allowedRoles: ['Client', 'Admin'],
},
],
},
];

View File

@@ -6,7 +6,7 @@ import {
DialogTitle,
} from "@/common/components/ui/dialog";
import EventFormWizard from "./EventFormWizard";
import { useCreateOrder, useListBusinesses, useListHubs } from "@/dataconnect-generated/react";
import { useCreateOrder, useListBusinesses, useListTeamHubs } from "@/dataconnect-generated/react";
import { OrderType, OrderStatus } from "@/dataconnect-generated";
import { dataConnect } from "@/features/auth/firebase";
import { useToast } from "@/common/components/ui/use-toast";
@@ -26,7 +26,7 @@ export default function CreateOrderDialog({ open, onOpenChange }: CreateOrderDia
const [selectedHubId, setSelectedHubId] = React.useState<string>("");
const { data: businessesData } = useListBusinesses(dataConnect);
const { data: hubsData } = useListHubs(dataConnect);
const { data: hubsData } = useListTeamHubs(dataConnect);
const createOrderMutation = useCreateOrder(dataConnect, {
onSuccess: () => {
@@ -109,9 +109,9 @@ export default function CreateOrderDialog({ open, onOpenChange }: CreateOrderDia
<SelectValue placeholder="Select Hub" />
</SelectTrigger>
<SelectContent>
{hubsData?.hubs.map((h) => (
{hubsData?.teamHubs.map((h) => (
<SelectItem key={h.id} value={h.id}>
{h.name}
{h.hubName}
</SelectItem>
))}
</SelectContent>

View File

@@ -4,7 +4,7 @@
# Reusable script for building the Flutter app
client-app-android-apk-build-script: &client-app-android-apk-build-script
name: 👷🤖 Build Client App APK (Android)
name: 👷 🤖 Build Client App APK (Android)
script: |
dart pub global activate melos
export PATH="$PATH":"$HOME/.pub-cache/bin"
@@ -12,7 +12,7 @@ client-app-android-apk-build-script: &client-app-android-apk-build-script
make mobile-client-build PLATFORM=apk MODE=release
client-app-ios-build-script: &client-app-ios-build-script
name: 👷🍎 Build Client App (iOS)
name: 👷 🍎 Build Client App (iOS)
script: |
dart pub global activate melos
export PATH="$PATH":"$HOME/.pub-cache/bin"
@@ -20,7 +20,7 @@ client-app-ios-build-script: &client-app-ios-build-script
make mobile-client-build PLATFORM=ios MODE=release
staff-app-android-apk-build-script: &staff-app-android-apk-build-script
name: 👷🤖 Build Staff App APK (Android)
name: 👷 🤖 Build Staff App APK (Android)
script: |
dart pub global activate melos
export PATH="$PATH":"$HOME/.pub-cache/bin"
@@ -28,7 +28,7 @@ staff-app-android-apk-build-script: &staff-app-android-apk-build-script
make mobile-staff-build PLATFORM=apk MODE=release
staff-app-ios-build-script: &staff-app-ios-build-script
name: 👷🍎 Build Staff App (iOS)
name: 👷 🍎 Build Staff App (iOS)
script: |
dart pub global activate melos
export PATH="$PATH":"$HOME/.pub-cache/bin"
@@ -37,7 +37,7 @@ staff-app-ios-build-script: &staff-app-ios-build-script
# Reusable script for distributing Android to Firebase
distribute-android-script: &distribute-android-script
name: 🚛🤖 Distribute Android to Firebase App Distribution
name: 🚛 🤖 Distribute Android to Firebase App Distribution
script: |
# Distribute Android APK
# Note: Using wildcards to catch app-release.apk
@@ -167,7 +167,7 @@ workflows:
# =================================================================================
client-app-dev-android:
<<: *client-app-base
name: 🚛🤖 Client App Dev (Android App Distribution)
name: 🚛 🤖 Client App Dev (Android App Distribution)
environment:
flutter: stable
xcode: latest
@@ -175,11 +175,7 @@ workflows:
groups:
- client_app_dev_credentials
android_signing:
- keystore: krow_client_dev
keystore_environment_variable: CM_KEYSTORE_PATH_CLIENT
keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_CLIENT
key_alias_environment_variable: CM_KEY_ALIAS_CLIENT
key_password_environment_variable: CM_KEY_PASSWORD_CLIENT
- keystore: KROW_CLIENT_DEV
vars:
ENV: dev
scripts:
@@ -196,11 +192,7 @@ workflows:
groups:
- client_app_staging_credentials
android_signing:
- keystore: krow_client_staging
keystore_environment_variable: CM_KEYSTORE_PATH_CLIENT
keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_CLIENT
key_alias_environment_variable: CM_KEY_ALIAS_CLIENT
key_password_environment_variable: CM_KEY_PASSWORD_CLIENT
- keystore: KROW_CLIENT_STAGING
vars:
ENV: staging
scripts:
@@ -209,12 +201,12 @@ workflows:
client-app-prod-android:
<<: *client-app-base
name: 🚛🤖 Client App Prod (Android App Distribution)
name: 🚛 🤖 Client App Prod (Android App Distribution)
environment:
groups:
- client_app_prod_credentials
android_signing:
- keystore: krow_client_prod
- keystore: KROW_CLIENT_PROD
keystore_environment_variable: CM_KEYSTORE_PATH_CLIENT
keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_CLIENT
key_alias_environment_variable: CM_KEY_ALIAS_CLIENT
@@ -230,7 +222,7 @@ workflows:
# =================================================================================
client-app-dev-ios:
<<: *client-app-base
name: 🚛🍎 Client App Dev (iOS App Distribution)
name: 🚛 🍎 Client App Dev (iOS App Distribution)
environment:
groups:
- client_app_dev_credentials
@@ -242,7 +234,7 @@ workflows:
client-app-staging-ios:
<<: *client-app-base
name: 🚛🍎 Client App Staging (iOS App Distribution)
name: 🚛 🍎 Client App Staging (iOS App Distribution)
environment:
groups:
- client_app_staging_credentials
@@ -254,7 +246,7 @@ workflows:
client-app-prod-ios:
<<: *client-app-base
name: 🚛🍎 Client App Prod (iOS App Distribution)
name: 🚛 🍎 Client App Prod (iOS App Distribution)
environment:
groups:
- client_app_prod_credentials
@@ -269,7 +261,7 @@ workflows:
# =================================================================================
staff-app-dev-android:
<<: *staff-app-base
name: 🚛🤖👨‍🍳 Staff App Dev (Android App Distribution)
name: 🚛 🤖 👨‍🍳 Staff App Dev (Android App Distribution)
environment:
flutter: stable
xcode: latest
@@ -277,11 +269,7 @@ workflows:
groups:
- staff_app_dev_credentials
android_signing:
- keystore: krow_staff_dev
keystore_environment_variable: CM_KEYSTORE_PATH_STAFF
keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_STAFF
key_alias_environment_variable: CM_KEY_ALIAS_STAFF
key_password_environment_variable: CM_KEY_PASSWORD_STAFF
- keystore: KROW_STAFF_DEV
vars:
ENV: dev
scripts:
@@ -290,7 +278,7 @@ workflows:
staff-app-staging-android:
<<: *staff-app-base
name: 🚛🤖👨‍🍳 Staff App Staging (Android App Distribution)
name: 🚛 🤖 👨‍🍳 Staff App Staging (Android App Distribution)
environment:
flutter: stable
xcode: latest
@@ -298,11 +286,7 @@ workflows:
groups:
- staff_app_staging_credentials
android_signing:
- keystore: krow_staff_staging
keystore_environment_variable: CM_KEYSTORE_PATH_STAFF
keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_STAFF
key_alias_environment_variable: CM_KEY_ALIAS_STAFF
key_password_environment_variable: CM_KEY_PASSWORD_STAFF
- keystore: KROW_STAFF_STAGING
vars:
ENV: staging
scripts:
@@ -311,7 +295,7 @@ workflows:
staff-app-prod-android:
<<: *staff-app-base
name: 🚛🤖👨‍🍳 Staff App Prod (Android App Distribution)
name: 🚛 🤖 👨‍🍳 Staff App Prod (Android App Distribution)
environment:
flutter: stable
xcode: latest
@@ -319,11 +303,7 @@ workflows:
groups:
- staff_app_prod_credentials
android_signing:
- keystore: krow_staff_prod
keystore_environment_variable: CM_KEYSTORE_PATH_STAFF
keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_STAFF
key_alias_environment_variable: CM_KEY_ALIAS_STAFF
key_password_environment_variable: CM_KEY_PASSWORD_STAFF
- keystore: KROW_STAFF_PROD
vars:
ENV: prod
scripts:
@@ -335,7 +315,7 @@ workflows:
# =================================================================================
staff-app-dev-ios:
<<: *staff-app-base
name: 🚛🍎👨‍🍳 Staff App Dev (iOS App Distribution)
name: 🚛 🍎 👨‍🍳 Staff App Dev (iOS App Distribution)
environment:
groups:
- staff_app_dev_credentials
@@ -347,7 +327,7 @@ workflows:
staff-app-staging-ios:
<<: *staff-app-base
name: 🚛🍎👨‍🍳 Staff App Staging (iOS App Distribution)
name: 🚛 🍎 👨‍🍳 Staff App Staging (iOS App Distribution)
environment:
groups:
- staff_app_staging_credentials
@@ -359,7 +339,7 @@ workflows:
staff-app-prod-ios:
<<: *staff-app-base
name: 🚛🍎👨‍🍳 Staff App Prod (iOS App Distribution)
name: 🚛 🍎 👨‍🍳 Staff App Prod (iOS App Distribution)
environment:
groups:
- staff_app_prod_credentials

View File

@@ -0,0 +1,82 @@
# KROW Workforce Platform — M4 Guide
**Version:** Milestone 4 (0.0.1-IlianaStaffM4 and 0.0.1-IlianaClientM4)
**Estimated Duration:** 25-30 minutes
---
## 📦 Deliverables
- **Client Mobile Application** (v0.0.1-IlianaClientM4)
- **Staff Mobile Application** (v0.0.1-IlianaStaffM4)
- **Full Demo Video** - Comprehensive walkthrough of all (M1 - M4) completed features of the mobile applications.
---
## 1. Overview
### Core Improvements
M4 delivers three key areas of improvement:
1. **Overall Application Improvements**
- Auth session persistence: Users stay signed in after reopening the app
- Stability fixes from M3 client feedback and dev team discoveries
- UI/UX improvements across key screens for clarity and speed
2. **Client App Updates**
- Complete order creation flow (Rapid, Permanent, Recurring orders)
- Shift manager assignment support
- Paid/unpaid break handling in orders
- Complete Reports section (Daily Ops, Spend, Coverage, No-show, Performance)
- Cost centres in hubs for location/business unit tracking
- Billing approval workflow for pending bills
3. **Staff App Updates**
- Profile completion requirements gating payments and clockings
- Worker benefits integration
- Enhanced shift discovery with filtering by location
- Spanish localization support
- AI-verified document uploads (Attire, Documents, Certificates)
- FAQ and Privacy Policy
- Worker profile visibility controls
---
## 2. Required Test Accounts
**Client Account (Business User):**
- Email: `legendary@krowd.com`
- Password: `Demo2026!`
- Client Name: "KROW"
**Staff Account (Worker):**
- Phone: `+15557654321`
- OTP Code: `123456` (testing mode)
- Name: "Mariana Torres"
***Note on Profile Completion***
When a staff user hasn't completed their profile, they see an empty/incomplete state on their home screen. Currently tracked sections to mark as complete:
- Profile Information (full name, email, phone, preferred locations)
- Emergency Contact
Future sections can be added as mandatory, such as Tax Forms, Bank Account, Documents, Certificates, and Attires.
***Profile Blocking Rules***
When the profile is incomplete, the following features are blocked to encourage completion:
- Clock-in page is hidden
- Payments are blocked
- "My Shifts" and History sections are hidden
- Users can view available shifts but cannot book them
This ensures we have all necessary information for compliance and payroll before workers are allowed to work.
---
## 3. M4 Key Deliverables
✅ Stronger reliability and stability
✅ Completed client ordering and reporting workflows
✅ Better profile and shift tooling for staff
✅ AI-assisted document verification
✅ Localization support (Spanish)
✅ Improved billing and cost tracking controls