Initial commit
This commit is contained in:
475
lib/features/dashboard/screens/home_screen.dart
Normal file
475
lib/features/dashboard/screens/home_screen.dart
Normal file
@@ -0,0 +1,475 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:doormile/main.dart';
|
||||
import 'package:doormile/shared/models/models.dart';
|
||||
import 'package:doormile/shared/state/app_state.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
import 'package:doormile/core/widgets/logo.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<AppState>().fetchOrders();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<AppState>();
|
||||
final active = state.orders.where((o) => o.status == OrderStatus.active).firstOrNull;
|
||||
|
||||
return AppBackground(
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_hero(context, state),
|
||||
const SizedBox(height: 26),
|
||||
active != null ? _activeShipment(context, active) : _emptyShipment(context),
|
||||
const SizedBox(height: 18),
|
||||
_ecoBanner(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).padding.top,
|
||||
color: AppColors.primaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _hero(BuildContext context, AppState state) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
height: 196,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.primaryContainer, AppColors.primary],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)),
|
||||
child: AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -20,
|
||||
top: 20,
|
||||
child: Transform.rotate(
|
||||
angle: -0.1,
|
||||
child: Icon(Icons.local_shipping_rounded, size: 150, color: Colors.white.withValues(alpha: 0.04)),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 40,
|
||||
bottom: -30,
|
||||
child: Transform.rotate(
|
||||
angle: 0.15,
|
||||
child: Icon(Icons.route_rounded, size: 100, color: Colors.white.withValues(alpha: 0.03)),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const DoormileMark(size: 48, color: Colors.white, glyphColor: AppColors.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on_rounded, color: Colors.white70, size: 15),
|
||||
const SizedBox(width: 4),
|
||||
Text('Bengaluru', style: AppTheme.labelMd.copyWith(color: Colors.white)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Hi, ${state.firstName.isEmpty ? 'Admin' : state.firstName}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTheme.headlineMd.copyWith(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
'Let deliveries move with less friction',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTheme.caption.copyWith(color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.16),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white24),
|
||||
),
|
||||
child: const Icon(Icons.notifications_none_rounded, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 130,
|
||||
child: AppCard(
|
||||
padding: const EdgeInsets.all(18),
|
||||
radius: BorderRadius.circular(24),
|
||||
border: Border.all(color: Colors.white),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Send a parcel', style: AppTheme.headlineLg),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Fast pickup, live tracking, EV-first delivery.',
|
||||
style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final draft = context.read<AppState>().draft;
|
||||
draft.pickupAddress = 'Current Location (GPS)';
|
||||
Navigator.pushNamed(context, Routes.bookingPickup);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_forward_rounded, size: 18),
|
||||
label: const Text('Book Now'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SizedBox(
|
||||
width: 90,
|
||||
height: 90,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Icon(Icons.local_shipping_rounded, size: 68, color: AppColors.primary),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: IconBadge(
|
||||
icon: Icons.eco_rounded,
|
||||
bg: AppColors.tertiaryContainer,
|
||||
color: AppColors.tertiary,
|
||||
size: 28,
|
||||
iconSize: 16,
|
||||
circle: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 310),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _activeShipment(BuildContext context, Order active) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Live Shipment', style: AppTheme.headlineSm.copyWith(fontWeight: FontWeight.w800)),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pushNamed(context, Routes.myOrders),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text('View Details', style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pushNamed(context, Routes.liveTracking, arguments: active),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primary, AppColors.secondary],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(color: AppColors.primary.withValues(alpha: 0.3), blurRadius: 24, offset: const Offset(0, 10)),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(2), // Gradient border width
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryFixed,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(Icons.inventory_2_rounded, color: AppColors.primary, size: 28),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text('PENDING PICKUP', style: AppTheme.caption.copyWith(color: AppColors.primary, fontWeight: FontWeight.w800, letterSpacing: 1.2)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(active.id, style: AppTheme.headlineSm),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: AppColors.outlineVariant),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Live', style: TextStyle(color: AppColors.onSurface, fontWeight: FontWeight.w600, fontSize: 12)),
|
||||
SizedBox(width: 4),
|
||||
Icon(Icons.arrow_forward_ios_rounded, size: 10, color: AppColors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.surfaceContainerHigh),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Column(
|
||||
children: [
|
||||
Icon(Icons.my_location_rounded, size: 20, color: AppColors.primary),
|
||||
SizedBox(height: 4),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
child: VerticalDivider(color: AppColors.outlineVariant, thickness: 2),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Icon(Icons.pin_drop_rounded, size: 20, color: AppColors.tertiary),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Pickup Location', style: AppTheme.caption),
|
||||
Text(active.fromLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTheme.labelMd),
|
||||
const SizedBox(height: 20),
|
||||
Text('Destination', style: AppTheme.caption),
|
||||
Text(active.toLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTheme.labelMd),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _trackNode(String title, String subtitle, bool active) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTheme.caption.copyWith(
|
||||
color: active ? AppColors.secondary : AppColors.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w700,
|
||||
)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _emptyShipment(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader(title: 'Active Shipment'),
|
||||
const SizedBox(height: 12),
|
||||
AppCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 18),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.primary.withValues(alpha: 0.1), AppColors.primary.withValues(alpha: 0.25)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withValues(alpha: 0.15),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 54,
|
||||
height: 54,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inbox_rounded,
|
||||
size: 28,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text('No active shipments', style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w800)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'You do not have any deliveries in progress right now.',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTheme.caption,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SecondaryButton(
|
||||
label: 'Book a Shipment',
|
||||
onPressed: () {
|
||||
final draft = context.read<AppState>().draft;
|
||||
draft.pickupAddress = 'Current Location (GPS)';
|
||||
Navigator.pushNamed(context, Routes.bookingPickup);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _ecoBanner() {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: EcoBanner(text: 'This delivery offsets 1.2kg of CO2 emissions.'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
189
lib/features/dashboard/screens/main_shell.dart
Normal file
189
lib/features/dashboard/screens/main_shell.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import 'package:doormile/main.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/features/dashboard/screens/home_screen.dart';
|
||||
import 'package:doormile/features/tracking/screens/my_orders_screen.dart';
|
||||
import 'package:doormile/features/dashboard/screens/summary_screen.dart';
|
||||
import 'package:doormile/features/dashboard/screens/profile_screen.dart';
|
||||
|
||||
/// Root scaffold hosting the 5-tab bottom navigation.
|
||||
class MainShell extends StatefulWidget {
|
||||
final int initialIndex;
|
||||
const MainShell({super.key, this.initialIndex = 0});
|
||||
|
||||
@override
|
||||
State<MainShell> createState() => _MainShellState();
|
||||
}
|
||||
|
||||
class _MainShellState extends State<MainShell> {
|
||||
late int _index = widget.initialIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_requestPermissions();
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
final status = await Permission.locationWhenInUse.status;
|
||||
if (!status.isGranted) {
|
||||
await Permission.locationWhenInUse.request();
|
||||
}
|
||||
}
|
||||
|
||||
// Tab indices: 0 Home, 1 Track, 3 Wallet, 4 Profile. 2 = Book (action).
|
||||
final _pages = const [
|
||||
HomeScreen(),
|
||||
MyOrdersScreen(embedded: true),
|
||||
SizedBox.shrink(), // Book slot — never shown (navigates away)
|
||||
SummaryScreen(),
|
||||
ProfileScreen(),
|
||||
];
|
||||
|
||||
void _onTap(int i) {
|
||||
if (i == 2) {
|
||||
Navigator.pushNamed(context, Routes.bookingPickup);
|
||||
return;
|
||||
}
|
||||
setState(() => _index = i);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
body: IndexedStack(index: _index, children: _pages),
|
||||
extendBody: true,
|
||||
bottomNavigationBar: _BottomBar(index: _index, onTap: _onTap),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomBar extends StatelessWidget {
|
||||
final int index;
|
||||
final ValueChanged<int> onTap;
|
||||
const _BottomBar({required this.index, required this.onTap});
|
||||
|
||||
static const _items = [
|
||||
(_NavItem(Icons.home_rounded, Icons.home_outlined, 'Home')),
|
||||
(_NavItem(Icons.map_rounded, Icons.map_outlined, 'Track')),
|
||||
(_NavItem(Icons.add_circle, Icons.add_circle, 'Book')),
|
||||
(_NavItem(Icons.bar_chart_rounded, Icons.bar_chart_outlined, 'Summary')),
|
||||
(_NavItem(Icons.person_rounded, Icons.person_outline, 'Profile')),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(14, 0, 14, 12),
|
||||
child: Container(
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainerLowest.withValues(alpha: 0.96),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: Colors.white),
|
||||
boxShadow: AppTheme.cardShadow,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: List.generate(_items.length, (i) {
|
||||
final item = _items[i];
|
||||
final active = i == index;
|
||||
if (i == 2) {
|
||||
// Centre "Book" action button.
|
||||
return _BookButton(onTap: () => onTap(2));
|
||||
}
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => onTap(i),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? AppColors.primaryFixed : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Icon(active ? item.active : item.inactive,
|
||||
color: active ? AppColors.primary : AppColors.onSurfaceVariant,
|
||||
size: 22),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(item.label,
|
||||
maxLines: 1,
|
||||
style: AppTheme.caption.copyWith(
|
||||
color: active ? AppColors.primary : AppColors.onSurfaceVariant,
|
||||
fontWeight: active ? FontWeight.w700 : FontWeight.w500,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BookButton extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const _BookButton({required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 46,
|
||||
height: 46,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primaryContainer, AppColors.primary],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withValues(alpha: 0.28),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: const Icon(Icons.add, color: Colors.white, size: 28),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text('Book',
|
||||
style: AppTheme.caption.copyWith(
|
||||
color: AppColors.primary, fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavItem {
|
||||
final IconData active;
|
||||
final IconData inactive;
|
||||
final String label;
|
||||
const _NavItem(this.active, this.inactive, this.label);
|
||||
}
|
||||
264
lib/features/dashboard/screens/profile_screen.dart
Normal file
264
lib/features/dashboard/screens/profile_screen.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:doormile/main.dart';
|
||||
import 'package:doormile/shared/state/app_state.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
import 'package:doormile/core/widgets/logo.dart';
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<AppState>();
|
||||
return AppBackground(
|
||||
child: Column(
|
||||
children: [
|
||||
const ImmersiveHeader(
|
||||
title: 'My Profile',
|
||||
subtitle: 'Addresses, support, and preferences',
|
||||
icon: Icons.person_rounded,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileAvatar(context, state),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ProfileStatCard(
|
||||
value: '${state.orders.length * 4 + 2}',
|
||||
label: 'Parcels',
|
||||
icon: Icons.inventory_2_rounded,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _ProfileStatCard(
|
||||
value: '4.9',
|
||||
label: 'Rating',
|
||||
icon: Icons.star_rounded,
|
||||
color: Colors.amber,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_tile(context, Icons.location_on_outlined, 'My addresses',
|
||||
onTap: () => Navigator.pushNamed(context, Routes.addresses)),
|
||||
_tile(context, Icons.notifications_outlined, 'Notifications', dot: true),
|
||||
_tile(context, Icons.card_giftcard_outlined, 'Refer & earn'),
|
||||
_tile(context, Icons.language_outlined, 'Language', sub: 'English'),
|
||||
_tile(context, Icons.help_outline, 'Help & support',
|
||||
onTap: () => Navigator.pushNamed(context, Routes.help)),
|
||||
const SizedBox(height: 4),
|
||||
AppCard(
|
||||
onTap: () => _confirmLogout(context, state),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout, color: AppColors.primary),
|
||||
const SizedBox(width: 16),
|
||||
Text('Logout',
|
||||
style: AppTheme.labelMd.copyWith(
|
||||
color: AppColors.primary, fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Doormile Customers V1.0.0', style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileAvatar(BuildContext context, AppState state) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
color: Colors.white,
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 10)],
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset(
|
||||
'assets/images/profileicon.png',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Icon(Icons.person, size: 50, color: Color(0xFFC60018)),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(color: Color(0xFFC60018), shape: BoxShape.circle),
|
||||
child: const Icon(Icons.edit, size: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.userName.isEmpty ? 'Admin User' : state.userName, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black87)),
|
||||
Text('+91 ${state.phone.isEmpty ? '98765 43210' : state.phone}', style: const TextStyle(fontSize: 14, color: Colors.black54)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _stat(String value, String label, Color color) {
|
||||
return const SizedBox.shrink(); // Replaced by _ProfileStatCard
|
||||
}
|
||||
|
||||
Widget _tile(BuildContext context, IconData icon, String title,
|
||||
{String? sub, bool dot = false, VoidCallback? onTap}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: AppCard(
|
||||
onTap: onTap ?? () {},
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: AppColors.primary, size: 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: AppTheme.labelMd),
|
||||
if (sub != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(sub, style: AppTheme.caption.copyWith(color: AppColors.onSurfaceVariant)),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
if (dot)
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.error, shape: BoxShape.circle),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded, color: AppColors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmLogout(BuildContext context, AppState state) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
backgroundColor: AppColors.surfaceContainerLowest,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text('Log out?', style: AppTheme.headlineSm),
|
||||
content: Text('You can sign back in anytime with your number.',
|
||||
style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Cancel',
|
||||
style: AppTheme.labelMd.copyWith(color: AppColors.onSurfaceVariant)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
state.logout();
|
||||
Navigator.pop(context);
|
||||
Navigator.pushNamedAndRemoveUntil(context, Routes.login, (r) => false);
|
||||
},
|
||||
child: Text('Logout', style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileStatCard extends StatelessWidget {
|
||||
final String value;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const _ProfileStatCard({required this.value, required this.label, required this.icon, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.surfaceContainerHigh, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(label, style: AppTheme.caption.copyWith(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: AppTheme.headlineMd.copyWith(color: AppColors.onSurface)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
217
lib/features/dashboard/screens/saved_addresses_screen.dart
Normal file
217
lib/features/dashboard/screens/saved_addresses_screen.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
|
||||
class SavedAddressesScreen extends StatefulWidget {
|
||||
const SavedAddressesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SavedAddressesScreen> createState() => _SavedAddressesScreenState();
|
||||
}
|
||||
|
||||
class _SavedAddressesScreenState extends State<SavedAddressesScreen> {
|
||||
final List<Map<String, dynamic>> _addresses = [
|
||||
{
|
||||
'title': 'Home',
|
||||
'address': '12th Main, Indiranagar, Bengaluru, Karnataka 560038',
|
||||
'icon': Icons.home_rounded,
|
||||
},
|
||||
{
|
||||
'title': 'Office',
|
||||
'address': 'Sector 4, HSR Layout, Bengaluru, Karnataka 560102',
|
||||
'icon': Icons.business_rounded,
|
||||
},
|
||||
{
|
||||
'title': "Mom's House",
|
||||
'address': 'Jayanagar 4th Block, Bengaluru, Karnataka 560011',
|
||||
'icon': Icons.favorite_rounded,
|
||||
},
|
||||
];
|
||||
|
||||
void _showAddAddressSheet() {
|
||||
final titleCtrl = TextEditingController();
|
||||
final addressCtrl = TextEditingController();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Add New Address', style: AppTheme.headlineSm),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('LABEL (e.g. Gym)', style: AppTheme.caption.copyWith(letterSpacing: 1)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: titleCtrl,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter label...',
|
||||
filled: true,
|
||||
fillColor: AppColors.surfaceContainerLowest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: AppColors.surfaceContainer),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: AppColors.surfaceContainer),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('FULL ADDRESS', style: AppTheme.caption.copyWith(letterSpacing: 1)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: addressCtrl,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter complete address...',
|
||||
filled: true,
|
||||
fillColor: AppColors.surfaceContainerLowest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: AppColors.surfaceContainer),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: AppColors.surfaceContainer),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
PrimaryButton(
|
||||
label: 'Save Address',
|
||||
onPressed: () {
|
||||
if (titleCtrl.text.trim().isNotEmpty && addressCtrl.text.trim().isNotEmpty) {
|
||||
setState(() {
|
||||
_addresses.add({
|
||||
'title': titleCtrl.text.trim(),
|
||||
'address': addressCtrl.text.trim(),
|
||||
'icon': Icons.location_on_rounded, // default icon
|
||||
});
|
||||
});
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
body: AppBackground(
|
||||
child: Column(
|
||||
children: [
|
||||
const ImmersiveHeader(
|
||||
title: 'Saved Addresses',
|
||||
subtitle: 'Manage your pickup & drop-off locations',
|
||||
icon: Icons.location_on_rounded,
|
||||
showBack: true,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(20),
|
||||
itemCount: _addresses.length + 1,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 16),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == _addresses.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: PrimaryButton(
|
||||
label: 'Add New Address',
|
||||
trailingIcon: Icons.add_rounded,
|
||||
onPressed: _showAddAddressSheet,
|
||||
),
|
||||
);
|
||||
}
|
||||
final item = _addresses[i];
|
||||
return _AddressCard(
|
||||
title: item['title'],
|
||||
address: item['address'],
|
||||
icon: item['icon'],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddressCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String address;
|
||||
final IconData icon;
|
||||
|
||||
const _AddressCard({required this.title, required this.address, required this.icon});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(icon, color: AppColors.primary, size: 24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: AppTheme.labelMd),
|
||||
const Icon(Icons.more_horiz_rounded, color: AppColors.onSurfaceVariant, size: 20),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(address, style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
168
lib/features/dashboard/screens/summary_screen.dart
Normal file
168
lib/features/dashboard/screens/summary_screen.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:doormile/shared/state/app_state.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
|
||||
class SummaryScreen extends StatelessWidget {
|
||||
const SummaryScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<AppState>();
|
||||
|
||||
// Dynamically calculate stats from real orders
|
||||
final totalDeliveries = state.orders.length;
|
||||
final totalSpent = state.orders.fold<double>(0, (sum, order) => sum + order.amount);
|
||||
|
||||
return AppBackground(
|
||||
child: Column(
|
||||
children: [
|
||||
const ImmersiveHeader(
|
||||
title: 'Summary',
|
||||
subtitle: 'Your delivery and eco-impact stats',
|
||||
icon: Icons.bar_chart_rounded,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SectionHeader(title: 'Overview', action: 'This Month', onAction: () {}),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_StatCard(
|
||||
title: 'Total Deliveries',
|
||||
value: '$totalDeliveries',
|
||||
icon: Icons.inventory_2_rounded,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_StatCard(
|
||||
title: 'Total Spent',
|
||||
value: '₹${totalSpent.toStringAsFixed(0)}',
|
||||
icon: Icons.payments_rounded,
|
||||
color: AppColors.tertiary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
SectionHeader(title: 'Top Locations', action: 'Manage', onAction: () => Navigator.pushNamed(context, '/addresses')),
|
||||
const SizedBox(height: 12),
|
||||
AppCard(
|
||||
onTap: () => Navigator.pushNamed(context, '/addresses'),
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: Column(
|
||||
children: [
|
||||
_LocationRow(title: 'Home', subtitle: 'Indiranagar, Bengaluru', isFirst: true),
|
||||
const Divider(height: 1, color: AppColors.surfaceContainer),
|
||||
_LocationRow(title: 'Office', subtitle: 'HSR Layout, Bengaluru', isFirst: false),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const _StatCard({required this.title, required this.value, required this.icon, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.surfaceContainerHigh, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(title, style: AppTheme.caption.copyWith(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: AppTheme.headlineMd.copyWith(color: AppColors.onSurface)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class _LocationRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool isFirst;
|
||||
const _LocationRow({required this.title, required this.subtitle, required this.isFirst});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, isFirst ? 16 : 12, 16, isFirst ? 12 : 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainerLowest,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColors.surfaceContainerHigh),
|
||||
),
|
||||
child: const Icon(Icons.business_rounded, size: 20, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle, style: AppTheme.caption),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded, color: AppColors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user