710 lines
21 KiB
Dart
710 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../models/client.dart';
|
|
import '../models/activity.dart';
|
|
import '../models/app_stats.dart';
|
|
import '../theme/app_theme.dart';
|
|
import 'surveys_list_screen.dart';
|
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
final AppStats stats;
|
|
final List<Activity> activities;
|
|
final List<Client> clients;
|
|
final List<dynamic> surveys;
|
|
final List<dynamic> pricing;
|
|
final void Function(int) onNavigate;
|
|
final VoidCallback onDataChanged;
|
|
final bool backendReady;
|
|
final VoidCallback onLogout;
|
|
final bool isActive;
|
|
|
|
const DashboardScreen({
|
|
super.key,
|
|
required this.stats,
|
|
required this.activities,
|
|
required this.clients,
|
|
this.surveys = const [],
|
|
this.pricing = const [],
|
|
required this.onNavigate,
|
|
required this.onDataChanged,
|
|
required this.backendReady,
|
|
required this.onLogout,
|
|
this.isActive = false,
|
|
});
|
|
|
|
@override
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
|
final ScrollController _scrollController = ScrollController();
|
|
|
|
@override
|
|
void didUpdateWidget(covariant DashboardScreen oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.isActive && !oldWidget.isActive) {
|
|
if (_scrollController.hasClients) {
|
|
_scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
controller: _scrollController,
|
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 100),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
WideTopBanner(
|
|
backendReady: widget.backendReady,
|
|
subtitle: 'DASHBOARD',
|
|
onLogout: widget.onLogout,
|
|
),
|
|
const SizedBox(height: 20),
|
|
_WelcomeHeader(),
|
|
const SizedBox(height: 20),
|
|
_BentoGrid(stats: widget.stats),
|
|
const SizedBox(height: 20),
|
|
_QuickActions(onNavigate: widget.onNavigate),
|
|
const SizedBox(height: 20),
|
|
_MarketInsights(surveys: widget.surveys, pricing: widget.pricing, onNavigate: widget.onNavigate, onDataChanged: widget.onDataChanged),
|
|
const SizedBox(height: 20),
|
|
_RecentActivity(
|
|
activities: widget.activities,
|
|
clients: widget.clients,
|
|
onNavigate: widget.onNavigate,
|
|
),
|
|
const SizedBox(height: 20),
|
|
_InsightCard(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _WelcomeHeader extends StatelessWidget {
|
|
static String get _greeting {
|
|
final h = DateTime.now().hour;
|
|
if (h < 12) return 'Good Morning';
|
|
if (h < 17) return 'Good Afternoon';
|
|
if (h < 21) return 'Good Evening';
|
|
return 'Good Night';
|
|
}
|
|
|
|
static String get _subtext {
|
|
final h = DateTime.now().hour;
|
|
if (h < 12) return 'Start your day with a fresh logistics survey.';
|
|
if (h < 17) return 'Keep the momentum log your field visits.';
|
|
if (h < 21) return 'Wrap up the day any pending surveys to log?';
|
|
return 'Late session active your data is synced.';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_greeting,
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.w800,
|
|
color: AppColors.darkBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_subtext,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BentoGrid extends StatelessWidget {
|
|
final AppStats stats;
|
|
const _BentoGrid({required this.stats});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
// Large card spanning full width
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFD3E4FE),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFFA6C8FF).withValues(alpha: 0.3)),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'TOTAL CLIENTS',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.5,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
stats.totalClients.toString().replaceAllMapped(
|
|
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
|
(m) => '${m[1]},',
|
|
),
|
|
style: const TextStyle(
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.w900,
|
|
color: AppColors.darkBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.arrow_upward,
|
|
size: 14, color: AppColors.greenDark),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'+12% from last month',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.greenDark,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
Positioned(
|
|
right: -20,
|
|
bottom: -20,
|
|
child: Container(
|
|
width: 100,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: AppColors.primary.withValues(alpha: 0.06),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _SmallStatCard(
|
|
icon: Icons.calendar_today,
|
|
iconColor: AppColors.primary,
|
|
value: stats.todayEntries.toString(),
|
|
label: "Today's Entries",
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _SmallStatCard(
|
|
icon: Icons.access_time,
|
|
iconColor: const Color(0xFF004D47),
|
|
value: stats.pendingFollowups.toString(),
|
|
label: 'Pending Follow-ups',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SmallStatCard extends StatelessWidget {
|
|
final IconData icon;
|
|
final Color iconColor;
|
|
final String value;
|
|
final String label;
|
|
|
|
const _SmallStatCard({
|
|
required this.icon,
|
|
required this.iconColor,
|
|
required this.value,
|
|
required this.label,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: AppColors.border.withValues(alpha: 0.35)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 24, color: iconColor),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.darkBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _QuickActions extends StatelessWidget {
|
|
final void Function(int) onNavigate;
|
|
const _QuickActions({required this.onNavigate});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'QUICK ACTIONS',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.2,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 48,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => onNavigate(4),
|
|
icon: const Icon(Icons.add, size: 18, color: Colors.white),
|
|
label: const Text(
|
|
'Quick Add Client',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primary,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
elevation: 1,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 48,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => onNavigate(1),
|
|
icon: Icon(Icons.search, size: 18, color: AppColors.darkBlue),
|
|
label: Text(
|
|
'Search Client Database',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.darkBlue,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFD3E4FE),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
side: const BorderSide(color: Color(0xFFA6C8FF)),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MarketInsights extends StatelessWidget {
|
|
final List<dynamic> surveys;
|
|
final List<dynamic> pricing;
|
|
final void Function(int) onNavigate;
|
|
final VoidCallback onDataChanged;
|
|
|
|
const _MarketInsights({required this.surveys, required this.pricing, required this.onNavigate, required this.onDataChanged});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'MARKET DATA',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.2,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
Navigator.push(context, MaterialPageRoute(
|
|
builder: (ctx) => SurveysListScreen(
|
|
surveys: surveys,
|
|
onDataChanged: onDataChanged,
|
|
),
|
|
));
|
|
},
|
|
child: _SmallStatCard(
|
|
icon: Icons.storefront_outlined,
|
|
iconColor: const Color(0xFFE87C00),
|
|
value: surveys.length.toString(),
|
|
label: 'Competitor Surveys',
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => onNavigate(3),
|
|
child: _SmallStatCard(
|
|
icon: Icons.price_change_outlined,
|
|
iconColor: const Color(0xFF15803D),
|
|
value: pricing.length.toString(),
|
|
label: 'Carrier Pricing',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RecentActivity extends StatelessWidget {
|
|
final List<Activity> activities;
|
|
final List<Client> clients;
|
|
final void Function(int) onNavigate;
|
|
|
|
const _RecentActivity({
|
|
required this.activities,
|
|
required this.clients,
|
|
required this.onNavigate,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'RECENT ACTIVITY',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.2,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: () => onNavigate(1),
|
|
child: Text(
|
|
'View all',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: AppColors.border.withValues(alpha: 0.2)),
|
|
),
|
|
child: Column(
|
|
children: activities
|
|
.take(5)
|
|
.map((activity) => _ActivityTile(
|
|
activity: activity,
|
|
onTap: () => onNavigate(1),
|
|
isLast: activity == activities.last,
|
|
))
|
|
.toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ActivityTile extends StatelessWidget {
|
|
final Activity activity;
|
|
final VoidCallback onTap;
|
|
final bool isLast;
|
|
|
|
const _ActivityTile({
|
|
required this.activity,
|
|
required this.onTap,
|
|
required this.isLast,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Color iconBg;
|
|
Color iconColor;
|
|
IconData iconData;
|
|
|
|
switch (activity.icon) {
|
|
case ActivityIcon.shipping:
|
|
iconBg = const Color(0xFFD5E0F8);
|
|
iconColor = const Color(0xFF1E3A8A);
|
|
iconData = Icons.local_shipping;
|
|
break;
|
|
case ActivityIcon.call:
|
|
iconBg = const Color(0xFFFFE4E6);
|
|
iconColor = AppColors.primary;
|
|
iconData = Icons.phone;
|
|
break;
|
|
case ActivityIcon.edit:
|
|
iconBg = const Color(0xFFDCFCE7);
|
|
iconColor = const Color(0xFF15803D);
|
|
iconData = Icons.edit;
|
|
break;
|
|
}
|
|
|
|
Widget? badge;
|
|
if (activity.status == ActivityStatus.newStatus) {
|
|
badge = _Badge(label: 'NEW', bg: const Color(0xFFFFDAD7), fg: const Color(0xFF930013));
|
|
} else if (activity.status == ActivityStatus.pending) {
|
|
badge = _Badge(label: 'PENDING', bg: const Color(0xFFD8E3FB), fg: const Color(0xFF3C475A));
|
|
} else if (activity.status == ActivityStatus.completed) {
|
|
badge = _Badge(label: 'DONE', bg: const Color(0xFFD8F9EE), fg: const Color(0xFF115E59));
|
|
}
|
|
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: isLast
|
|
? const BorderRadius.only(
|
|
bottomLeft: Radius.circular(16),
|
|
bottomRight: Radius.circular(16),
|
|
)
|
|
: null,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
border: isLast
|
|
? null
|
|
: Border(
|
|
bottom: BorderSide(
|
|
color: AppColors.border.withValues(alpha: 0.15),
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: iconBg,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(iconData, size: 18, color: iconColor),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
activity.clientName,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.darkBlue,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
activity.action,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
?badge,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Badge extends StatelessWidget {
|
|
final String label;
|
|
final Color bg;
|
|
final Color fg;
|
|
|
|
const _Badge({required this.label, required this.bg, required this.fg});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: bg,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w700,
|
|
color: fg,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _InsightCard extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 160,
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Color(0xFF0B1C30), Color(0xFF8D0012)],
|
|
),
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: CustomPaint(painter: _GridPainter()),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.auto_awesome,
|
|
size: 12, color: const Color(0xFFFFDAD7)),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'INSIGHT OF THE WEEK',
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 1.2,
|
|
color: const Color(0xFFFFDAD7),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
const Text(
|
|
'Weekly Performance Report',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
"Analyze your team's survey conversion rates.",
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
fontWeight: FontWeight.w300,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _GridPainter extends CustomPainter {
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = Colors.white.withValues(alpha: 0.04)
|
|
..strokeWidth = 1;
|
|
const spacing = 30.0;
|
|
for (double x = 0; x < size.width; x += spacing) {
|
|
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
|
|
}
|
|
for (double y = 0; y < size.height; y += spacing) {
|
|
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_GridPainter oldDelegate) => false;
|
|
}
|