2026-03-10-1
This commit is contained in:
@@ -53,7 +53,7 @@ class CaptainAdminController extends GetxController {
|
||||
update();
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.find_driver_by_phone,
|
||||
payload: {'phone': phone},
|
||||
payload: {'phone': "963$phone"},
|
||||
);
|
||||
var d = (res);
|
||||
if (d != 'failure') {
|
||||
|
||||
@@ -54,14 +54,14 @@ class DashboardController extends GetxController {
|
||||
}
|
||||
|
||||
// 🔹 Check SMS credit
|
||||
var res2 = await CRUD().kazumiSMS(
|
||||
link: 'https://sms.kazumi.me/api/sms/check-credit',
|
||||
payload: {"username": "Sefer", "password": AK.smsPasswordEgypt},
|
||||
);
|
||||
// var res2 = await CRUD().kazumiSMS(
|
||||
// link: 'https://sms.kazumi.me/api/sms/check-credit',
|
||||
// payload: {"username": "Sefer", "password": AK.smsPasswordEgypt},
|
||||
// );
|
||||
|
||||
creditSMS = res2['credit'];
|
||||
print('📱 SMS Credit Response: ${jsonEncode(res2)}');
|
||||
print('💰 creditSMS: $creditSMS');
|
||||
// creditSMS = res2['credit'];
|
||||
// print('📱 SMS Credit Response: ${jsonEncode(res2)}');
|
||||
// print('💰 creditSMS: $creditSMS');
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -7,8 +8,72 @@ 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 {
|
||||
// --- Date & State Management ---
|
||||
// ─── 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);
|
||||
@@ -19,56 +84,53 @@ class StaticController extends GetxController {
|
||||
bool isComparing = false;
|
||||
bool isLoading = false;
|
||||
|
||||
// --- Daily Notes State ---
|
||||
// ─── Daily Notes State ─────────────────────────────────────
|
||||
bool isLoadingNotes = false;
|
||||
List<dynamic> dailyNotesList = [];
|
||||
|
||||
// --- Chart Data (Current Range) ---
|
||||
// ─── Main Chart Data ───────────────────────────────────────
|
||||
List<FlSpot> chartDataPassengers = [];
|
||||
List<FlSpot> chartDataDrivers = [];
|
||||
List<FlSpot> chartDataRides = [];
|
||||
List<FlSpot> chartDataDriversMatchingNotes = [];
|
||||
|
||||
// Employee Data (Notes/General Stats)
|
||||
List<FlSpot> chartDataEmployeerama1 = [];
|
||||
List<FlSpot> chartDataEmployeeshahd = [];
|
||||
List<FlSpot> chartDataEmployeeRama2 = [];
|
||||
List<FlSpot> chartDataEmployeeSefer4 = [];
|
||||
|
||||
// Employee Data (Calls/Activations Stats)
|
||||
List<FlSpot> chartDataCallsrama1 = [];
|
||||
List<FlSpot> chartDataCallsShahd = [];
|
||||
List<FlSpot> chartDataCallsRama2 = [];
|
||||
List<FlSpot> chartDataCallsSefer4 = [];
|
||||
|
||||
// --- Chart Data (Comparison Range) ---
|
||||
List<FlSpot> chartDataPassengersCompare = [];
|
||||
List<FlSpot> chartDataDriversCompare = [];
|
||||
List<FlSpot> chartDataRidesCompare = [];
|
||||
List<FlSpot> chartDataDriversMatchingNotesCompare = [];
|
||||
|
||||
// Employee Comparison (Notes)
|
||||
List<FlSpot> chartDataEmployeerama1Compare = [];
|
||||
List<FlSpot> chartDataEmployeeshahdCompare = [];
|
||||
List<FlSpot> chartDataEmployeeRama2Compare = [];
|
||||
List<FlSpot> chartDataEmployeeSefer4Compare = [];
|
||||
// ─── 🔥 DYNAMIC Employee Data ─────────────────────────────
|
||||
// Key = employee name (from server), Value = their chart data
|
||||
Map<String, EmployeeChartData> employeeData = {};
|
||||
Map<String, EmployeeChartData> employeeDataCompare = {};
|
||||
|
||||
// Employee Comparison (Calls/Activations)
|
||||
List<FlSpot> chartDataCallsrama1Compare = [];
|
||||
List<FlSpot> chartDataCallsShahdCompare = [];
|
||||
List<FlSpot> chartDataCallsRama2Compare = [];
|
||||
List<FlSpot> chartDataCallsSefer4Compare = [];
|
||||
// Set of all known employee names (union of current + compare)
|
||||
Set<String> get allEmployeeNames => {
|
||||
...employeeData.keys,
|
||||
...employeeDataCompare.keys,
|
||||
};
|
||||
|
||||
// --- Totals ---
|
||||
// ─── Employment Stats ──────────────────────────────────────
|
||||
List<EmploymentStat> employmentStatsList = [];
|
||||
|
||||
// ─── Totals ────────────────────────────────────────────────
|
||||
String totalMonthlyPassengers = '0';
|
||||
String totalMonthlyRides = '0';
|
||||
String totalMonthlyDrivers = '0';
|
||||
|
||||
// --- Raw Lists ---
|
||||
// ─── Raw Lists ─────────────────────────────────────────────
|
||||
List staticList = [];
|
||||
|
||||
// --- Employment Type Stats List (Simple Count) ---
|
||||
List<Map<String, dynamic>> employmentStatsList = [];
|
||||
// ─── 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() {
|
||||
@@ -76,7 +138,7 @@ class StaticController extends GetxController {
|
||||
getAll();
|
||||
}
|
||||
|
||||
// --- Helpers for View ---
|
||||
// ─── Helpers ───────────────────────────────────────────────
|
||||
double get daysInPeriod {
|
||||
if (startDate == null || endDate == null) return 31;
|
||||
return endDate!.difference(startDate!).inDays + 1.0;
|
||||
@@ -84,10 +146,17 @@ class StaticController extends GetxController {
|
||||
|
||||
String get currentDateString {
|
||||
if (startDate == null || endDate == null) return "";
|
||||
return "${DateFormat('yyyy-MM-dd').format(startDate!)} : ${DateFormat('yyyy-MM-dd').format(endDate!)}";
|
||||
return "${DateFormat('yyyy-MM-dd').format(startDate!)} : "
|
||||
"${DateFormat('yyyy-MM-dd').format(endDate!)}";
|
||||
}
|
||||
|
||||
// --- Date Actions ---
|
||||
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;
|
||||
@@ -120,29 +189,18 @@ class StaticController extends GetxController {
|
||||
chartDataDriversCompare.clear();
|
||||
chartDataRidesCompare.clear();
|
||||
chartDataDriversMatchingNotesCompare.clear();
|
||||
|
||||
chartDataEmployeerama1Compare.clear();
|
||||
chartDataEmployeeshahdCompare.clear();
|
||||
chartDataEmployeeRama2Compare.clear();
|
||||
chartDataEmployeeSefer4Compare.clear();
|
||||
|
||||
chartDataCallsrama1Compare.clear();
|
||||
chartDataCallsShahdCompare.clear();
|
||||
chartDataCallsRama2Compare.clear();
|
||||
chartDataCallsSefer4Compare.clear();
|
||||
employeeDataCompare.clear();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getPayload(DateTime start, DateTime end) {
|
||||
return {
|
||||
"start_date": DateFormat('yyyy-MM-dd').format(start),
|
||||
"end_date": DateFormat('yyyy-MM-dd').format(end),
|
||||
"month": start.month.toString(),
|
||||
"year": start.year.toString(),
|
||||
};
|
||||
}
|
||||
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 Logic ---
|
||||
Future getAll() async {
|
||||
// ─── Main Fetch ────────────────────────────────────────────
|
||||
Future<void> getAll() async {
|
||||
if (startDate == null || endDate == null) return;
|
||||
|
||||
isLoading = true;
|
||||
@@ -152,8 +210,8 @@ class StaticController extends GetxController {
|
||||
fetchPassengers(isCompare: false),
|
||||
fetchRides(isCompare: false),
|
||||
fetchDrivers(isCompare: false),
|
||||
fetchEmployee(isCompare: false),
|
||||
fetchEditorCalls(isCompare: false),
|
||||
fetchEmployeeDynamic(isCompare: false),
|
||||
fetchEditorCallsDynamic(isCompare: false),
|
||||
fetchEmploymentStats(),
|
||||
]);
|
||||
|
||||
@@ -162,8 +220,8 @@ class StaticController extends GetxController {
|
||||
fetchPassengers(isCompare: true),
|
||||
fetchRides(isCompare: true),
|
||||
fetchDrivers(isCompare: true),
|
||||
fetchEmployee(isCompare: true),
|
||||
fetchEditorCalls(isCompare: true),
|
||||
fetchEmployeeDynamic(isCompare: true),
|
||||
fetchEditorCallsDynamic(isCompare: true),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -171,90 +229,124 @@ class StaticController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
// ... (Existing Functions _generateSpots, fetchPassengers, etc.) ...
|
||||
List<FlSpot> _generateSpots(List<dynamic> data, String dateKey,
|
||||
String valueKey, DateTime startOfRange) {
|
||||
List<FlSpot> spots = [];
|
||||
Map<String, double> dataMap = {};
|
||||
for (var item in data) {
|
||||
String dateStr = item[dateKey].toString();
|
||||
double val = double.tryParse(item[valueKey].toString()) ?? 0.0;
|
||||
dataMap[dateStr] = val;
|
||||
}
|
||||
DateTime rangeEnd =
|
||||
(startOfRange == startDate) ? endDate! : compareEndDate!;
|
||||
int totalDays = rangeEnd.difference(startOfRange).inDays + 1;
|
||||
for (int i = 0; i < totalDays; i++) {
|
||||
DateTime currentDate = startOfRange.add(Duration(days: i));
|
||||
String dateKeyStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||
double value = dataMap[dateKeyStr] ?? 0.0;
|
||||
spots.add(FlSpot((i + 1).toDouble(), value));
|
||||
}
|
||||
return spots;
|
||||
// ─── 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);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fetchPassengers({bool isCompare = false}) async {
|
||||
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getPassengersStatic, payload: _getPayload(start, end));
|
||||
var jsonResponse = jsonDecode(res);
|
||||
if (jsonResponse['status'] == 'failure') return;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
if (!isCompare &&
|
||||
jsonData.isNotEmpty &&
|
||||
jsonData[0]['totalMonthly'] != null) {
|
||||
totalMonthlyPassengers = jsonData[0]['totalMonthly'].toString();
|
||||
/// 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);
|
||||
}
|
||||
List<FlSpot> spots =
|
||||
_generateSpots(jsonData, 'day', 'totalPassengers', start);
|
||||
|
||||
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 {
|
||||
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||
var res = await CRUD()
|
||||
final start = isCompare ? compareStartDate! : startDate!;
|
||||
final end = isCompare ? compareEndDate! : endDate!;
|
||||
final res = await CRUD()
|
||||
.get(link: AppLink.getRidesStatic, payload: _getPayload(start, end));
|
||||
var jsonResponse = jsonDecode(res);
|
||||
if (jsonResponse['status'] == 'failure') return;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
if (!isCompare &&
|
||||
jsonData.isNotEmpty &&
|
||||
jsonData[0]['totalMonthly'] != null) {
|
||||
totalMonthlyRides = jsonData[0]['totalMonthly'].toString();
|
||||
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();
|
||||
}
|
||||
List<FlSpot> spots = _generateSpots(jsonData, 'day', 'totalRides', start);
|
||||
final spots = _generateSpots(data, 'day', 'totalRides', start, end);
|
||||
if (isCompare)
|
||||
chartDataRidesCompare = spots;
|
||||
else
|
||||
chartDataRides = spots;
|
||||
}
|
||||
|
||||
// ─── Drivers ──────────────────────────────────────────────
|
||||
Future<void> fetchDrivers({bool isCompare = false}) async {
|
||||
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||
var res = await CRUD().get(
|
||||
final start = isCompare ? compareStartDate! : startDate!;
|
||||
final end = isCompare ? compareEndDate! : endDate!;
|
||||
final res = await CRUD().get(
|
||||
link: AppLink.getdriverstotalMonthly, payload: _getPayload(start, end));
|
||||
var jsonResponse = jsonDecode(res);
|
||||
if (jsonResponse['status'] == 'failure') return;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
final json = jsonDecode(res);
|
||||
if (json['status'] == 'failure') return;
|
||||
final List<dynamic> data = json['message'];
|
||||
if (!isCompare &&
|
||||
jsonData.isNotEmpty &&
|
||||
jsonData[0]['totalMonthlyDrivers'] != null) {
|
||||
totalMonthlyDrivers = jsonData[0]['totalMonthlyDrivers'].toString();
|
||||
data.isNotEmpty &&
|
||||
data[0]['totalMonthlyDrivers'] != null) {
|
||||
totalMonthlyDrivers = data[0]['totalMonthlyDrivers'].toString();
|
||||
staticList = data;
|
||||
}
|
||||
if (!isCompare) {
|
||||
staticList = jsonData;
|
||||
}
|
||||
|
||||
List<FlSpot> spotsDrivers =
|
||||
_generateSpots(jsonData, 'day', 'dailyTotalDrivers', start);
|
||||
List<FlSpot> spotsNotes =
|
||||
_generateSpots(jsonData, 'day', 'dailyMatchingNotes', start);
|
||||
final spotsDrivers =
|
||||
_generateSpots(data, 'day', 'dailyTotalDrivers', start, end);
|
||||
final spotsNotes =
|
||||
_generateSpots(data, 'day', 'dailyMatchingNotes', start, end);
|
||||
if (isCompare) {
|
||||
chartDataDriversCompare = spotsDrivers;
|
||||
chartDataDriversMatchingNotesCompare = spotsNotes;
|
||||
@@ -264,237 +356,126 @@ class StaticController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchEmployee({bool isCompare = false}) async {
|
||||
// ─── 🔥 DYNAMIC: Employee Notes ───────────────────────────
|
||||
Future<void> fetchEmployeeDynamic({bool isCompare = false}) async {
|
||||
try {
|
||||
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||
var res = await CRUD().get(
|
||||
final start = isCompare ? compareStartDate! : startDate!;
|
||||
final end = isCompare ? compareEndDate! : endDate!;
|
||||
final res = await CRUD().get(
|
||||
link: AppLink.getEmployeeStatic, payload: _getPayload(start, end));
|
||||
|
||||
if (isCompare) {
|
||||
chartDataEmployeerama1Compare = [];
|
||||
chartDataEmployeeshahdCompare = [];
|
||||
chartDataEmployeeRama2Compare = [];
|
||||
chartDataEmployeeSefer4Compare = [];
|
||||
} else {
|
||||
chartDataEmployeerama1 = [];
|
||||
chartDataEmployeeshahd = [];
|
||||
chartDataEmployeeRama2 = [];
|
||||
chartDataEmployeeSefer4 = [];
|
||||
}
|
||||
|
||||
if (res == 'failure') return;
|
||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||
if (jsonResponse['status'] == 'failure') return;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
if (jsonData.isEmpty) return;
|
||||
final json = jsonDecode(res) as Map<String, dynamic>;
|
||||
if (json['status'] == 'failure') return;
|
||||
final List<dynamic> data = json['message'];
|
||||
if (data.isEmpty) return;
|
||||
|
||||
Map<String, Map<String, double>> dateNameMap = {};
|
||||
for (var item in jsonData) {
|
||||
String dateKeyStr = item['date'] ?? item['day'];
|
||||
String name = item['NAME'].toString().toLowerCase().trim();
|
||||
double count = double.tryParse(item['count'].toString()) ?? 0.0;
|
||||
if (!dateNameMap.containsKey(dateKeyStr)) dateNameMap[dateKeyStr] = {};
|
||||
if (dateNameMap[dateKeyStr]!.containsKey(name)) {
|
||||
dateNameMap[dateKeyStr]![name] =
|
||||
dateNameMap[dateKeyStr]![name]! + count;
|
||||
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 {
|
||||
dateNameMap[dateKeyStr]![name] = count;
|
||||
}
|
||||
}
|
||||
|
||||
final targetLists = isCompare
|
||||
? {
|
||||
'rama1': chartDataEmployeerama1Compare,
|
||||
'shahd': chartDataEmployeeshahdCompare,
|
||||
'rama2': chartDataEmployeeRama2Compare,
|
||||
'mayar': chartDataEmployeeSefer4Compare
|
||||
}
|
||||
: {
|
||||
'rama1': chartDataEmployeerama1,
|
||||
'shahd': chartDataEmployeeshahd,
|
||||
'rama2': chartDataEmployeeRama2,
|
||||
'mayar': chartDataEmployeeSefer4
|
||||
};
|
||||
|
||||
int totalDays = end.difference(start).inDays + 1;
|
||||
targetLists.forEach((key, listToFill) {
|
||||
for (int i = 0; i < totalDays; i++) {
|
||||
DateTime currentDate = start.add(Duration(days: i));
|
||||
String currentDateStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||
double value = 0;
|
||||
Map<String, double>? dayData = dateNameMap[currentDateStr];
|
||||
if (dayData != null) {
|
||||
// if (key == 'mayar') {
|
||||
// value = (dayData['mayar'] ?? 0) +
|
||||
// (dayData['rama1'] ?? 0) +
|
||||
// (dayData['sefer4'] ?? 0);
|
||||
// } else {
|
||||
value = dayData[key] ?? 0;
|
||||
// }
|
||||
}
|
||||
listToFill.add(FlSpot((i + 1).toDouble(), value));
|
||||
target[name] = EmployeeChartData(
|
||||
name: name,
|
||||
color: color,
|
||||
notesSpots: spots,
|
||||
callsSpots: [],
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Log.print('Error in fetchEmployee: $e');
|
||||
Log.print('Error in fetchEmployeeDynamic: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchEditorCalls({bool isCompare = false}) async {
|
||||
// ─── 🔥 DYNAMIC: Employee Calls ───────────────────────────
|
||||
Future<void> fetchEditorCallsDynamic({bool isCompare = false}) async {
|
||||
try {
|
||||
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||
|
||||
var res = await CRUD().get(
|
||||
final start = isCompare ? compareStartDate! : startDate!;
|
||||
final end = isCompare ? compareEndDate! : endDate!;
|
||||
final res = await CRUD().get(
|
||||
link: AppLink.getEditorStatsCalls, payload: _getPayload(start, end));
|
||||
|
||||
if (isCompare) {
|
||||
chartDataCallsrama1Compare = [];
|
||||
chartDataCallsShahdCompare = [];
|
||||
chartDataCallsRama2Compare = [];
|
||||
chartDataCallsSefer4Compare = [];
|
||||
} else {
|
||||
chartDataCallsrama1 = [];
|
||||
chartDataCallsShahd = [];
|
||||
chartDataCallsRama2 = [];
|
||||
chartDataCallsSefer4 = [];
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||
if (jsonResponse['status'] == 'failure') return;
|
||||
final dateNameMap = _parseDateNameMap(data);
|
||||
final spotsMap = _generateEmployeeSpots(dateNameMap, start, end);
|
||||
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
if (jsonData.isEmpty) return;
|
||||
final target = isCompare ? employeeDataCompare : employeeData;
|
||||
|
||||
Map<String, Map<String, double>> dateNameMap = {};
|
||||
|
||||
for (var item in jsonData) {
|
||||
String dateKeyStr = item['date'] ?? item['day'];
|
||||
String name = item['NAME'].toString().toLowerCase().trim();
|
||||
double count = double.tryParse(item['count'].toString()) ?? 0.0;
|
||||
|
||||
if (!dateNameMap.containsKey(dateKeyStr)) {
|
||||
dateNameMap[dateKeyStr] = {};
|
||||
}
|
||||
|
||||
if (dateNameMap[dateKeyStr]!.containsKey(name)) {
|
||||
dateNameMap[dateKeyStr]![name] =
|
||||
dateNameMap[dateKeyStr]![name]! + count;
|
||||
spotsMap.forEach((name, spots) {
|
||||
final color = _getOrAssignColor(name);
|
||||
if (target.containsKey(name)) {
|
||||
target[name] = target[name]!.copyWith(callsSpots: spots);
|
||||
} else {
|
||||
dateNameMap[dateKeyStr]![name] = count;
|
||||
}
|
||||
}
|
||||
|
||||
final targetLists = isCompare
|
||||
? {
|
||||
'rama1': chartDataCallsrama1Compare,
|
||||
'shahd': chartDataCallsShahdCompare,
|
||||
'rama2': chartDataCallsRama2Compare,
|
||||
'mayar': chartDataCallsSefer4Compare,
|
||||
}
|
||||
: {
|
||||
'rama1': chartDataCallsrama1,
|
||||
'shahd': chartDataCallsShahd,
|
||||
'rama2': chartDataCallsRama2,
|
||||
'mayar': chartDataCallsSefer4,
|
||||
};
|
||||
|
||||
int totalDays = end.difference(start).inDays + 1;
|
||||
|
||||
targetLists.forEach((key, listToFill) {
|
||||
for (int i = 0; i < totalDays; i++) {
|
||||
DateTime currentDate = start.add(Duration(days: i));
|
||||
String currentDateStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||
|
||||
double value = 0;
|
||||
Map<String, double>? dayData = dateNameMap[currentDateStr];
|
||||
|
||||
if (dayData != null) {
|
||||
// if (key == 'mayar_group') {
|
||||
// value = (dayData['mayar'] ?? 0) +
|
||||
// (dayData['rama1'] ?? 0) +
|
||||
// (dayData['sefer4'] ?? 0);
|
||||
// } else {
|
||||
value = dayData[key] ?? 0;
|
||||
// }
|
||||
}
|
||||
listToFill.add(FlSpot((i + 1).toDouble(), value));
|
||||
target[name] = EmployeeChartData(
|
||||
name: name,
|
||||
color: color,
|
||||
notesSpots: [],
|
||||
callsSpots: spots,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Log.print('Error in fetchEditorCalls: $e');
|
||||
Log.print('Error in fetchEditorCallsDynamic: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// --- 🔴 FIXED: Fetch Employment Stats with Unique Check ---
|
||||
// ─── Employment Stats ─────────────────────────────────────
|
||||
Future<void> fetchEmploymentStats() async {
|
||||
try {
|
||||
// لا نستخدم .clear() هنا، سنقوم باستبدال القائمة بالكامل في النهاية
|
||||
|
||||
var res = await CRUD().get(
|
||||
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'] ?? [];
|
||||
|
||||
var jsonResponse = jsonDecode(res);
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
if (jsonResponse['message'] != null &&
|
||||
jsonResponse['message']['data'] != null) {
|
||||
List<dynamic> data = jsonResponse['message']['data'];
|
||||
|
||||
List<String> allowedNames = ['shahd', 'mayar', 'rama1', 'rama2'];
|
||||
|
||||
// استخدام Map لضمان عدم تكرار الأسماء (تجميع القيم)
|
||||
Map<String, int> uniqueMap = {};
|
||||
|
||||
for (var item in data) {
|
||||
String name =
|
||||
item['employmentType'].toString().toLowerCase().trim();
|
||||
int count = int.tryParse(item['count'].toString()) ?? 0;
|
||||
|
||||
if (allowedNames.contains(name)) {
|
||||
if (uniqueMap.containsKey(name)) {
|
||||
uniqueMap[name] = uniqueMap[name]! + count;
|
||||
} else {
|
||||
uniqueMap[name] = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// تحويل الـ Map إلى القائمة النهائية
|
||||
List<Map<String, dynamic>> tempList = [];
|
||||
uniqueMap.forEach((key, value) {
|
||||
tempList.add({'name': key, 'count': value});
|
||||
});
|
||||
|
||||
// استبدال القائمة القديمة بالقائمة الجديدة النظيفة
|
||||
employmentStatsList = tempList;
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fetch Daily Notes Log ---
|
||||
// ─── Daily Notes ──────────────────────────────────────────
|
||||
Future<void> fetchDailyNotes(DateTime date) async {
|
||||
try {
|
||||
isLoadingNotes = true;
|
||||
dailyNotesList.clear();
|
||||
update();
|
||||
|
||||
var res = await CRUD().post(
|
||||
final res = await CRUD().post(
|
||||
link: AppLink.getNotesForEmployee,
|
||||
payload: {"date": DateFormat('yyyy-MM-dd').format(date)});
|
||||
|
||||
if (res != 'failure') {
|
||||
var jsonResponse = (res);
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
dailyNotesList = jsonResponse['message'];
|
||||
final json = res;
|
||||
if (json['status'] == 'success') {
|
||||
dailyNotesList = json['message'];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -504,4 +485,28 @@ class StaticController extends GetxController {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -56,19 +56,19 @@ class WalletController extends GetxController {
|
||||
});
|
||||
}
|
||||
|
||||
Future addDrivergift3000(String paymentMethod, driverID, point, phone) async {
|
||||
Future addDrivergift300(String paymentMethod, driverID, point, phone) async {
|
||||
// paymentToken = await generateToken(count);
|
||||
// var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
var res = await CRUD().postWallet(link: AppLink.add300ToDriver, payload: {
|
||||
'driverID': driverID.toString(),
|
||||
'paymentID': paymentMethod,
|
||||
'amount': point,
|
||||
'token': 'gift_connect_30000',
|
||||
'token': 'gift_connect_300',
|
||||
'paymentMethod': paymentMethod,
|
||||
'phone': phone,
|
||||
});
|
||||
if (res != 'failure') {
|
||||
Get.snackbar('success', 'addDrivergift3000',
|
||||
Get.snackbar('success', 'addDrivergift300',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
} else {
|
||||
Get.snackbar('error', res, backgroundColor: AppColor.redColor);
|
||||
|
||||
@@ -1,234 +1,237 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_admin1/constant/box_name.dart';
|
||||
import 'package:sefer_admin1/constant/links.dart';
|
||||
import 'package:sefer_admin1/controller/firebase/firbase_messge.dart';
|
||||
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||
import 'package:sefer_admin1/main.dart';
|
||||
import 'package:sefer_admin1/views/widgets/elevated_btn.dart';
|
||||
import 'package:sefer_admin1/views/widgets/my_textField.dart';
|
||||
|
||||
import '../constant/style.dart';
|
||||
import '../print.dart';
|
||||
import 'firebase/notification_service.dart';
|
||||
|
||||
class NotificationController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final title = TextEditingController();
|
||||
final body = TextEditingController();
|
||||
List<String> tokensDriver = [];
|
||||
List<String> tokensPassengers = [];
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
final TextEditingController bodyController = TextEditingController();
|
||||
|
||||
// getTokensDrivers() async {
|
||||
// await FirebaseMessagesController().loadAllPagesAndSendNotifications();
|
||||
// }
|
||||
// ألوان الثيم (متناسقة مع باقي الصفحات المحسنة)
|
||||
final Color _dialogColor = const Color(0xFF1A1F3A);
|
||||
final Color _inputColor = const Color(0xFF0A0E27);
|
||||
final Color _primaryAccent = const Color(0xFF6366F1);
|
||||
|
||||
// getTokensPassengers() async {
|
||||
// await FirebaseMessagesController()
|
||||
// .loadAllPagesAndSendNotificationsPassengers();
|
||||
// }
|
||||
|
||||
Future<dynamic> sendNotificationDrivers() {
|
||||
return Get.defaultDialog(
|
||||
title: 'send notification'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: MyTextForm(
|
||||
controller: title,
|
||||
label: 'title notification'.tr,
|
||||
hint: 'title notification'.tr,
|
||||
type: TextInputType.name),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: MyTextForm(
|
||||
controller: body,
|
||||
label: 'body notification'.tr,
|
||||
hint: 'body notification'.tr,
|
||||
type: TextInputType.name),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'send'.tr,
|
||||
onPressed: () async {
|
||||
// tokensDriver = box.read(BoxName.tokensDrivers)['message'];
|
||||
// Log.print('tokensDriver: ${tokensDriver}');
|
||||
// if (formKey.currentState!.validate()) {
|
||||
// box.read(BoxName.tokensDrivers)['message'].length;
|
||||
// for (var i = 0;
|
||||
// i < box.read(BoxName.tokensDrivers)['message'].length;
|
||||
// i++) {
|
||||
// // for (var i = 0; i < 2; i++) {
|
||||
// // print(i);
|
||||
// var res = await CRUD()
|
||||
// .post(link: AppLink.addNotificationCaptain, payload: {
|
||||
// "driverID": box
|
||||
// .read(BoxName.tokensDrivers)['message'][i]['id']
|
||||
// .toString(),
|
||||
// "title": title.text,
|
||||
// "body": body.text,
|
||||
// "isPin": 'unPin',
|
||||
// });
|
||||
// Log.print(
|
||||
// 'res: ${res}for ${box.read(BoxName.tokensDrivers)['message'][i]['id']}');
|
||||
// // Log.print('tokensDriver[i]: ${tokensDriver[i]}');
|
||||
// Future.delayed(const Duration(microseconds: 50));
|
||||
NotificationService.sendNotification(
|
||||
target: 'drivers', // الإرسال لجميع المشتركين في "service"
|
||||
title: title.text,
|
||||
body: body.text,
|
||||
isTopic: true,
|
||||
category: 'fromAdmin', // فئة توضح نوع الإشعار
|
||||
);
|
||||
// FirebaseMessagesController().sendNotificationToAnyWithoutData(
|
||||
// title.text,
|
||||
// body.text,
|
||||
// box
|
||||
// .read(BoxName.tokensDrivers)['message'][i]['token']
|
||||
// .toString(),
|
||||
// 'tone2.wav');
|
||||
// }
|
||||
Get.back();
|
||||
// }
|
||||
}),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'cancel',
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
}));
|
||||
@override
|
||||
void onClose() {
|
||||
titleController.dispose();
|
||||
bodyController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<dynamic> sendNotificationPassengers() {
|
||||
return Get.defaultDialog(
|
||||
title: 'send notification'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: MyTextForm(
|
||||
controller: title,
|
||||
label: 'title notification'.tr,
|
||||
hint: 'title notification'.tr,
|
||||
type: TextInputType.name),
|
||||
/// تنظيف الحقول
|
||||
void _clearFields() {
|
||||
titleController.clear();
|
||||
bodyController.clear();
|
||||
}
|
||||
|
||||
/// إرسال إشعار للسائقين
|
||||
Future<void> sendNotificationDrivers() async {
|
||||
_clearFields();
|
||||
await _showCustomDialog(
|
||||
dialogTitle: 'إشعار للسائقين',
|
||||
targetAudience: 'drivers',
|
||||
icon: Icons.drive_eta_rounded,
|
||||
iconColor: Colors.orangeAccent,
|
||||
);
|
||||
}
|
||||
|
||||
/// إرسال إشعار للركاب
|
||||
Future<void> sendNotificationPassengers() async {
|
||||
_clearFields();
|
||||
await _showCustomDialog(
|
||||
dialogTitle: 'إشعار للركاب',
|
||||
targetAudience: 'passengers',
|
||||
icon: Icons.people_alt_rounded,
|
||||
iconColor: Colors.blueAccent,
|
||||
);
|
||||
}
|
||||
|
||||
/// دالة عامة لإظهار نافذة الإرسال بتصميم عصري
|
||||
Future<void> _showCustomDialog({
|
||||
required String dialogTitle,
|
||||
required String targetAudience,
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
}) {
|
||||
return Get.dialog(
|
||||
Dialog(
|
||||
backgroundColor: _dialogColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
elevation: 10,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1. أيقونة العنوان
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.15),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: iconColor.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 32),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 2. العنوان
|
||||
Text(
|
||||
dialogTitle,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Segoe UI',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 3. حقول الإدخال
|
||||
_buildModernTextField(
|
||||
controller: titleController,
|
||||
hint: 'عنوان الإشعار',
|
||||
icon: Icons.title_rounded,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildModernTextField(
|
||||
controller: bodyController,
|
||||
hint: 'نص الرسالة',
|
||||
icon: Icons.message_rounded,
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 4. الأزرار
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.white54,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: const Text('إلغاء'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (titleController.text.trim().isEmpty ||
|
||||
bodyController.text.trim().isEmpty) {
|
||||
Get.snackbar(
|
||||
"تنبيه",
|
||||
"الرجاء تعبئة جميع الحقول",
|
||||
backgroundColor: Colors.amber.withOpacity(0.8),
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Get.back(); // إغلاق النافذة
|
||||
await _processSending(targetAudience);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _primaryAccent,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('إرسال'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: MyTextForm(
|
||||
controller: body,
|
||||
label: 'body notification'.tr,
|
||||
hint: 'body notification'.tr,
|
||||
type: TextInputType.name),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'send'.tr,
|
||||
onPressed: () async {
|
||||
// tokensPassengers = box.read(BoxName.tokensPassengers);
|
||||
// var tokensPassengersData =
|
||||
// box.read(BoxName.tokensPassengers)['data'];
|
||||
),
|
||||
barrierColor: Colors.black.withOpacity(0.7), // تعتيم الخلفية
|
||||
);
|
||||
}
|
||||
|
||||
// // Debug print to check structure of the 'data' field
|
||||
// print('Tokens Passengers Data: $tokensPassengersData');
|
||||
/// تنفيذ عملية الإرسال الفعلية
|
||||
Future<void> _processSending(String target) async {
|
||||
// إظهار تنبيه بدء العملية
|
||||
Get.snackbar(
|
||||
"جاري الإرسال",
|
||||
"يتم إرسال الإشعار لـ $target...",
|
||||
backgroundColor: _primaryAccent.withOpacity(0.2),
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
// if (tokensPassengersData is List) {
|
||||
// for (var i = 0; i < tokensPassengersData.length; i++) {
|
||||
// if (formKey.currentState!.validate()) {
|
||||
// var res = await CRUD()
|
||||
// .post(link: AppLink.addNotificationPassenger, payload: {
|
||||
// "passenger_id":
|
||||
// tokensPassengersData[i]['passengerID'].toString(),
|
||||
// "title": title.text,
|
||||
// "body": body.text,
|
||||
// });
|
||||
// Log.print('res: ${res}');
|
||||
// FirebaseMessagesController()
|
||||
// .sendNotificationToAnyWithoutData(
|
||||
// title.text,
|
||||
// body.text,
|
||||
// tokensPassengersData[i]['token']
|
||||
// .toString(), // Access token correctly
|
||||
// 'order.wav',
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
NotificationService.sendNotification(
|
||||
target: 'passengers', // الإرسال لجميع المشتركين في "service"
|
||||
title: title.text,
|
||||
body: body.text,
|
||||
isTopic: true,
|
||||
category: 'fromAdmin', // فئة توضح نوع الإشعار
|
||||
);
|
||||
Get.back();
|
||||
// } else {
|
||||
// // Handle the case where 'data' is not a list
|
||||
// print('Data is not a list: $tokensPassengersData');
|
||||
// }
|
||||
}),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'cancel',
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
}));
|
||||
try {
|
||||
// استدعاء خدمة الإرسال
|
||||
await NotificationService.sendNotification(
|
||||
target: target,
|
||||
title: titleController.text,
|
||||
body: bodyController.text,
|
||||
isTopic: true,
|
||||
category: 'fromAdmin',
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
"نجاح",
|
||||
"تم إرسال الإشعار بنجاح",
|
||||
backgroundColor: Colors.green.withOpacity(0.5),
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"خطأ",
|
||||
"فشل إرسال الإشعار: $e",
|
||||
backgroundColor: Colors.red.withOpacity(0.5),
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// تصميم حقل الإدخال المخصص (Dark Input Field)
|
||||
Widget _buildModernTextField({
|
||||
required TextEditingController controller,
|
||||
required String hint,
|
||||
required IconData icon,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _inputColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle:
|
||||
TextStyle(color: Colors.white.withOpacity(0.3), fontSize: 14),
|
||||
prefixIcon: Icon(icon, color: Colors.white38, size: 20),
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// يلا دلوقتي! تطبيق سفر جاهز عشان تبدأ تستقبل الطلبات
|
||||
// • افتح التطبيق دلوقتي، واستعد إنك تستقبل طلبات أكتر. كل ما تكون فاتح، فرصتك في الطلبات بتزيد!
|
||||
// 2. خليك فاتح واستقبل طلبات أكتر مع تطبيق سفر
|
||||
// • وجودك متصل في التطبيق هيخليك تستقبل طلبات أكتر. افتح التطبيق دلوقتي وما تفوتش الفرصة!
|
||||
// 3. فرصتك لزيادة دخلك مع تطبيق سفر تبدأ من دلوقتي!
|
||||
// • مجرد إنك تفتح التطبيق مش هيأثر عليك، بالعكس، هيزود فرصتك في طلبات أكتر. افتح التطبيق واشترك دلوقتي!
|
||||
|
||||
//sms
|
||||
// link sefer driver is https://shorturl.at/IHJcm1.
|
||||
// // ميزات الأمان بعد 500 رحلة:
|
||||
// • “بعد 500 رحلة مع سفر، تحصل على مميزات أمان إضافية لضمان راحتك.”
|
||||
// • “نوفر لك ميزات أمان متقدمة بعد 500 رحلة لتجربة قيادة أكثر أمانًا.”
|
||||
// • “مع 500 رحلة، تحصل على دعم أمني متقدم لتوفير أفضل تجربة قيادة.”
|
||||
// 2. ميزات الصيانة:
|
||||
// • “احصل على خدمات صيانة مجانية بعد عدد معين من الرحلات مع سفر.”
|
||||
// • “استمتع بخدمات صيانة حصرية عند الوصول إلى عدد محدد من الرحلات.”
|
||||
// • “مع سفر، نقدم لك عروض صيانة مميزة لتحافظ على سيارتك في أفضل حال.”
|
||||
// 3. ميزات فتح حسابات البنوك:
|
||||
// • “مع سفر، يمكنك فتح حساب بنكي بسهولة واستفادة من عروض مميزة.”
|
||||
// • “افتح حساب بنكي مع تطبيق سفر واستفد من خدمات مالية حصرية.”
|
||||
// • “نساعدك على فتح حساب بنكي بأفضل العروض بالتعاون مع البنوك المحلية.”
|
||||
// 4. ميزات ورود السيارات ومعارض السيارات الخاصة بنا:
|
||||
// • “استمتع بعروض مميزة لشراء السيارات من معارض سفر الحصرية.”
|
||||
// • “اختر سيارتك المثالية من معارض سفر بأسعار تنافسية وخدمات مميزة.”
|
||||
// • “نقدم لك أفضل عروض السيارات من معارضنا لتسهيل امتلاك سيارتك الجديدة.”
|
||||
// 5. ميزات أوفر كار:
|
||||
// • “أوفر كار من سفر توفر لك سيارات اقتصادية لزيادة دخلك بكفاءة.”
|
||||
// • “مع أوفر كار، يمكنك العمل بسيارات اقتصادية وتحقيق أرباح أكبر.”
|
||||
// • “تطبيق سفر يقدم لك أوفر كار، الخيار الاقتصادي المثالي لزيادة دخلك.”
|
||||
// 6. مستوى الدخل المحدود والطلبات الاقتصادية:
|
||||
// • “لأصحاب الدخل المحدود، وفرنا طلبات اقتصادية تضمن لك زيادة دخلك.”
|
||||
// • “الطلبات الاقتصادية من سفر تساعدك على زيادة دخلك بسهولة وفعالية.”
|
||||
// • “استفد من طلبات اقتصادية تناسب أصحاب الدخل المحدود لزيادة أرباحك.”
|
||||
// 7. طلبات الليل:
|
||||
// • “مع طلبات الليل من سفر، زود دخلك واستفد من فرص إضافية في المساء.”
|
||||
// • “لا تفوت فرصة طلبات الليل مع سفر، زود دخلك في أي وقت.”
|
||||
// • “طلبات الليل من سفر توفر لك فرصًا إضافية لتحقيق دخل أعلى.”
|
||||
// 8. طلبات الكمفورت الأكثر راحة والسيارات المكيفة:
|
||||
// • “قدّم خدمة مريحة مع طلبات الكمفورت من سفر والسيارات المكيفة.”
|
||||
// • “طلبات الكمفورت توفر تجربة راقية للركاب بسيارات مكيفة ومريحة.”
|
||||
// • “مع سفر، سيارات الكمفورت المكيفة تضمن راحة الركاب وزيادة الطلبات.”
|
||||
// 9. طلبات السبيد:
|
||||
// • “استقبل طلبات السبيد مع سفر لتقديم رحلات أسرع وزيادة دخلك.”
|
||||
// • “طلبات السبيد توفر لك فرصة إكمال المزيد من الرحلات في وقت أقل.”
|
||||
// • “مع طلبات السبيد من سفر، تقدم خدمة سريعة وفعالة لزيادة الأرباح.”
|
||||
// 10. الطلبات الثابتة والمعتدلة السعر والنسبة الثابتة 8%:
|
||||
// • “مع نسبة ثابتة 8%، تحصل على أفضل عروض الأسعار مع سفر.”
|
||||
// • “استمتع بنسبة ثابتة 8%، أقل نسبة بين المنافسين لزيادة دخلك.”
|
||||
// • “طلبات سفر الثابتة تضمن لك دخلاً مستقراً بنسبة أقل من 8%.”
|
||||
|
||||
150
lib/controller/server/server_monitor_controller.dart
Normal file
150
lib/controller/server/server_monitor_controller.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_admin1/constant/links.dart';
|
||||
|
||||
import '../../print.dart';
|
||||
|
||||
// --- Models ---
|
||||
|
||||
class ServerData {
|
||||
final CpuInfo cpu;
|
||||
final MemoryInfo memory;
|
||||
final DiskInfo disk;
|
||||
final Map<String, String> services;
|
||||
final List<ProcessInfo> topProcesses;
|
||||
final NetworkInfo network;
|
||||
final UptimeInfo uptime;
|
||||
final String timestamp;
|
||||
|
||||
ServerData({
|
||||
required this.cpu,
|
||||
required this.memory,
|
||||
required this.disk,
|
||||
required this.services,
|
||||
required this.topProcesses,
|
||||
required this.network,
|
||||
required this.uptime,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory ServerData.fromJson(Map<String, dynamic> json) {
|
||||
return ServerData(
|
||||
cpu: CpuInfo.fromJson(json['cpu']),
|
||||
memory: MemoryInfo.fromJson(json['memory']),
|
||||
disk: DiskInfo.fromJson(json['disk']),
|
||||
services: Map<String, String>.from(json['services']),
|
||||
topProcesses: (json['top_processes'] as List)
|
||||
.map((i) => ProcessInfo.fromJson(i))
|
||||
.toList(),
|
||||
network: NetworkInfo.fromJson(json['network']),
|
||||
uptime: UptimeInfo.fromJson(json['uptime']),
|
||||
timestamp: json['timestamp'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CpuInfo {
|
||||
final double percent;
|
||||
final int cores;
|
||||
final double load1m;
|
||||
CpuInfo({required this.percent, required this.cores, required this.load1m});
|
||||
factory CpuInfo.fromJson(Map<String, dynamic> json) => CpuInfo(
|
||||
percent: json['percent'].toDouble(),
|
||||
cores: json['cores'],
|
||||
load1m: json['load_1m'].toDouble());
|
||||
}
|
||||
|
||||
class MemoryInfo {
|
||||
final double percent;
|
||||
final double usedGb;
|
||||
final double totalGb;
|
||||
MemoryInfo(
|
||||
{required this.percent, required this.usedGb, required this.totalGb});
|
||||
factory MemoryInfo.fromJson(Map<String, dynamic> json) => MemoryInfo(
|
||||
percent: json['percent'].toDouble(),
|
||||
usedGb: json['used_gb'].toDouble(),
|
||||
totalGb: json['total_gb'].toDouble());
|
||||
}
|
||||
|
||||
class DiskInfo {
|
||||
final double percent;
|
||||
final double usedGb;
|
||||
final double totalGb;
|
||||
DiskInfo(
|
||||
{required this.percent, required this.usedGb, required this.totalGb});
|
||||
factory DiskInfo.fromJson(Map<String, dynamic> json) => DiskInfo(
|
||||
percent: json['percent'].toDouble(),
|
||||
usedGb: json['used_gb'].toDouble(),
|
||||
totalGb: json['total_gb'].toDouble());
|
||||
}
|
||||
|
||||
class ProcessInfo {
|
||||
final String name;
|
||||
final String usage;
|
||||
ProcessInfo({required this.name, required this.usage});
|
||||
factory ProcessInfo.fromJson(Map<String, dynamic> json) =>
|
||||
ProcessInfo(name: json['name'], usage: json['usage']);
|
||||
}
|
||||
|
||||
class NetworkInfo {
|
||||
final double receivedMb;
|
||||
final double sentMb;
|
||||
NetworkInfo({required this.receivedMb, required this.sentMb});
|
||||
factory NetworkInfo.fromJson(Map<String, dynamic> json) => NetworkInfo(
|
||||
receivedMb: json['received_mb'].toDouble(),
|
||||
sentMb: json['sent_mb'].toDouble());
|
||||
}
|
||||
|
||||
class UptimeInfo {
|
||||
final String formatted;
|
||||
UptimeInfo({required this.formatted});
|
||||
factory UptimeInfo.fromJson(Map<String, dynamic> json) =>
|
||||
UptimeInfo(formatted: json['formatted']);
|
||||
}
|
||||
|
||||
// --- Controller ---
|
||||
|
||||
class ServerMonitorController extends GetxController {
|
||||
var isLoading = false.obs;
|
||||
var serverData = Rxn<ServerData>();
|
||||
var errorMessage = ''.obs;
|
||||
|
||||
Timer? _timer; // تخزين التايمر
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchServerData();
|
||||
// تحديث تلقائي كل 60 ثانية
|
||||
_timer = Timer.periodic(Duration(seconds: 60), (_) {
|
||||
fetchServerData();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// إلغاء التحديث عند إغلاق الصفحة
|
||||
_timer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> fetchServerData() async {
|
||||
try {
|
||||
isLoading(true);
|
||||
errorMessage('');
|
||||
|
||||
final response = await GetConnect().get(AppLink.serverMonitor);
|
||||
|
||||
if (response.status.hasError) {
|
||||
errorMessage.value = 'خطأ في الاتصال: ${response.statusText}';
|
||||
} else {
|
||||
serverData.value = ServerData.fromJson(response.body);
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'حدث خطأ: $e';
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user