Initial commit of Doormile CRM Mobile with UI modernizations

This commit is contained in:
2026-06-26 13:43:37 +05:30
commit 3965046984
183 changed files with 18135 additions and 0 deletions

291
lib/app.dart Normal file
View File

@@ -0,0 +1,291 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'models/client.dart';
import 'models/activity.dart';
import 'models/app_stats.dart';
import 'screens/dashboard_screen.dart';
import 'screens/clients_screen.dart';
import 'screens/survey_screen.dart';
import 'screens/surveys_list_screen.dart';
import 'screens/pricing_screen.dart';
import 'services/crm_api_service.dart';
import 'services/auth_service.dart';
class AppShell extends StatefulWidget {
final AuthUser currentUser;
final VoidCallback onLogout;
const AppShell({
super.key,
required this.currentUser,
required this.onLogout,
});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
int _currentIndex = 0;
bool _backendReady = false;
List<Client> _clients = [];
List<Activity> _activities = [];
List<dynamic> _surveys = [];
List<dynamic> _pricing = [];
AppStats _stats = const AppStats(
totalClients: 0,
avgVolume: 0,
revenueOpportunity: 0,
todayEntries: 0,
pendingFollowups: 0,
);
final _api = CrmApiService();
Timer? _pollingTimer;
@override
void initState() {
super.initState();
_api.setToken(widget.currentUser.token);
_loadFromBackend();
_startPolling();
}
void _startPolling() {
_pollingTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_loadFromBackend(silent: true);
});
}
@override
void dispose() {
_pollingTimer?.cancel();
super.dispose();
}
Future<void> _loadFromBackend({bool silent = false}) async {
try {
final results = await Future.wait([
_api.loadClients(),
_api.fetchSurveys(),
_api.fetchPricing(),
]);
final clients = results[0] as List<Client>;
final surveys = results[1];
final pricing = results[2];
if (!mounted) return;
setState(() {
_clients = clients;
_surveys = surveys;
_pricing = pricing;
_stats = _computeStats(clients);
_backendReady = true;
});
} catch (_) {
if (mounted && !silent) setState(() => _backendReady = false);
}
}
AppStats _computeStats(List<Client> clients) {
if (clients.isEmpty) {
return const AppStats(
totalClients: 0,
avgVolume: 0,
revenueOpportunity: 0,
todayEntries: 0,
pendingFollowups: 0,
);
}
final totalVolume = clients.fold(0.0, (acc, c) => acc + c.parcelVolume);
final avg = (totalVolume / clients.length).round();
return AppStats(
totalClients: clients.length,
avgVolume: avg,
revenueOpportunity: double.parse((totalVolume * 0.01).toStringAsFixed(2)),
todayEntries: 0,
pendingFollowups: clients.where((c) => c.dataConsent == DataConsent.basicOnly).length,
);
}
void _handleAddClient(Client incoming) async {
final newClient = incoming.copyWith(
recordedByName: widget.currentUser.name,
recordedByEmail: widget.currentUser.email,
recordedByRole: widget.currentUser.role,
);
// Optimistic UI update with a temp ID
setState(() {
_clients = [newClient, ..._clients];
_activities = [
Activity(
id: 'act_${DateTime.now().millisecondsSinceEpoch}',
clientName: newClient.name,
action: 'Survey submitted just now',
icon: ActivityIcon.shipping,
status: ActivityStatus.newStatus,
),
..._activities,
];
_stats = _computeStats(_clients);
});
// Persist to backend and swap temp ID with server-assigned ID
final saved = await _api.createClient(newClient);
if (saved != null && mounted) {
setState(() {
_clients = _clients
.map((c) => c.id == newClient.id ? saved : c)
.toList();
});
}
}
void _handleUpdateNotes(String clientId, String newNotes) async {
setState(() {
_clients = _clients.map((c) {
if (c.id == clientId) {
return c.copyWith(notes: newNotes, lastUpdated: 'Updated just now');
}
return c;
}).toList();
final updated = _clients.firstWhere((c) => c.id == clientId);
_activities = [
Activity(
id: 'act_${DateTime.now().millisecondsSinceEpoch}',
clientName: updated.name,
action: 'Profile updated by Rep',
icon: ActivityIcon.edit,
),
..._activities,
];
});
final updated = _clients.firstWhere((c) => c.id == clientId);
await _api.updateClient(updated);
}
void _handleCompleteClientProfile(
String clientId,
double parcelVolume,
int activeContracts,
String provider,
String notes,
) async {
String efficiency = 'High Efficiency';
if (provider == 'Blue Dart Express') efficiency = 'Premium Service';
if (provider == 'Gati-KWE') efficiency = 'Cost Leader';
if (provider == 'Delhivery') efficiency = 'Local Last Mile';
setState(() {
_clients = _clients.map((c) {
if (c.id != clientId) return c;
return c.copyWith(
parcelVolume: parcelVolume,
activeContracts: activeContracts,
provider: provider,
efficiency: efficiency,
notes: notes,
dataConsent: DataConsent.full,
lastUpdated: 'Just now',
surveySubmitted: true,
recordedByName: widget.currentUser.name,
recordedByEmail: widget.currentUser.email,
recordedByRole: widget.currentUser.role,
);
}).toList();
_stats = _computeStats(_clients);
});
final updated = _clients.firstWhere((c) => c.id == clientId);
await _api.updateClient(updated);
}
void _navigateTo(int index) {
if (index == 4) {
Navigator.push(context, MaterialPageRoute(
builder: (ctx) => Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.black87),
title: const Text('Add Client', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)),
),
body: SurveyScreen(
clients: _clients,
onAddClient: _handleAddClient,
onNavigate: (i) {
Navigator.pop(ctx);
_navigateTo(i);
},
backendReady: _backendReady,
isActive: true,
),
),
));
return;
}
setState(() => _currentIndex = index);
}
@override
Widget build(BuildContext context) {
final screens = [
DashboardScreen(
stats: _stats,
activities: _activities,
clients: _clients,
surveys: _surveys,
pricing: _pricing,
onNavigate: _navigateTo,
onDataChanged: _loadFromBackend,
backendReady: _backendReady,
onLogout: widget.onLogout,
isActive: _currentIndex == 0,
),
ClientsScreen(
clients: _clients,
onUpdateClientNotes: _handleUpdateNotes,
onCompleteClientProfile: _handleCompleteClientProfile,
onNavigate: _navigateTo,
backendReady: _backendReady,
isActive: _currentIndex == 1,
),
SurveysListScreen(
surveys: _surveys,
onDataChanged: _loadFromBackend,
isActive: _currentIndex == 2,
),
PricingScreen(
pricing: _pricing,
backendReady: _backendReady,
onDataChanged: _loadFromBackend,
isActive: _currentIndex == 3,
),
];
return Scaffold(
body: SafeArea(
child: IndexedStack(
index: _currentIndex,
children: screens,
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: _navigateTo,
height: 68,
destinations: const [
NavigationDestination(icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), label: 'Home'),
NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Clients'),
NavigationDestination(icon: Icon(Icons.storefront_outlined), selectedIcon: Icon(Icons.storefront), label: 'Surveys'),
NavigationDestination(icon: Icon(Icons.local_shipping_outlined), selectedIcon: Icon(Icons.local_shipping), label: 'Pricing'),
],
),
);
}
}