324 lines
11 KiB
Dart
324 lines
11 KiB
Dart
// custom_stacked_bar_chart.dart
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import '../../../core/services/api_service.dart';
|
|
import '../../../data/models/bar_data.dart';
|
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import '../../providers/manager_provider.dart';
|
|
import 'appChartColors.dart';
|
|
|
|
class CustomStackedBarChart extends ConsumerStatefulWidget {
|
|
final List<String> labels;
|
|
final List<String> labelsId;
|
|
final List<dynamic> dataList;
|
|
final double maxY;
|
|
final int initialRotation;
|
|
final List<List<List<dynamic>>> productsList;
|
|
final List<List<String>>? vehicleTypeNames;
|
|
final Map<String, Color>? vehicleColorMap; // Dynamic color mapping
|
|
|
|
const CustomStackedBarChart({
|
|
super.key,
|
|
required this.labels,
|
|
required this.labelsId,
|
|
required this.dataList,
|
|
this.maxY = 100.0,
|
|
this.initialRotation = 1,
|
|
required this.productsList,
|
|
this.vehicleTypeNames,
|
|
this.vehicleColorMap,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<CustomStackedBarChart> createState() =>
|
|
_CustomStackedBarChartState();
|
|
}
|
|
|
|
class _CustomStackedBarChartState extends ConsumerState<CustomStackedBarChart> {
|
|
late int rotationTurns;
|
|
dynamic managerId;
|
|
late ApiService apiService;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
apiService = ApiService();
|
|
rotationTurns = widget.initialRotation;
|
|
Future.microtask(() {
|
|
managerId = ref.read(managerIdProvider);
|
|
});
|
|
}
|
|
|
|
String compactNumber(num value) {
|
|
if (value >= 10000000) {
|
|
return '${(value / 10000000).toStringAsFixed(1).replaceAll('.0', '')}Cr';
|
|
} else if (value >= 1000000) {
|
|
return '${(value / 1000000).toStringAsFixed(1).replaceAll('.0', '')}M';
|
|
} else if (value >= 1000) {
|
|
return '${(value / 1000).toStringAsFixed(1).replaceAll('.0', '')}K';
|
|
} else if (value >= 1) {
|
|
// 👇 remove .0 ONLY when it's an integer
|
|
return value % 1 == 0
|
|
? value.toInt().toString()
|
|
: value.toStringAsFixed(1);
|
|
} else {
|
|
// 👇 keep decimals for small values
|
|
return value == 0 ? '0' : value.toStringAsFixed(1);
|
|
}
|
|
}
|
|
|
|
// else {
|
|
// return value.toInt().toString();
|
|
// }
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final maxY = widget.maxY <= 0 ? 100.0 : widget.maxY;
|
|
|
|
return AspectRatio(
|
|
aspectRatio: 4,
|
|
child: BarChart(
|
|
BarChartData(
|
|
alignment: BarChartAlignment.spaceAround,
|
|
rotationQuarterTurns: rotationTurns,
|
|
|
|
// gridData: FlGridData(
|
|
// show: true,
|
|
// checkToShowHorizontalLine: (value) {
|
|
// final step = maxY / 5;
|
|
// if (step == 0) return false;
|
|
// return (value % step).abs() < 0.0001;
|
|
// },
|
|
// getDrawingHorizontalLine: (value) =>
|
|
// FlLine(color: Colors.grey.withOpacity(0.12), strokeWidth: 0.8),
|
|
// drawVerticalLine: false,
|
|
//
|
|
// ),
|
|
gridData: FlGridData(
|
|
show: true,
|
|
// drawVerticalLine: true,
|
|
drawVerticalLine: false,
|
|
getDrawingHorizontalLine: (value) => FlLine(
|
|
// color: AppColors.gridLinesColor.withValues(alpha: 0.2),
|
|
color: Colors.blueGrey.shade100,
|
|
// color: Colors.black,
|
|
// color: AppColors.borderColor,
|
|
strokeWidth: 0.3,
|
|
),
|
|
),
|
|
// borderData: FlBorderData(show: false),
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: AppColors.contentColorBlack,
|
|
width: 0.1,
|
|
),
|
|
right: BorderSide(color: AppColors.contentColorBlack, width: 0.1),
|
|
),
|
|
),
|
|
titlesData: FlTitlesData(
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 130,
|
|
getTitlesWidget: (value, meta) {
|
|
final i = value.toInt();
|
|
if (i < 0 || i >= widget.labels.length) {
|
|
return const SizedBox();
|
|
}
|
|
return SideTitleWidget(
|
|
meta: meta,
|
|
child: Text(
|
|
widget.labels[i],
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.end,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 50,
|
|
getTitlesWidget: (value, meta) {
|
|
return Transform.rotate(
|
|
angle: -3,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(right: 3.0),
|
|
child: Text(
|
|
compactNumber(value),
|
|
// value.toInt().toString(),
|
|
textAlign: TextAlign.end,
|
|
style: GoogleFonts.inter(fontSize: 10),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
leftTitles: const AxisTitles(),
|
|
topTitles: const AxisTitles(),
|
|
),
|
|
barTouchData: BarTouchData(
|
|
enabled: true,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
tooltipRoundedRadius: 8,
|
|
tooltipPadding: const EdgeInsets.all(8),
|
|
tooltipMargin: 8,
|
|
fitInsideHorizontally: true,
|
|
fitInsideVertically: true,
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
// Determine if this is current or previous month
|
|
final isCurrentMonth = rodIndex == 0;
|
|
final monthLabel = isCurrentMonth ? 'Current' : 'Previous';
|
|
|
|
// Calculate total for this rod
|
|
final total = rod.rodStackItems.isNotEmpty
|
|
? rod.rodStackItems
|
|
.map((s) => s.toY - s.fromY)
|
|
.reduce((a, b) => a + b)
|
|
: 0.0;
|
|
|
|
// Build detailed breakdown
|
|
String breakdown = '';
|
|
if (rod.rodStackItems.isNotEmpty) {
|
|
for (int i = 0; i < rod.rodStackItems.length; i++) {
|
|
final item = rod.rodStackItems[i];
|
|
final value = item.toY - item.fromY;
|
|
if (value > 0) {
|
|
// Try to get vehicle type name if provided
|
|
String vehicleName = 'Item ${i + 1}';
|
|
if (widget.vehicleTypeNames != null &&
|
|
groupIndex < widget.vehicleTypeNames!.length &&
|
|
i < widget.vehicleTypeNames![groupIndex].length) {
|
|
vehicleName = widget.vehicleTypeNames![groupIndex][i];
|
|
}
|
|
breakdown +=
|
|
'\n$vehicleName: ${value.toStringAsFixed(0)}';
|
|
}
|
|
}
|
|
}
|
|
|
|
return BarTooltipItem(
|
|
'$monthLabel Month\nTotal: ${total.toStringAsFixed(0)}$breakdown',
|
|
textAlign: TextAlign.start,
|
|
GoogleFonts.poppins(
|
|
color: Colors.white,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
touchCallback: (event, response) async {
|
|
if (response == null || response.spot == null) return;
|
|
|
|
// ✅ ONLY handle clicks
|
|
if (event is! FlTapUpEvent) return;
|
|
|
|
final spot = response.spot!;
|
|
final int groupIndex = spot.touchedBarGroupIndex;
|
|
final int rodIndex = spot.touchedRodDataIndex;
|
|
final int stackIndex = spot.touchedStackItemIndex!;
|
|
|
|
final label =
|
|
widget.vehicleTypeNames?[groupIndex][stackIndex] ?? 'Unknown';
|
|
final isCurrentMonth = rodIndex == 0;
|
|
final monthLabel = isCurrentMonth ? 'Current' : 'Previous';
|
|
final categoryLabel = widget.labels[groupIndex];
|
|
final categoryLabelID = widget.labelsId[groupIndex];
|
|
|
|
debugPrint(
|
|
'Clicked stack → $label -> $monthLabel ->$managerId ->$categoryLabel->$categoryLabelID',
|
|
);
|
|
|
|
await apiService.generatePerformanceDashboardExcel(
|
|
'StaffProduct',
|
|
label,
|
|
categoryLabelID,
|
|
monthLabel,
|
|
managerId,
|
|
);
|
|
},
|
|
),
|
|
|
|
barGroups: widget.productsList.asMap().entries.map((entry) {
|
|
int x = entry.key;
|
|
List<List<dynamic>> rods = entry.value;
|
|
|
|
List<BarChartRodStackItem> currentStack = [];
|
|
List<BarChartRodStackItem> previousStack = [];
|
|
|
|
double cumCurrent = 0;
|
|
// double cumPrev = 25000;
|
|
double cumPrev = 0;
|
|
|
|
// Current month
|
|
for (var item in rods[0]) {
|
|
double value = (item[1] - item[0]).toDouble();
|
|
currentStack.add(
|
|
BarChartRodStackItem(
|
|
cumCurrent,
|
|
cumCurrent + value,
|
|
item[2] as Color? ?? Colors.grey,
|
|
),
|
|
);
|
|
cumCurrent += value;
|
|
}
|
|
|
|
// Previous month
|
|
for (var item in rods[1]) {
|
|
double value = (item[1] - item[0]).toDouble();
|
|
previousStack.add(
|
|
BarChartRodStackItem(
|
|
cumPrev,
|
|
cumPrev + value,
|
|
item[2] as Color? ?? Colors.grey,
|
|
),
|
|
);
|
|
cumPrev += value;
|
|
}
|
|
|
|
return BarChartGroupData(
|
|
x: x,
|
|
groupVertically: false,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY: cumCurrent,
|
|
width: 16,
|
|
rodStackItems: currentStack,
|
|
borderRadius: BorderRadius.zero,
|
|
),
|
|
BarChartRodData(
|
|
toY: cumPrev,
|
|
width: 16,
|
|
rodStackItems: previousStack,
|
|
borderRadius: BorderRadius.zero,
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
maxY: maxY,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|