import "package:flutter/material.dart"; import "../main.dart"; import "../models.dart"; import "../format.dart"; import "../theme.dart"; import "car_detail_screen.dart"; import "car_form_sheet.dart"; class _CarRow { final Car car; final ServiceRecord? latest; final int count; _CarRow(this.car, this.latest, this.count); } class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override State createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { late Future> _future; @override void initState() { super.initState(); _future = _load(); } Future> _load() async { final cars = await apiClient.listCars(); final rows = <_CarRow>[]; for (final c in cars) { final services = await apiClient.listCarServices(c.id); rows.add(_CarRow(c, services.isNotEmpty ? services.first : null, services.length)); } return rows; } Future _refresh() async { final f = _load(); setState(() => _future = f); await f; } Future _addCar() async { final created = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => const CarFormSheet(), ); if (created != null && mounted) { await Navigator.push( context, MaterialPageRoute(builder: (_) => CarDetailScreen(carId: created.id)), ); await _refresh(); } } void _toggleTheme() { final next = Theme.of(context).brightness == Brightness.dark ? "light" : "dark"; appSettings.patch(theme: next); // optimistic + persisted locally apiClient.updateMe({"theme": next}).then((_) {}).catchError((_) {}); } @override Widget build(BuildContext context) { final dark = DriverVault.isDark(context); return Scaffold( floatingActionButton: FloatingActionButton.extended( onPressed: _addCar, icon: const Icon(Icons.add), label: const Text("Add car"), ), body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Home header — eyebrow + title on the left, quick actions right. Padding( padding: const EdgeInsets.fromLTRB(20, 14, 16, 8), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("YOUR CARS", style: DriverVault.mono(context, size: 10, weight: FontWeight.w500, color: DriverVault.muted(context)) .copyWith(letterSpacing: 2.2)), const SizedBox(height: 2), const Text("Garage", style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, letterSpacing: -0.5)), ], ), ), _HeaderButton( icon: dark ? Icons.light_mode_outlined : Icons.dark_mode_outlined, tooltip: dark ? "Light mode" : "Dark mode", onTap: _toggleTheme, ), const SizedBox(width: 8), _HeaderButton( icon: Icons.logout, tooltip: "Log out", onTap: () => authService.logout(), ), ], ), ), Expanded( child: RefreshIndicator( onRefresh: _refresh, child: FutureBuilder>( future: _future, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return _ErrorView(message: "${snap.error}", onRetry: _refresh); } final rows = snap.data ?? []; if (rows.isEmpty) { return ListView(children: const [ SizedBox(height: 80), Center(child: Text("No cars yet.")), ]); } return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 4, 16, 96), itemCount: rows.length, itemBuilder: (context, i) => _CarCard(row: rows[i], onChanged: _refresh), ); }, ), ), ), ], ), ), ); } } /// A bordered 40×40 square icon button used in the home header (mockup style). class _HeaderButton extends StatelessWidget { final IconData icon; final String tooltip; final VoidCallback onTap; const _HeaderButton({required this.icon, required this.tooltip, required this.onTap}); @override Widget build(BuildContext context) { return Tooltip( message: tooltip, child: Material( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(DriverVault.radiusControl), child: InkWell( borderRadius: BorderRadius.circular(DriverVault.radiusControl), onTap: onTap, child: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(DriverVault.radiusControl), border: Border.all(color: DriverVault.isDark(context) ? DriverVault.darkBorderStrong : DriverVault.ink200), ), child: Icon(icon, size: 19, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ), ), ); } } class _CarCard extends StatelessWidget { final _CarRow row; final Future Function() onChanged; const _CarCard({required this.row, required this.onChanged}); @override Widget build(BuildContext context) { final status = serviceStatus(row.latest, row.car); return Card( margin: const EdgeInsets.only(bottom: 10), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: DriverVault.isDark(context) ? DriverVault.darkBorder : DriverVault.ink100), ), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () async { await Navigator.push( context, MaterialPageRoute(builder: (_) => CarDetailScreen(carId: row.car.id)), ); await onChanged(); }, child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(row.car.name, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 16, letterSpacing: -0.3)), if (row.car.subtitle.isNotEmpty) Text(row.car.subtitle, style: TextStyle(color: DriverVault.muted(context), fontSize: 12)), ], ), ), _Badge(status: status), ], ), if (!row.car.isOwner) ...[ const SizedBox(height: 8), _SharedChip(readOnly: row.car.isReadOnly), ], ..._serviceLife(context, status), const SizedBox(height: 12), _kv(context, "Last service", formatDate(row.latest?.date)), _kv(context, "Current odometer", formatKm(row.car.currentKm)), _kv(context, "Next due", formatDate(row.latest?.nextServiceDate)), _kv(context, "Next due km", formatKm(row.latest?.nextServiceKm)), const SizedBox(height: 6), Text("${row.count} service record${row.count == 1 ? '' : 's'}", style: TextStyle(color: DriverVault.muted(context), fontSize: 11)), ], ), ), ), ); } Widget _kv(BuildContext context, String k, String v) => Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(k, style: TextStyle(color: DriverVault.muted(context), fontSize: 13)), Text(v, style: DriverVault.mono(context, size: 13, weight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface)), ], ), ); /// Service-life bar: fraction of the km interval used up (echoes the mockup's /// battery bar). Returns [] when there isn't enough data to compute it. List _serviceLife(BuildContext context, Status status) { final interval = row.car.serviceIntervalKm; final nextKm = row.latest?.nextServiceKm ?? 0; final currentKm = row.car.currentKm; if (interval <= 0 || nextKm <= 0 || currentKm <= 0) return const []; final remaining = nextKm - currentKm; final pct = (100 * (1 - remaining / interval)).clamp(0, 100).round(); final tone = status.fg(DriverVault.isDark(context)); return [ const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("SERVICE LIFE", style: DriverVault.mono(context, size: 9, weight: FontWeight.w500, color: DriverVault.muted(context)).copyWith(letterSpacing: 1.6)), Text("$pct%", style: DriverVault.mono(context, size: 11, weight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface)), ], ), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(99), child: LinearProgressIndicator( value: pct / 100, minHeight: 7, backgroundColor: DriverVault.isDark(context) ? DriverVault.darkSunken : DriverVault.ink100, valueColor: AlwaysStoppedAnimation(tone), ), ), ]; } } class _SharedChip extends StatelessWidget { final bool readOnly; const _SharedChip({required this.readOnly}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: DriverVault.brandTint(context), borderRadius: BorderRadius.circular(999), ), child: Text( readOnly ? "Shared · read-only" : "Shared", style: TextStyle(color: DriverVault.brandOnTint(context), fontSize: 11, fontWeight: FontWeight.w600), ), ); } } class _Badge extends StatelessWidget { final Status status; const _Badge({required this.status}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: status.bg(DriverVault.isDark(context)), borderRadius: BorderRadius.circular(999)), child: Text(status.label, style: TextStyle(color: status.fg(DriverVault.isDark(context)), fontSize: 12, fontWeight: FontWeight.w600)), ); } } class _ErrorView extends StatelessWidget { final String message; final Future Function() onRetry; const _ErrorView({required this.message, required this.onRetry}); @override Widget build(BuildContext context) { return ListView( children: [ const SizedBox(height: 80), Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ const Icon(Icons.error_outline, color: DriverVault.danger, size: 40), const SizedBox(height: 12), Text(message, textAlign: TextAlign.center), const SizedBox(height: 12), FilledButton(onPressed: onRetry, child: const Text("Retry")), ], ), ), ), ], ); } }