Initial commit

This commit is contained in:
2026-06-23 11:29:07 +05:30
commit 401cf600e8
224 changed files with 12165 additions and 0 deletions

View 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.'),
);
}
}

View 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);
}

View 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)),
],
),
);
}
}

View 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)),
],
),
),
],
),
);
}
}

View 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),
],
),
);
}
}