feat(location): Add latitude and longitude to shift details and integrate Google Maps for location display

This commit is contained in:
Achintha Isuru
2026-02-16 14:21:33 -05:00
parent e1e255f8f0
commit 7cc779cca2
6 changed files with 94 additions and 56 deletions

View File

@@ -1,3 +1,3 @@
{ {
"GOOGLE_MAPS_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU" "GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0"
} }

View File

@@ -141,6 +141,8 @@ 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( breakInfo: BreakAdapter.fromData(
isPaid: app.shiftRole.isBreakPaid ?? false, isPaid: app.shiftRole.isBreakPaid ?? false,
breakTime: app.shiftRole.breakType?.stringValue, breakTime: app.shiftRole.breakType?.stringValue,
@@ -212,6 +214,8 @@ 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( breakInfo: BreakAdapter.fromData(
isPaid: app.shiftRole.isBreakPaid ?? false, isPaid: app.shiftRole.isBreakPaid ?? false,
breakTime: app.shiftRole.breakType?.stringValue, breakTime: app.shiftRole.breakType?.stringValue,
@@ -285,6 +289,8 @@ 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( breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false, isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue, breakTime: sr.breakType?.stringValue,
@@ -362,6 +368,8 @@ 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( breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false, isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue, breakTime: sr.breakType?.stringValue,
@@ -417,6 +425,8 @@ class ShiftsRepositoryImpl
durationDays: s.durationDays, durationDays: s.durationDays,
requiredSlots: required, requiredSlots: required,
filledSlots: filled, filledSlots: filled,
latitude: s.latitude,
longitude: s.longitude,
breakInfo: breakInfo, breakInfo: breakInfo,
); );
} }

View File

@@ -1,13 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:krow_core/core.dart'; // Import AppConfig from krow_core import 'package:google_maps_flutter/google_maps_flutter.dart';
class ShiftLocationMap extends StatelessWidget { /// 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; final Shift shift;
/// The height of the map widget.
final double height; final double height;
/// The border radius for the map container.
final double borderRadius; final double borderRadius;
/// Creates a [ShiftLocationMap].
const ShiftLocationMap({ const ShiftLocationMap({
super.key, super.key,
required this.shift, required this.shift,
@@ -16,77 +23,88 @@ class ShiftLocationMap extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { State<ShiftLocationMap> createState() => _ShiftLocationMapState();
if (AppConfig.googleMapsApiKey.isEmpty) { }
return _buildPlaceholder(context, "Config Map Key");
}
final String mapUrl = _generateStaticMapUrl(); class _ShiftLocationMapState extends State<ShiftLocationMap> {
late final CameraPosition _initialPosition;
final Set<Marker> _markers = {};
return Container( @override
height: height, void initState() {
width: double.infinity, super.initState();
decoration: BoxDecoration(
color: UiColors.background, // Default to a fallback coordinate if latitude/longitude are null.
borderRadius: BorderRadius.circular(borderRadius), // In a real app, you might want to geocode the address if coordinates are missing.
), final double lat = widget.shift.latitude ?? 0.0;
clipBehavior: Clip.antiAlias, final double lng = widget.shift.longitude ?? 0.0;
child: Image.network(
mapUrl, final LatLng position = LatLng(lat, lng);
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) { _initialPosition = CameraPosition(
return _buildPlaceholder(context, "Map unavailable"); target: position,
}, zoom: 15,
loadingBuilder: (context, child, loadingProgress) { );
if (loadingProgress == null) return child;
return Center( _markers.add(
child: CircularProgressIndicator( Marker(
value: loadingProgress.expectedTotalBytes != null markerId: MarkerId(widget.shift.id),
? loadingProgress.cumulativeBytesLoaded / position: position,
loadingProgress.expectedTotalBytes! infoWindow: InfoWindow(
: null, title: widget.shift.location,
), snippet: widget.shift.locationAddress,
); ),
},
), ),
); );
} }
String _generateStaticMapUrl() { @override
// Base URL Widget build(BuildContext context) {
const String baseUrl = "https://maps.googleapis.com/maps/api/staticmap"; // If coordinates are missing, we show a placeholder.
if (widget.shift.latitude == null || widget.shift.longitude == null) {
// Parameters return _buildPlaceholder(context, "Coordinates unavailable");
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 return Container(
// scale=2 for retina displays height: widget.height * 1.25, // Slightly taller to accommodate map controls
return "$baseUrl?center=$center&zoom=15&size=600x300&maptype=roadmap&markers=color:red%7C$center&key=${AppConfig.googleMapsApiKey}&scale=2"; 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) { Widget _buildPlaceholder(BuildContext context, String message) {
return Container( return Container(
height: height, height: widget.height,
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.secondary, color: UiColors.bgThird,
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(color: UiColors.border),
), ),
child: Center( child: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( const Icon(
UiIcons.mapPin, UiIcons.mapPin,
size: 32, size: 32,
color: UiColors.iconSecondary, color: UiColors.primary,
), ),
if (message.isNotEmpty) ...[ if (message.isNotEmpty) ...[
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),

View File

@@ -108,7 +108,6 @@ class ShiftLocationSection extends StatelessWidget {
ShiftLocationMap( ShiftLocationMap(
shift: shift, shift: shift,
height: 160,
borderRadius: UiConstants.radiusBase, borderRadius: UiConstants.radiusBase,
), ),
], ],

View File

@@ -661,6 +661,8 @@ query listCompletedApplicationsByStaffId(
status status
description description
durationDays durationDays
latitude
longitude
order { order {
id id

View File

@@ -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