first commit
This commit is contained in:
512
siro_admin/lib/controller/admin/static_controller.dart
Normal file
512
siro_admin/lib/controller/admin/static_controller.dart
Normal file
@@ -0,0 +1,512 @@
|
||||
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<FlSpot> notesSpots;
|
||||
final List<FlSpot> 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<FlSpot>? notesSpots,
|
||||
List<FlSpot>? 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<Color> _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<dynamic> dailyNotesList = [];
|
||||
|
||||
// ─── Main Chart Data ───────────────────────────────────────
|
||||
List<FlSpot> chartDataPassengers = [];
|
||||
List<FlSpot> chartDataDrivers = [];
|
||||
List<FlSpot> chartDataRides = [];
|
||||
List<FlSpot> chartDataDriversMatchingNotes = [];
|
||||
|
||||
List<FlSpot> chartDataPassengersCompare = [];
|
||||
List<FlSpot> chartDataDriversCompare = [];
|
||||
List<FlSpot> chartDataRidesCompare = [];
|
||||
List<FlSpot> chartDataDriversMatchingNotesCompare = [];
|
||||
|
||||
// ─── 🔥 DYNAMIC Employee Data ─────────────────────────────
|
||||
// Key = employee name (from server), Value = their chart data
|
||||
Map<String, EmployeeChartData> employeeData = {};
|
||||
Map<String, EmployeeChartData> employeeDataCompare = {};
|
||||
|
||||
// Set of all known employee names (union of current + compare)
|
||||
Set<String> get allEmployeeNames => {
|
||||
...employeeData.keys,
|
||||
...employeeDataCompare.keys,
|
||||
};
|
||||
|
||||
// ─── Employment Stats ──────────────────────────────────────
|
||||
List<EmploymentStat> employmentStatsList = [];
|
||||
|
||||
// ─── Totals ────────────────────────────────────────────────
|
||||
String totalMonthlyPassengers = '0';
|
||||
String totalMonthlyRides = '0';
|
||||
String totalMonthlyDrivers = '0';
|
||||
|
||||
// ─── Raw Lists ─────────────────────────────────────────────
|
||||
List staticList = [];
|
||||
|
||||
// ─── Color Registry (stable across rebuilds) ───────────────
|
||||
final Map<String, Color> _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<void> 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<String, dynamic> _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<void> 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<FlSpot> _generateSpots(
|
||||
List<dynamic> data,
|
||||
String dateKey,
|
||||
String valueKey,
|
||||
DateTime startOfRange,
|
||||
DateTime endOfRange,
|
||||
) {
|
||||
Map<String, double> 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<String, List<FlSpot>> _generateEmployeeSpots(
|
||||
Map<String, Map<String, double>> dateNameMap,
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
) {
|
||||
// Discover all employee names dynamically
|
||||
final Set<String> names = {};
|
||||
for (var dayData in dateNameMap.values) {
|
||||
names.addAll(dayData.keys);
|
||||
}
|
||||
|
||||
int totalDays = end.difference(start).inDays + 1;
|
||||
final Map<String, List<FlSpot>> 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<String, Map<String, double>> _parseDateNameMap(List<dynamic> jsonData) {
|
||||
final Map<String, Map<String, double>> 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<void> 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<dynamic> 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<void> 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<dynamic> 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<void> 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<dynamic> 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<void> 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<String, dynamic>;
|
||||
if (json['status'] == 'failure') return;
|
||||
final List<dynamic> 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<void> 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<String, dynamic>;
|
||||
if (json['status'] == 'failure') return;
|
||||
final List<dynamic> 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<void> 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<dynamic> data = json['message']?['data'] ?? [];
|
||||
|
||||
// Aggregate by name (dynamic — no hardcoded allowed list)
|
||||
final Map<String, int> 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<void> 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<EmployeeChartData> 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<EmployeeChartData> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user