Merge branch '312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation' into fix_staff_app_bugs

This commit is contained in:
José Salazar
2026-02-02 22:33:10 +09:00
50 changed files with 2315 additions and 884 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -83,13 +83,42 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
return null;
}
/// Helper method to map Data Connect application to domain Shift using ShiftAdapter.
Shift _mapApplicationToShift(
dynamic app,
String status, {
bool hasApplied = true,
}) {
return ShiftAdapter.fromApplicationData(
shiftId: app.shift.id,
roleId: app.shiftRole.roleId,
roleName: app.shiftRole.role.name,
businessName: app.shift.order.business.businessName,
companyLogoUrl: app.shift.order.business.companyLogoUrl,
costPerHour: app.shiftRole.role.costPerHour,
shiftLocation: app.shift.location,
teamHubName: app.shift.order.teamHub.hubName,
shiftDate: _toDateTime(app.shift.date),
startTime: _toDateTime(app.shiftRole.startTime),
endTime: _toDateTime(app.shiftRole.endTime),
createdAt: _toDateTime(app.createdAt),
status: status,
description: app.shift.description,
durationDays: app.shift.durationDays,
count: app.shiftRole.count,
assigned: app.shiftRole.assigned,
eventName: app.shift.order.eventName,
hasApplied: hasApplied,
);
}
@override
Future<List<Shift>> getMyShifts({
required DateTime start,
required DateTime end,
}) async {
return _fetchApplications(
dc.ApplicationStatus.ACCEPTED,
[dc.ApplicationStatus.ACCEPTED, dc.ApplicationStatus.CONFIRMED],
start: start,
end: end,
);
@@ -97,12 +126,12 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
@override
Future<List<Shift>> getPendingAssignments() async {
return _fetchApplications(dc.ApplicationStatus.PENDING);
return _fetchApplications([dc.ApplicationStatus.PENDING]);
}
@override
Future<List<Shift>> getCancelledShifts() async {
return _fetchApplications(dc.ApplicationStatus.REJECTED);
return _fetchApplications([dc.ApplicationStatus.REJECTED]);
}
@override
@@ -118,37 +147,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
_shiftToAppIdMap[app.shift.id] = app.id;
_appToRoleIdMap[app.id] = app.shiftRole.id;
final String roleName = app.shiftRole.role.name;
final String orderName =
(app.shift.order.eventName ?? '').trim().isNotEmpty
? app.shift.order.eventName!
: app.shift.order.business.businessName;
final String title = '$roleName - $orderName';
final DateTime? shiftDate = _toDateTime(app.shift.date);
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
final DateTime? createdDt = _toDateTime(app.createdAt);
shifts.add(
Shift(
id: app.shift.id,
roleId: app.shiftRole.roleId,
title: title,
clientName: app.shift.order.business.businessName,
logoUrl: app.shift.order.business.companyLogoUrl,
hourlyRate: app.shiftRole.role.costPerHour,
location: app.shift.location ?? '',
locationAddress: app.shift.order.teamHub.hubName,
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT),
description: app.shift.description,
durationDays: app.shift.durationDays,
requiredSlots: app.shiftRole.count,
filledSlots: app.shiftRole.assigned ?? 0,
hasApplied: true,
_mapApplicationToShift(
app,
_mapStatus(dc.ApplicationStatus.CHECKED_OUT),
),
);
}
@@ -159,7 +161,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
}
Future<List<Shift>> _fetchApplications(
dc.ApplicationStatus status, {
List<dc.ApplicationStatus> statuses, {
DateTime? start,
DateTime? end,
}) async {
@@ -173,8 +175,9 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
}
final response = await query.execute();
final statusNames = statuses.map((s) => s.name).toSet();
final apps = response.data.applications.where(
(app) => app.status.stringValue == status.name,
(app) => statusNames.contains(app.status.stringValue),
);
final List<Shift> shifts = [];
@@ -513,7 +516,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
shiftId: shiftId,
staffId: staffId,
roleId: targetRoleId,
status: dc.ApplicationStatus.ACCEPTED,
status: dc.ApplicationStatus.CONFIRMED,
origin: dc.ApplicationOrigin.STAFF,
)
// TODO: this should be PENDING so a vendor can accept it.
@@ -547,7 +550,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
@override
Future<void> acceptShift(String shiftId) async {
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED);
}
@override