add dashboard to phone app
This commit is contained in:
@@ -7,14 +7,22 @@ import '../providers/client_providers.dart';
|
||||
import '../../../core/models/marketplace.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../widgets/preferences_bar.dart';
|
||||
import 'client_shell.dart';
|
||||
import 'widgets/marketplace_vehicle_card.dart';
|
||||
import 'widgets/offer_banner.dart';
|
||||
|
||||
const _orange = Color(0xFFFF6B00);
|
||||
const _blue = Color(0xFF1A56DB);
|
||||
|
||||
const _allCarTypes = [
|
||||
'ECONOMY', 'COMPACT', 'MIDSIZE', 'FULLSIZE',
|
||||
'SUV', 'LUXURY', 'VAN', 'TRUCK',
|
||||
'ECONOMY',
|
||||
'COMPACT',
|
||||
'MIDSIZE',
|
||||
'FULLSIZE',
|
||||
'SUV',
|
||||
'LUXURY',
|
||||
'VAN',
|
||||
'TRUCK',
|
||||
];
|
||||
|
||||
String _capitalize(String s) =>
|
||||
@@ -62,8 +70,9 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
endDate: _endDate,
|
||||
categories: _selectedTypes.isEmpty ? null : Set.from(_selectedTypes),
|
||||
maxPriceCents: _priceFilterActive ? _maxPriceCents : null,
|
||||
minPriceCents:
|
||||
_priceFilterActive && _minPriceCents > 0 ? _minPriceCents : null,
|
||||
minPriceCents: _priceFilterActive && _minPriceCents > 0
|
||||
? _minPriceCents
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,9 +87,9 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
? DateTimeRange(start: _startDate!, end: _endDate!)
|
||||
: DateTimeRange(start: now, end: now.add(const Duration(days: 1))),
|
||||
builder: (context, child) => Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: cs.copyWith(primary: _orange),
|
||||
),
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(colorScheme: cs.copyWith(primary: _orange)),
|
||||
child: child!,
|
||||
),
|
||||
);
|
||||
@@ -174,7 +183,9 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
}
|
||||
|
||||
List<MarketplaceVehicle> _applyClientFilters(
|
||||
List<MarketplaceVehicle> all, SearchParams params) {
|
||||
List<MarketplaceVehicle> all,
|
||||
SearchParams params,
|
||||
) {
|
||||
var result = all;
|
||||
final cats = params.categories;
|
||||
if (cats != null && cats.length > 1) {
|
||||
@@ -199,14 +210,14 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
final typeLabel = _selectedTypes.isEmpty
|
||||
? l10n.allCarTypes
|
||||
: _selectedTypes.length == 1
|
||||
? _capitalize(_selectedTypes.first)
|
||||
: '${_selectedTypes.length} types';
|
||||
? _capitalize(_selectedTypes.first)
|
||||
: '${_selectedTypes.length} types';
|
||||
|
||||
final priceLabel = _priceFilterActive
|
||||
? '\$${(_minPriceCents / 100).toStringAsFixed(0)} – \$${(_maxPriceCents / 100).toStringAsFixed(0)}/day'
|
||||
: l10n.anyPrice;
|
||||
|
||||
return Scaffold(
|
||||
return ClientShell(
|
||||
backgroundColor: cs.surface,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
@@ -268,12 +279,14 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
children: [
|
||||
// Pickup
|
||||
_SearchInputBox(
|
||||
onTap: () => citiesAsync
|
||||
.whenData((c) => _showCitySheet(c, l10n)),
|
||||
onTap: () => citiesAsync.whenData(
|
||||
(c) => _showCitySheet(c, l10n),
|
||||
),
|
||||
child: _LocationRow(
|
||||
icon: Icons.radio_button_checked,
|
||||
iconColor: _orange,
|
||||
text: _city ??
|
||||
text:
|
||||
_city ??
|
||||
(_sameDropoff
|
||||
? l10n.findCarToRent
|
||||
: l10n.pickupLocation),
|
||||
@@ -307,17 +320,20 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
// Drop-off box
|
||||
_SearchInputBox(
|
||||
onTap: () => citiesAsync.whenData(
|
||||
(c) => _showDropoffCitySheet(c, l10n)),
|
||||
(c) => _showDropoffCitySheet(c, l10n),
|
||||
),
|
||||
child: _LocationRow(
|
||||
icon: Icons.location_on,
|
||||
iconColor: _blue,
|
||||
text: _dropoffCity ??
|
||||
text:
|
||||
_dropoffCity ??
|
||||
l10n.dropoffLocation,
|
||||
placeholder: _dropoffCity == null,
|
||||
cs: cs,
|
||||
onClear: _dropoffCity != null
|
||||
? () => setState(
|
||||
() => _dropoffCity = null)
|
||||
() => _dropoffCity = null,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
@@ -343,13 +359,18 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Icon(Icons.calendar_today_outlined,
|
||||
size: 18, color: cs.onSurfaceVariant),
|
||||
Icon(
|
||||
Icons.calendar_today_outlined,
|
||||
size: 18,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n.pickDates,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 15),
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -382,10 +403,10 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
onTap: () => _showPriceSheet(l10n),
|
||||
onClear: _priceFilterActive
|
||||
? () => setState(() {
|
||||
_priceFilterActive = false;
|
||||
_minPriceCents = 0;
|
||||
_maxPriceCents = 50000;
|
||||
})
|
||||
_priceFilterActive = false;
|
||||
_minPriceCents = 0;
|
||||
_maxPriceCents = 50000;
|
||||
})
|
||||
: null,
|
||||
),
|
||||
),
|
||||
@@ -426,28 +447,38 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 10),
|
||||
child: searchAsync.when(
|
||||
loading: () => Row(children: [
|
||||
SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: cs.onSurfaceVariant,
|
||||
loading: () => Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.5,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.searching,
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.searching,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
]),
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
data: (r) {
|
||||
final count =
|
||||
_applyClientFilters(r.data, params).length;
|
||||
final count = _applyClientFilters(
|
||||
r.data,
|
||||
params,
|
||||
).length;
|
||||
return Text(
|
||||
l10n.vehiclesFound(count),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 13),
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -458,12 +489,60 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// ── Active offers ────────────────────────────────────────────────
|
||||
ref
|
||||
.watch(offersProvider)
|
||||
.when(
|
||||
loading: () =>
|
||||
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||
error: (_, _) =>
|
||||
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||
data: (offers) {
|
||||
if (offers.isEmpty) {
|
||||
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||
}
|
||||
return SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 10),
|
||||
child: Text(
|
||||
'Special Offers',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 110,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: offers.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const SizedBox(width: 10),
|
||||
itemBuilder: (_, i) =>
|
||||
OfferBanner(offer: offers[i]),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// ── Results ──────────────────────────────────────────────────────
|
||||
searchAsync.when(
|
||||
loading: () => const SliverFillRemaining(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: _orange, strokeWidth: 2.5),
|
||||
color: _orange,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (_, _) => SliverFillRemaining(
|
||||
@@ -477,7 +556,8 @@ class _ClientHomeScreenState extends ConsumerState<ClientHomeScreen> {
|
||||
final vehicles = _applyClientFilters(result.data, params);
|
||||
if (vehicles.isEmpty) {
|
||||
return SliverFillRemaining(
|
||||
child: _EmptyBody(l10n: l10n, cs: cs));
|
||||
child: _EmptyBody(l10n: l10n, cs: cs),
|
||||
);
|
||||
}
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
|
||||
@@ -511,8 +591,11 @@ class _DropoffTab extends StatelessWidget {
|
||||
final bool active;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _DropoffTab(
|
||||
{required this.label, required this.active, required this.onTap});
|
||||
const _DropoffTab({
|
||||
required this.label,
|
||||
required this.active,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -579,26 +662,25 @@ class _LocationRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: iconColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: placeholder ? cs.onSurfaceVariant : cs.onSurface,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Icon(icon, size: 18, color: iconColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: placeholder ? cs.onSurfaceVariant : cs.onSurface,
|
||||
fontSize: 15,
|
||||
),
|
||||
if (onClear != null)
|
||||
GestureDetector(
|
||||
onTap: onClear,
|
||||
child:
|
||||
Icon(Icons.close, size: 16, color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
);
|
||||
),
|
||||
),
|
||||
if (onClear != null)
|
||||
GestureDetector(
|
||||
onTap: onClear,
|
||||
child: Icon(Icons.close, size: 16, color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Date range display ────────────────────────────────────────────────────────
|
||||
@@ -629,8 +711,11 @@ class _DateRangeRow extends StatelessWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Icon(Icons.arrow_forward,
|
||||
size: 14, color: cs.onSurfaceVariant),
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 14,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
_DateChunk(
|
||||
date: dayFmt.format(end),
|
||||
@@ -647,23 +732,23 @@ class _DateChunk extends StatelessWidget {
|
||||
final String sub;
|
||||
final ColorScheme cs;
|
||||
|
||||
const _DateChunk(
|
||||
{required this.date, required this.sub, required this.cs});
|
||||
const _DateChunk({required this.date, required this.sub, required this.cs});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
Text(date,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600)),
|
||||
const SizedBox(width: 6),
|
||||
Text(sub,
|
||||
style:
|
||||
TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
);
|
||||
children: [
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(sub, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Option tile (car type / price) ────────────────────────────────────────────
|
||||
@@ -696,15 +781,11 @@ class _OptionTile extends StatelessWidget {
|
||||
? _blue.withValues(alpha: 0.12)
|
||||
: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: active ? _blue : cs.outlineVariant,
|
||||
),
|
||||
border: Border.all(color: active ? _blue : cs.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 16,
|
||||
color: active ? _blue : cs.onSurfaceVariant),
|
||||
Icon(icon, size: 16, color: active ? _blue : cs.onSurfaceVariant),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -712,8 +793,7 @@ class _OptionTile extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight:
|
||||
active ? FontWeight.w600 : FontWeight.normal,
|
||||
fontWeight: active ? FontWeight.w600 : FontWeight.normal,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
@@ -722,9 +802,11 @@ class _OptionTile extends StatelessWidget {
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: onClear,
|
||||
child: Icon(Icons.close,
|
||||
size: 14,
|
||||
color: active ? _blue : cs.onSurfaceVariant),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 14,
|
||||
color: active ? _blue : cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -843,7 +925,11 @@ class _CarTypeSheetState extends State<_CarTypeSheet> {
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16, 10, 16, 16 + MediaQuery.of(context).padding.bottom),
|
||||
16,
|
||||
10,
|
||||
16,
|
||||
16 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
@@ -853,12 +939,17 @@ class _CarTypeSheetState extends State<_CarTypeSheet> {
|
||||
backgroundColor: _orange,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(l10n.apply,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
child: Text(
|
||||
l10n.apply,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -903,8 +994,9 @@ class _PriceSheetState extends State<_PriceSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
String _fmt(double v) =>
|
||||
v >= _PriceSheet._cap ? '\$${v.toStringAsFixed(0)}+' : '\$${v.toStringAsFixed(0)}';
|
||||
String _fmt(double v) => v >= _PriceSheet._cap
|
||||
? '\$${v.toStringAsFixed(0)}+'
|
||||
: '\$${v.toStringAsFixed(0)}';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -912,8 +1004,9 @@ class _PriceSheetState extends State<_PriceSheet> {
|
||||
final l10n = widget.l10n;
|
||||
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -941,8 +1034,10 @@ class _PriceSheetState extends State<_PriceSheet> {
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(l10n.clearAll,
|
||||
style: const TextStyle(fontSize: 14)),
|
||||
child: Text(
|
||||
l10n.clearAll,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -955,8 +1050,7 @@ class _PriceSheetState extends State<_PriceSheet> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_PriceTag(label: _fmt(_range.start), cs: cs),
|
||||
Icon(Icons.arrow_forward,
|
||||
size: 14, color: cs.onSurfaceVariant),
|
||||
Icon(Icons.arrow_forward, size: 14, color: cs.onSurfaceVariant),
|
||||
_PriceTag(label: '${_fmt(_range.end)}/day', cs: cs),
|
||||
],
|
||||
),
|
||||
@@ -987,7 +1081,11 @@ class _PriceSheetState extends State<_PriceSheet> {
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16, 4, 16, 16 + MediaQuery.of(context).padding.bottom),
|
||||
16,
|
||||
4,
|
||||
16,
|
||||
16 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
@@ -1001,12 +1099,17 @@ class _PriceSheetState extends State<_PriceSheet> {
|
||||
backgroundColor: _orange,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(l10n.apply,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
child: Text(
|
||||
l10n.apply,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1024,21 +1127,21 @@ class _PriceTag extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.outlineVariant),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15),
|
||||
),
|
||||
);
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.outlineVariant),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── City Sheet ────────────────────────────────────────────────────────────────
|
||||
@@ -1089,9 +1192,10 @@ class _CitySheetState extends State<_CitySheet> {
|
||||
Text(
|
||||
widget.titleOverride ?? widget.l10n.selectCity,
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cs.onSurface),
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
@@ -1099,10 +1203,12 @@ class _CitySheetState extends State<_CitySheet> {
|
||||
style: TextStyle(color: cs.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.l10n.searchCities,
|
||||
prefixIcon: Icon(Icons.search,
|
||||
size: 20, color: cs.onSurfaceVariant),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 8),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
onChanged: (v) => setState(() => _query = v),
|
||||
),
|
||||
@@ -1114,10 +1220,14 @@ class _CitySheetState extends State<_CitySheet> {
|
||||
controller: controller,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(Icons.public_outlined,
|
||||
color: cs.onSurfaceVariant),
|
||||
title: Text(widget.l10n.allCities,
|
||||
style: TextStyle(color: cs.onSurface)),
|
||||
leading: Icon(
|
||||
Icons.public_outlined,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
title: Text(
|
||||
widget.l10n.allCities,
|
||||
style: TextStyle(color: cs.onSurface),
|
||||
),
|
||||
selected: widget.selected == null,
|
||||
selectedColor: _orange,
|
||||
trailing: widget.selected == null
|
||||
@@ -1125,19 +1235,21 @@ class _CitySheetState extends State<_CitySheet> {
|
||||
: null,
|
||||
onTap: () => widget.onSelect(null),
|
||||
),
|
||||
...filtered.map((city) => ListTile(
|
||||
leading: Icon(Icons.location_city_outlined,
|
||||
color: cs.onSurfaceVariant),
|
||||
title: Text(city,
|
||||
style: TextStyle(color: cs.onSurface)),
|
||||
selected: widget.selected == city,
|
||||
selectedColor: _orange,
|
||||
trailing: widget.selected == city
|
||||
? const Icon(Icons.check,
|
||||
color: _orange, size: 18)
|
||||
: null,
|
||||
onTap: () => widget.onSelect(city),
|
||||
)),
|
||||
...filtered.map(
|
||||
(city) => ListTile(
|
||||
leading: Icon(
|
||||
Icons.location_city_outlined,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
title: Text(city, style: TextStyle(color: cs.onSurface)),
|
||||
selected: widget.selected == city,
|
||||
selectedColor: _orange,
|
||||
trailing: widget.selected == city
|
||||
? const Icon(Icons.check, color: _orange, size: 18)
|
||||
: null,
|
||||
onTap: () => widget.onSelect(city),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1155,13 +1267,13 @@ class _SheetHandle extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Error / Empty states ─────────────────────────────────────────────────────
|
||||
@@ -1171,46 +1283,48 @@ class _ErrorBody extends StatelessWidget {
|
||||
final AppLocalizations l10n;
|
||||
final ColorScheme cs;
|
||||
|
||||
const _ErrorBody(
|
||||
{required this.onRetry, required this.l10n, required this.cs});
|
||||
const _ErrorBody({
|
||||
required this.onRetry,
|
||||
required this.l10n,
|
||||
required this.cs,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.wifi_off_outlined,
|
||||
size: 48, color: cs.onSurfaceVariant),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.couldNotLoadVehicles,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.checkConnectionRetry,
|
||||
style:
|
||||
TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
OutlinedButton(
|
||||
onPressed: onRetry,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: _orange,
|
||||
side: const BorderSide(color: _orange),
|
||||
),
|
||||
child: Text(l10n.retry),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.wifi_off_outlined, size: 48, color: cs.onSurfaceVariant),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.couldNotLoadVehicles,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.checkConnectionRetry,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
OutlinedButton(
|
||||
onPressed: onRetry,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: _orange,
|
||||
side: const BorderSide(color: _orange),
|
||||
),
|
||||
child: Text(l10n.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _EmptyBody extends StatelessWidget {
|
||||
@@ -1221,29 +1335,29 @@ class _EmptyBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.search_off, size: 56, color: cs.outlineVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noVehiclesFound,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.tryDifferentSearch,
|
||||
style:
|
||||
TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.search_off, size: 56, color: cs.outlineVariant),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noVehiclesFound,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.tryDifferentSearch,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
class ClientShell extends ConsumerWidget {
|
||||
final Widget child;
|
||||
class ClientShell extends StatelessWidget {
|
||||
final PreferredSizeWidget? appBar;
|
||||
final Widget body;
|
||||
final Widget? floatingActionButton;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const ClientShell({super.key, required this.child});
|
||||
const ClientShell({
|
||||
super.key,
|
||||
this.appBar,
|
||||
required this.body,
|
||||
this.floatingActionButton,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final location = GoRouterState.of(context).matchedLocation;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
@@ -17,7 +25,10 @@ class ClientShell extends ConsumerWidget {
|
||||
if (location == '/client/bookings') currentIndex = 1;
|
||||
|
||||
return Scaffold(
|
||||
body: child,
|
||||
appBar: appBar,
|
||||
backgroundColor: backgroundColor,
|
||||
body: body,
|
||||
floatingActionButton: floatingActionButton,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: currentIndex,
|
||||
onDestinationSelected: (i) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/providers/auth_provider.dart';
|
||||
import 'client_shell.dart';
|
||||
|
||||
class MyBookingsScreen extends ConsumerWidget {
|
||||
const MyBookingsScreen({super.key});
|
||||
@@ -11,7 +12,7 @@ class MyBookingsScreen extends ConsumerWidget {
|
||||
final auth = ref.watch(authProvider);
|
||||
|
||||
if (auth.status != AuthStatus.authenticated) {
|
||||
return Scaffold(
|
||||
return ClientShell(
|
||||
appBar: AppBar(title: const Text('My Bookings')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
@@ -19,8 +20,11 @@ class MyBookingsScreen extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.bookmark_outline,
|
||||
size: 64, color: Color(0xFFD1D5DB)),
|
||||
const Icon(
|
||||
Icons.bookmark_outline,
|
||||
size: 64,
|
||||
color: Color(0xFFD1D5DB),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Sign in to view your bookings',
|
||||
@@ -48,7 +52,7 @@ class MyBookingsScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return ClientShell(
|
||||
appBar: AppBar(title: const Text('My Bookings')),
|
||||
body: const Center(
|
||||
child: Column(
|
||||
@@ -58,10 +62,7 @@ class MyBookingsScreen extends ConsumerWidget {
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'No bookings yet',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
style: TextStyle(fontSize: 16, color: Color(0xFF6B7280)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user