Files
crm_mobile/lib/screens/dashboard_screen.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;
}