first app design
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ErrorView extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const ErrorView({super.key, required this.message, this.onRetry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFFE02424)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF6B7280)),
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: onRetry, child: const Text('Retry')),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class LoadingList extends StatelessWidget {
|
||||
final int count;
|
||||
final double itemHeight;
|
||||
|
||||
const LoadingList({super.key, this.count = 5, this.itemHeight = 100});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: const Color(0xFFE5E7EB),
|
||||
highlightColor: const Color(0xFFF9FAFB),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: count,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
||||
itemBuilder: (_, _) => Container(
|
||||
height: itemHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/providers/locale_provider.dart';
|
||||
import '../core/providers/theme_provider.dart';
|
||||
|
||||
const _orange = Color(0xFFFF6B00);
|
||||
|
||||
/// Compact row of language pills + theme toggle.
|
||||
/// [onDark] = true when rendered on a dark background (landing screen).
|
||||
class PreferencesBar extends ConsumerWidget {
|
||||
final bool onDark;
|
||||
|
||||
const PreferencesBar({super.key, this.onDark = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentLocale = ref.watch(localeProvider);
|
||||
final themeMode = ref.watch(themeProvider);
|
||||
final isDark = themeMode == ThemeMode.dark;
|
||||
|
||||
final inactiveText = onDark
|
||||
? Colors.white.withValues(alpha: 0.55)
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
final activeText = Colors.white;
|
||||
final inactiveBg = onDark
|
||||
? Colors.white.withValues(alpha: 0.12)
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest;
|
||||
|
||||
const langs = [
|
||||
('EN', Locale('en')),
|
||||
('FR', Locale('fr')),
|
||||
('AR', Locale('ar')),
|
||||
];
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Language pills
|
||||
...langs.map((item) {
|
||||
final (code, locale) = item;
|
||||
final selected =
|
||||
currentLocale.languageCode == locale.languageCode;
|
||||
return GestureDetector(
|
||||
onTap: () => ref.read(localeProvider.notifier).setLocale(locale),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 11, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? _orange : inactiveBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
code,
|
||||
style: TextStyle(
|
||||
color: selected ? activeText : inactiveText,
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
selected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(width: 2),
|
||||
|
||||
// Theme toggle icon
|
||||
GestureDetector(
|
||||
onTap: () => ref.read(themeProvider.notifier).toggle(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: inactiveBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
isDark ? Icons.light_mode_outlined : Icons.dark_mode_outlined,
|
||||
size: 15,
|
||||
color: onDark
|
||||
? Colors.white.withValues(alpha: 0.8)
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../core/models/reservation.dart';
|
||||
import 'status_badge.dart';
|
||||
|
||||
class ReservationCard extends StatelessWidget {
|
||||
final Reservation reservation;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ReservationCard({super.key, required this.reservation, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fmt = DateFormat('MMM d, yyyy');
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
reservation.vehicle?.displayName ?? 'Reservation',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
StatusBadge(status: reservation.status),
|
||||
],
|
||||
),
|
||||
if (reservation.customer != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
reservation.customer!.fullName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: Color(0xFF6B7280)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today,
|
||||
size: 14, color: Color(0xFF9CA3AF)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${fmt.format(reservation.startDate)} → ${fmt.format(reservation.endDate)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: Color(0xFF6B7280)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.attach_money,
|
||||
size: 14, color: Color(0xFF9CA3AF)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'\$${reservation.totalAmount.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF111928),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatusBadge(status: reservation.paymentStatus),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String? subtitle;
|
||||
|
||||
const StatCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF111928),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF9CA3AF),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatusBadge extends StatelessWidget {
|
||||
final String status;
|
||||
|
||||
const StatusBadge({super.key, required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (color, bg, label) = _style(status);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static (Color, Color, String) _style(String status) {
|
||||
switch (status.toUpperCase()) {
|
||||
case 'AVAILABLE':
|
||||
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Available');
|
||||
case 'RENTED':
|
||||
return (const Color(0xFF1A56DB), const Color(0xFFE1EFFE), 'Rented');
|
||||
case 'MAINTENANCE':
|
||||
return (const Color(0xFFB45309), const Color(0xFFFDF6B2), 'Maintenance');
|
||||
case 'OUT_OF_SERVICE':
|
||||
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Out of Service');
|
||||
case 'CONFIRMED':
|
||||
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Confirmed');
|
||||
case 'ACTIVE':
|
||||
return (const Color(0xFF1A56DB), const Color(0xFFE1EFFE), 'Active');
|
||||
case 'COMPLETED':
|
||||
return (const Color(0xFF5521B5), const Color(0xFFEDEBFE), 'Completed');
|
||||
case 'CANCELLED':
|
||||
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Cancelled');
|
||||
case 'DRAFT':
|
||||
return (const Color(0xFF374151), const Color(0xFFF3F4F6), 'Draft');
|
||||
case 'NO_SHOW':
|
||||
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'No Show');
|
||||
case 'PENDING':
|
||||
return (const Color(0xFFB45309), const Color(0xFFFDF6B2), 'Pending');
|
||||
case 'SUCCEEDED':
|
||||
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Paid');
|
||||
case 'FAILED':
|
||||
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Failed');
|
||||
case 'APPROVED':
|
||||
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Approved');
|
||||
case 'DENIED':
|
||||
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Denied');
|
||||
case 'VALID':
|
||||
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Valid');
|
||||
case 'EXPIRED':
|
||||
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Expired');
|
||||
default:
|
||||
return (const Color(0xFF374151), const Color(0xFFF3F4F6), status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../core/models/vehicle.dart';
|
||||
import '../core/constants/app_constants.dart';
|
||||
import 'status_badge.dart';
|
||||
|
||||
class VehicleCard extends StatelessWidget {
|
||||
final Vehicle vehicle;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const VehicleCard({super.key, required this.vehicle, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildImage(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
vehicle.displayName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
StatusBadge(status: vehicle.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
vehicle.licensePlate,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.attach_money,
|
||||
size: 16, color: Color(0xFF1A56DB)),
|
||||
Text(
|
||||
'${vehicle.dailyRate.toStringAsFixed(0)}/day',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A56DB),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3F4F6),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
vehicle.category,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Color(0xFF6B7280)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImage() {
|
||||
final photo = vehicle.primaryPhoto;
|
||||
if (photo == null) {
|
||||
return Container(
|
||||
height: 150,
|
||||
color: const Color(0xFFF3F4F6),
|
||||
child: const Center(
|
||||
child: Icon(Icons.directions_car, size: 48, color: Color(0xFFD1D5DB)),
|
||||
),
|
||||
);
|
||||
}
|
||||
final url = photo.startsWith('http')
|
||||
? photo
|
||||
: '${AppConstants.baseUrl.replaceAll('/api/v1', '')}$photo';
|
||||
return CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, _) => Container(
|
||||
height: 150,
|
||||
color: const Color(0xFFF3F4F6),
|
||||
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
),
|
||||
errorWidget: (_, _, _) => Container(
|
||||
height: 150,
|
||||
color: const Color(0xFFF3F4F6),
|
||||
child: const Center(
|
||||
child: Icon(Icons.directions_car, size: 48, color: Color(0xFFD1D5DB)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user