import 'dart:convert'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import '../../constant/links.dart'; import '../../print.dart'; import '../functions/crud.dart'; // ══════════════════════════════════════════════════════════════ // MODEL: Represents one employee's full data for a period // ══════════════════════════════════════════════════════════════ class EmployeeChartData { final String name; final Color color; final List notesSpots; final List callsSpots; const EmployeeChartData({ required this.name, required this.color, required this.notesSpots, required this.callsSpots, }); int get totalNotes => notesSpots.fold(0, (sum, s) => sum + s.y.toInt()); int get totalCalls => callsSpots.fold(0, (sum, s) => sum + s.y.toInt()); EmployeeChartData copyWith({ List? notesSpots, List? callsSpots, }) { return EmployeeChartData( name: name, color: color, notesSpots: notesSpots ?? this.notesSpots, callsSpots: callsSpots ?? this.callsSpots, ); } } // ══════════════════════════════════════════════════════════════ // MODEL: Employment activation stats per employee // ══════════════════════════════════════════════════════════════ class EmploymentStat { final String name; final int count; final Color color; const EmploymentStat({ required this.name, required this.count, required this.color, }); } // ══════════════════════════════════════════════════════════════ // CONTROLLER // ══════════════════════════════════════════════════════════════ class StaticController extends GetxController { // ─── Color Palette for Dynamic Employees ─────────────────── static const List _employeeColors = [ Color(0xFF00D4AA), // teal Color(0xFF82AAFF), // blue Color(0xFFFFCB6B), // amber Color(0xFFC792EA), // purple Color(0xFFFF5370), // red Color(0xFFC3E88D), // green Color(0xFFF07178), // coral Color(0xFF89DDFF), // cyan ]; Color _colorForIndex(int i) => _employeeColors[i % _employeeColors.length]; // ─── Date & State ─────────────────────────────────────────── DateTime? startDate = DateTime(DateTime.now().year, DateTime.now().month, 1); DateTime? endDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0); DateTime? compareStartDate; DateTime? compareEndDate; bool isComparing = false; bool isLoading = false; // ─── Daily Notes State ───────────────────────────────────── bool isLoadingNotes = false; List dailyNotesList = []; // ─── Main Chart Data ─────────────────────────────────────── List chartDataPassengers = []; List chartDataDrivers = []; List chartDataRides = []; List chartDataDriversMatchingNotes = []; List chartDataPassengersCompare = []; List chartDataDriversCompare = []; List chartDataRidesCompare = []; List chartDataDriversMatchingNotesCompare = []; // ─── 🔥 DYNAMIC Employee Data ───────────────────────────── // Key = employee name (from server), Value = their chart data Map employeeData = {}; Map employeeDataCompare = {}; // Set of all known employee names (union of current + compare) Set get allEmployeeNames => { ...employeeData.keys, ...employeeDataCompare.keys, }; // ─── Employment Stats ────────────────────────────────────── List employmentStatsList = []; // ─── Totals ──────────────────────────────────────────────── String totalMonthlyPassengers = '0'; String totalMonthlyRides = '0'; String totalMonthlyDrivers = '0'; // ─── Raw Lists ───────────────────────────────────────────── List staticList = []; // ─── Color Registry (stable across rebuilds) ─────────────── final Map _employeeColorRegistry = {}; Color _getOrAssignColor(String name) { if (!_employeeColorRegistry.containsKey(name)) { _employeeColorRegistry[name] = _colorForIndex(_employeeColorRegistry.length); } return _employeeColorRegistry[name]!; } @override void onInit() { super.onInit(); getAll(); } // ─── Helpers ─────────────────────────────────────────────── double get daysInPeriod { if (startDate == null || endDate == null) return 31; return endDate!.difference(startDate!).inDays + 1.0; } String get currentDateString { if (startDate == null || endDate == null) return ""; return "${DateFormat('yyyy-MM-dd').format(startDate!)} : " "${DateFormat('yyyy-MM-dd').format(endDate!)}"; } String get compareDateString { if (compareStartDate == null || compareEndDate == null) return ""; return "${DateFormat('yyyy-MM-dd').format(compareStartDate!)} : " "${DateFormat('yyyy-MM-dd').format(compareEndDate!)}"; } // ─── Date Actions ────────────────────────────────────────── void updateDateRange(DateTime start, DateTime end) { startDate = start; endDate = end; if (isComparing) _calculateCompareDates(); getAll(); update(); } void _calculateCompareDates() { if (startDate == null || endDate == null) return; Duration duration = endDate!.difference(startDate!); compareEndDate = startDate!.subtract(const Duration(days: 1)); compareStartDate = compareEndDate!.subtract(duration); } Future toggleComparison() async { isComparing = !isComparing; if (isComparing) { _calculateCompareDates(); } else { compareStartDate = null; compareEndDate = null; _clearComparisonData(); } await getAll(); } void _clearComparisonData() { chartDataPassengersCompare.clear(); chartDataDriversCompare.clear(); chartDataRidesCompare.clear(); chartDataDriversMatchingNotesCompare.clear(); employeeDataCompare.clear(); } Map _getPayload(DateTime start, DateTime end) => { "start_date": DateFormat('yyyy-MM-dd').format(start), "end_date": DateFormat('yyyy-MM-dd').format(end), "month": start.month.toString(), "year": start.year.toString(), }; // ─── Main Fetch ──────────────────────────────────────────── Future getAll() async { if (startDate == null || endDate == null) return; isLoading = true; update(); await Future.wait([ fetchPassengers(isCompare: false), fetchRides(isCompare: false), fetchDrivers(isCompare: false), fetchEmployeeDynamic(isCompare: false), fetchEditorCallsDynamic(isCompare: false), fetchEmploymentStats(), ]); if (isComparing && compareStartDate != null && compareEndDate != null) { await Future.wait([ fetchPassengers(isCompare: true), fetchRides(isCompare: true), fetchDrivers(isCompare: true), fetchEmployeeDynamic(isCompare: true), fetchEditorCallsDynamic(isCompare: true), ]); } isLoading = false; update(); } // ─── Spot Generator ─────────────────────────────────────── List _generateSpots( List data, String dateKey, String valueKey, DateTime startOfRange, DateTime endOfRange, ) { Map dataMap = { for (var item in data) item[dateKey].toString(): double.tryParse(item[valueKey].toString()) ?? 0.0 }; int totalDays = endOfRange.difference(startOfRange).inDays + 1; return List.generate(totalDays, (i) { final date = startOfRange.add(Duration(days: i)); final key = DateFormat('yyyy-MM-dd').format(date); return FlSpot((i + 1).toDouble(), dataMap[key] ?? 0.0); }); } /// Generates spots map keyed by employee name from a date→name→value structure Map> _generateEmployeeSpots( Map> dateNameMap, DateTime start, DateTime end, ) { // Discover all employee names dynamically final Set names = {}; for (var dayData in dateNameMap.values) { names.addAll(dayData.keys); } int totalDays = end.difference(start).inDays + 1; final Map> result = {}; for (final name in names) { result[name] = List.generate(totalDays, (i) { final date = start.add(Duration(days: i)); final dateStr = DateFormat('yyyy-MM-dd').format(date); final value = dateNameMap[dateStr]?[name] ?? 0.0; return FlSpot((i + 1).toDouble(), value); }); } return result; } /// Parses a list of {date/day, NAME, count} records into a dateNameMap Map> _parseDateNameMap(List jsonData) { final Map> result = {}; for (var item in jsonData) { final dateStr = (item['date'] ?? item['day']).toString(); final name = item['NAME'].toString().toLowerCase().trim(); final count = double.tryParse(item['count'].toString()) ?? 0.0; result.putIfAbsent(dateStr, () => {})[name] = (result[dateStr]?[name] ?? 0) + count; } return result; } // ─── Passengers ─────────────────────────────────────────── Future fetchPassengers({bool isCompare = false}) async { final start = isCompare ? compareStartDate! : startDate!; final end = isCompare ? compareEndDate! : endDate!; final res = await CRUD().get( link: AppLink.getPassengersStatic, payload: _getPayload(start, end)); final json = jsonDecode(res); if (json['status'] == 'failure') return; final List data = json['message']; if (!isCompare && data.isNotEmpty && data[0]['totalMonthly'] != null) { totalMonthlyPassengers = data[0]['totalMonthly'].toString(); } final spots = _generateSpots(data, 'day', 'totalPassengers', start, end); if (isCompare) chartDataPassengersCompare = spots; else chartDataPassengers = spots; } // ─── Rides ──────────────────────────────────────────────── Future fetchRides({bool isCompare = false}) async { final start = isCompare ? compareStartDate! : startDate!; final end = isCompare ? compareEndDate! : endDate!; final res = await CRUD() .get(link: AppLink.getRidesStatic, payload: _getPayload(start, end)); final json = jsonDecode(res); if (json['status'] == 'failure') return; final List data = json['message']; if (!isCompare && data.isNotEmpty && data[0]['totalMonthly'] != null) { totalMonthlyRides = data[0]['totalMonthly'].toString(); } final spots = _generateSpots(data, 'day', 'totalRides', start, end); if (isCompare) chartDataRidesCompare = spots; else chartDataRides = spots; } // ─── Drivers ────────────────────────────────────────────── Future fetchDrivers({bool isCompare = false}) async { final start = isCompare ? compareStartDate! : startDate!; final end = isCompare ? compareEndDate! : endDate!; final res = await CRUD().get( link: AppLink.getdriverstotalMonthly, payload: _getPayload(start, end)); final json = jsonDecode(res); if (json['status'] == 'failure') return; final List data = json['message']; if (!isCompare && data.isNotEmpty && data[0]['totalMonthlyDrivers'] != null) { totalMonthlyDrivers = data[0]['totalMonthlyDrivers'].toString(); staticList = data; } final spotsDrivers = _generateSpots(data, 'day', 'dailyTotalDrivers', start, end); final spotsNotes = _generateSpots(data, 'day', 'dailyMatchingNotes', start, end); if (isCompare) { chartDataDriversCompare = spotsDrivers; chartDataDriversMatchingNotesCompare = spotsNotes; } else { chartDataDrivers = spotsDrivers; chartDataDriversMatchingNotes = spotsNotes; } } // ─── 🔥 DYNAMIC: Employee Notes ─────────────────────────── Future fetchEmployeeDynamic({bool isCompare = false}) async { try { final start = isCompare ? compareStartDate! : startDate!; final end = isCompare ? compareEndDate! : endDate!; final res = await CRUD().get( link: AppLink.getEmployeeStatic, payload: _getPayload(start, end)); if (res == 'failure') return; final json = jsonDecode(res) as Map; if (json['status'] == 'failure') return; final List data = json['message']; if (data.isEmpty) return; final dateNameMap = _parseDateNameMap(data); final spotsMap = _generateEmployeeSpots(dateNameMap, start, end); // Merge into employee data map final target = isCompare ? employeeDataCompare : employeeData; spotsMap.forEach((name, spots) { final color = _getOrAssignColor(name); if (target.containsKey(name)) { target[name] = target[name]!.copyWith(notesSpots: spots); } else { target[name] = EmployeeChartData( name: name, color: color, notesSpots: spots, callsSpots: [], ); } }); } catch (e) { Log.print('Error in fetchEmployeeDynamic: $e'); } } // ─── 🔥 DYNAMIC: Employee Calls ─────────────────────────── Future fetchEditorCallsDynamic({bool isCompare = false}) async { try { final start = isCompare ? compareStartDate! : startDate!; final end = isCompare ? compareEndDate! : endDate!; final res = await CRUD().get( link: AppLink.getEditorStatsCalls, payload: _getPayload(start, end)); if (res == 'failure') return; final json = jsonDecode(res) as Map; if (json['status'] == 'failure') return; final List data = json['message']; if (data.isEmpty) return; final dateNameMap = _parseDateNameMap(data); final spotsMap = _generateEmployeeSpots(dateNameMap, start, end); final target = isCompare ? employeeDataCompare : employeeData; spotsMap.forEach((name, spots) { final color = _getOrAssignColor(name); if (target.containsKey(name)) { target[name] = target[name]!.copyWith(callsSpots: spots); } else { target[name] = EmployeeChartData( name: name, color: color, notesSpots: [], callsSpots: spots, ); } }); } catch (e) { Log.print('Error in fetchEditorCallsDynamic: $e'); } } // ─── Employment Stats ───────────────────────────────────── Future fetchEmploymentStats() async { try { final res = await CRUD().get( link: AppLink.getEmployeeDriverAfterCallingRegister, payload: _getPayload(startDate!, endDate!)); if (res == 'failure') return; final json = jsonDecode(res); if (json['status'] != 'success') return; final List data = json['message']?['data'] ?? []; // Aggregate by name (dynamic — no hardcoded allowed list) final Map aggregated = {}; for (var item in data) { final name = item['employmentType'].toString().toLowerCase().trim(); final count = int.tryParse(item['count'].toString()) ?? 0; aggregated[name] = (aggregated[name] ?? 0) + count; } employmentStatsList = aggregated.entries.map((e) { return EmploymentStat( name: e.key, count: e.value, color: _getOrAssignColor(e.key), ); }).toList() ..sort((a, b) => b.count.compareTo(a.count)); // sort descending } catch (e) { Log.print("Error fetchEmploymentStats: $e"); } } // ─── Daily Notes ────────────────────────────────────────── Future fetchDailyNotes(DateTime date) async { try { isLoadingNotes = true; dailyNotesList.clear(); update(); final res = await CRUD().post( link: AppLink.getNotesForEmployee, payload: {"date": DateFormat('yyyy-MM-dd').format(date)}); if (res != 'failure') { final json = res; if (json['status'] == 'success') { dailyNotesList = json['message']; } } } catch (e) { Log.print("Error fetchDailyNotes: $e"); } finally { isLoadingNotes = false; update(); } } // ─── Computed Summaries for UI ──────────────────────────── /// Returns sorted list of employees by total notes descending List get employeesSortedByNotes { final list = employeeData.values.toList(); list.sort((a, b) => b.totalNotes.compareTo(a.totalNotes)); return list; } /// Returns sorted list of employees by total calls descending List get employeesSortedByCalls { final list = employeeData.values.toList(); list.sort((a, b) => b.totalCalls.compareTo(a.totalCalls)); return list; } /// Grand total notes across all employees int get grandTotalNotes => employeeData.values.fold(0, (s, e) => s + e.totalNotes); /// Grand total calls across all employees int get grandTotalCalls => employeeData.values.fold(0, (s, e) => s + e.totalCalls); }