feat(location): Add latitude and longitude to shift details and integrate Google Maps for location display
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"GOOGLE_MAPS_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU"
|
"GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0"
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ class ShiftLocationSection extends StatelessWidget {
|
|||||||
|
|
||||||
ShiftLocationMap(
|
ShiftLocationMap(
|
||||||
shift: shift,
|
shift: shift,
|
||||||
height: 160,
|
|
||||||
borderRadius: UiConstants.radiusBase,
|
borderRadius: UiConstants.radiusBase,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -661,6 +661,8 @@ query listCompletedApplicationsByStaffId(
|
|||||||
status
|
status
|
||||||
description
|
description
|
||||||
durationDays
|
durationDays
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
|
|
||||||
order {
|
order {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -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