nrb 3.0.1
nrb: ^3.0.1 copied to clipboard
A highly responsive Flutter table and report builder for complex nested headers, editable data grids, and premium Excel/PDF exports.
example/lib/main.dart
import 'dart:io' show Directory, File, Platform;
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:nrb/nrb.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
void main() {
runApp(
const MaterialApp(
debugShowCheckedModeBanner: false,
home: EnterpriseDashboardScreen(),
),
);
}
class EnterpriseDashboardScreen extends StatefulWidget {
const EnterpriseDashboardScreen({super.key});
@override
State<EnterpriseDashboardScreen> createState() => _EnterpriseDashboardScreenState();
}
class _EnterpriseDashboardScreenState extends State<EnterpriseDashboardScreen> {
// --- BRAND COLORS ---
final Color primaryBrandColor = const Color(0xFF0B7A3E);
final Color secondaryAccent = const Color(0xFF1E88E5);
final Color warningAccent = const Color(0xFFFFB300);
// ===========================================================================
// DATA FOR GRID 1: Financial Report (Complex Nested Headers)
// ===========================================================================
final _financialHeaders = [
const HeaderCell(text: "Q4 Financials (USD)", span: 3, backgroundColor: Color(0xFF0B7A3E)),
const HeaderCell(text: "Operations", span: 2, backgroundColor: Color(0xFF0B7A3E)),
];
final _financialSubHeaders = [
const SubHeaderCell(text: "Target Rev.", backgroundColor: Color(0xFF0B7A3E), foregroundColor: Colors.white),
const SubHeaderCell(text: "Actual Rev.", backgroundColor: Color(0xFF0B7A3E), foregroundColor: Colors.white),
const SubHeaderCell(text: "Margin %", backgroundColor: Color(0xFF0B7A3E), foregroundColor: Colors.white),
const SubHeaderCell(text: "Active Clients", backgroundColor: Color(0xFF0B7A3E), foregroundColor: Colors.white),
const SubHeaderCell(text: "SLA Uptime", backgroundColor: Color(0xFF0B7A3E), foregroundColor: Colors.white),
];
final _financialLeftColumn = [
const TextCell(itemContent: "North America", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Europe", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Asia Pacific", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Latin America", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Middle East", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Africa", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Australia", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Global Ops", textAlignment: Alignment.centerLeft),
];
final _financialData = [
[
const TextCell(itemContent: "1200000", isAmount: true),
const TextCell(itemContent: "1350000", isAmount: true),
const TextCell(itemContent: "28%"),
const TextCell(itemContent: "145"),
const TextCell(itemContent: "99.99%"),
],
[
const TextCell(itemContent: "950000", isAmount: true),
const TextCell(itemContent: "920000", isAmount: true),
const TextCell(itemContent: "24%"),
const TextCell(itemContent: "112"),
const TextCell(itemContent: "99.95%"),
],
[
const TextCell(itemContent: "600000", isAmount: true),
const TextCell(itemContent: "780000", isAmount: true),
const TextCell(itemContent: "31%"),
const TextCell(itemContent: "88"),
const TextCell(itemContent: "99.90%"),
],
[
const TextCell(itemContent: "450000", isAmount: true),
const TextCell(itemContent: "410000", isAmount: true),
const TextCell(itemContent: "18%"),
const TextCell(itemContent: "55"),
const TextCell(itemContent: "99.80%"),
],
[
const TextCell(itemContent: "550000", isAmount: true),
const TextCell(itemContent: "620000", isAmount: true),
const TextCell(itemContent: "22%"),
const TextCell(itemContent: "70"),
const TextCell(itemContent: "99.99%"),
],
[
const TextCell(itemContent: "250000", isAmount: true),
const TextCell(itemContent: "280000", isAmount: true),
const TextCell(itemContent: "15%"),
const TextCell(itemContent: "30"),
const TextCell(itemContent: "99.50%"),
],
[
const TextCell(itemContent: "320000", isAmount: true),
const TextCell(itemContent: "350000", isAmount: true),
const TextCell(itemContent: "26%"),
const TextCell(itemContent: "42"),
const TextCell(itemContent: "99.99%"),
],
[
const TextCell(itemContent: "4320000", isAmount: true),
const TextCell(itemContent: "4710000", isAmount: true),
const TextCell(itemContent: "25%"),
const TextCell(itemContent: "542"),
const TextCell(itemContent: "99.95%"),
],
];
// ===========================================================================
// DATA FOR GRID 2: Employee Performance (Editable Data Grid)
// ===========================================================================
final _employeeHeaders = [
const HeaderCell(text: "Core Metrics", span: 2, backgroundColor: Color(0xFF1E88E5)),
const HeaderCell(text: "Manager Review (Editable)", span: 1, backgroundColor: Color(0xFF1E88E5)),
];
final _employeeSubHeaders = [
const SubHeaderCell(text: "Tasks Completed", backgroundColor: Color(0xFF1E88E5), foregroundColor: Colors.white),
const SubHeaderCell(text: "Efficiency Score", backgroundColor: Color(0xFF1E88E5), foregroundColor: Colors.white),
const SubHeaderCell(text: "Bonus Allocation", backgroundColor: Color(0xFF1E88E5), foregroundColor: Colors.white),
];
final _employeeLeftColumn = [
const TextCell(itemContent: "Alice Smith", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Bob Jones", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Charlie Brown", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Diana Prince", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Evan Wright", textAlignment: Alignment.centerLeft),
const TextCell(itemContent: "Fiona Gallagher", textAlignment: Alignment.centerLeft),
];
final _employeeData = [
[
const TextCell(itemContent: "124"),
const TextCell(itemContent: "94%"),
TextFieldCell(initialValue: "\$5,000", keyboardType: TextInputType.number),
],
[
const TextCell(itemContent: "98"),
const TextCell(itemContent: "88%"),
TextFieldCell(initialValue: "\$3,200", keyboardType: TextInputType.number),
],
[
const TextCell(itemContent: "145"),
const TextCell(itemContent: "98%"),
TextFieldCell(initialValue: "\$7,500", keyboardType: TextInputType.number),
],
[
const TextCell(itemContent: "110"),
const TextCell(itemContent: "91%"),
TextFieldCell(initialValue: "\$4,000", keyboardType: TextInputType.number),
],
[
const TextCell(itemContent: "85"),
const TextCell(itemContent: "82%"),
TextFieldCell(initialValue: "\$2,000", keyboardType: TextInputType.number),
],
[
const TextCell(itemContent: "132"),
const TextCell(itemContent: "96%"),
TextFieldCell(initialValue: "\$6,000", keyboardType: TextInputType.number),
],
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), // Light enterprise gray background
appBar: AppBar(
title: const Text(
'NRB Enterprise Dashboard',
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 1,
centerTitle: false,
),
body: LayoutBuilder(
builder: (context, constraints) {
final bool isDesktop = constraints.maxWidth > 1000;
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
// --- TOP ROW: KPI SUMMARY & GAUGES ---
if (isDesktop)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Circular Charts (Donut + Gauge)
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionTitle("Key Performance Indicators"),
const SizedBox(height: 12),
Wrap(
spacing: 16.0,
runSpacing: 16.0,
children: [
_buildKpiCard("Project Completion", 75.0, primaryBrandColor),
_buildKpiCard("Server Uptime", 99.9, secondaryAccent),
_buildGaugeCard("Collection Ach%", 88.0),
],
),
],
),
),
// Solid Metric Cards
Expanded(
flex: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionTitle("Today's Logistics"),
const SizedBox(height: 12),
Wrap(
spacing: 16.0,
runSpacing: 16.0,
children: const [
NrbMetricCard(
title: "Made",
value: "142",
backgroundColor: Color(0xFF1976D2), // Blue
),
NrbMetricCard(
title: "Pushed",
value: "89",
backgroundColor: Color(0xFF455A64), // Blue Grey
),
NrbMetricCard(
title: "Failed",
value: "56",
backgroundColor: Color(0xFF00796B), // Teal
),
],
),
],
),
),
],
)
else
// Mobile Layout for Top Row
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const _SectionTitle("Today's Logistics"),
const SizedBox(height: 12),
Wrap(
spacing: 12.0,
runSpacing: 12.0,
alignment: WrapAlignment.center,
children: const [
NrbMetricCard(title: "Scheduled", value: "142", backgroundColor: Color(0xFF1976D2)),
NrbMetricCard(title: "Created", value: "89", backgroundColor: Color(0xFF455A64)),
NrbMetricCard(title: "Synced", value: "56", backgroundColor: Color(0xFF00796B)),
NrbMetricCard(title: "Backlog", value: "12", backgroundColor: Color(0xFFD32F2F)),
],
),
const SizedBox(height: 32),
const _SectionTitle("Key Performance Indicators"),
const SizedBox(height: 12),
Wrap(
spacing: 16.0,
runSpacing: 16.0,
alignment: WrapAlignment.center,
children: [
_buildKpiCard("Project Completion", 75.0, primaryBrandColor),
_buildKpiCard("Server Uptime", 99.9, secondaryAccent),
_buildGaugeCard("Collection Ach%", 88.0),
],
),
],
),
const SizedBox(height: 32),
// --- SECTION 2: RESPONSIVE DASHBOARD LAYOUT (Charts + Grids) ---
if (isDesktop)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column: Analytics Charts
Expanded(
flex: 4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionTitle("Growth & Revenue Trends"),
const SizedBox(height: 12),
// Simple Bar Chart
_buildChartCard(
title: "Monthly Revenue (Last 6 Months)",
child: const NrbBarChart(
data: [120, 150, 180, 220, 300, 280],
labels: ["Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
height: 180,
width: double.infinity,
barColor: Color(0xFF0B7A3E),
barSpacing: 12.0,
barRadius: 6.0,
),
),
const SizedBox(height: 16),
// Simple Line Chart
_buildChartCard(
title: "Active Users Trend (10k+)",
child: const NrbLineChart(
data: [50, 55, 60, 80, 120, 110, 140, 180, 175, 210],
xAxisLabels: ["M1", "M2", "M3", "M4", "M5", "M6", "M7", "M8", "M9", "M10"],
height: 180,
width: double.infinity,
lineColor: Color(0xFF1E88E5),
showGrid: true,
lineWidth: 3.0,
),
),
const SizedBox(height: 16),
// Advanced Multi-Line Chart (Day Mode Card)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Averages (Last 7 Days)",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87),
),
const SizedBox(height: 24),
NrbMultiLineChart(
series: [
NrbLineSeries(
data: [64000, 60000, 55000, 50000, 45000, 40000, 35000],
color: Colors.blueAccent,
label: "Qty",
),
NrbLineSeries(
data: [33000, 31000, 29000, 27000, 24000, 22000, 19000],
color: Colors.greenAccent,
label: "Lines",
),
],
xAxisLabels: const ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
height: 220,
width: double.infinity,
),
],
),
),
const SizedBox(height: 16),
// Row for Pie Chart and Scatter Plot
Row(
children: [
Expanded(
child: _buildChartCard(
title: "Sales Distribution",
child: Center(
child: NrbPieChart(
size: 160,
slices: [
NrbPieSlice(value: 45, color: const Color(0xFF5C6BC0), label: "Lighting"),
NrbPieSlice(value: 30, color: const Color(0xFF26A69A), label: "Accessories"),
NrbPieSlice(value: 25, color: const Color(0xFFFFA726), label: "Others"),
],
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: _buildChartCard(
title: "Defect Correlation",
child: NrbScatterPlot(
height: 160,
points: [
NrbScatterPoint(1, 10), NrbScatterPoint(2, 12),
NrbScatterPoint(3, 15), NrbScatterPoint(4, 14),
NrbScatterPoint(5, 20), NrbScatterPoint(6, 18),
NrbScatterPoint(7, 25), NrbScatterPoint(8, 22),
NrbScatterPoint(9, 30),
],
pointColor: Colors.deepPurpleAccent,
),
),
),
],
),
const SizedBox(height: 16),
// Histogram
_buildChartCard(
title: "Age Distribution of Users",
child: NrbHistogram(
height: 160,
color: Colors.teal.shade400,
bins: [
NrbHistogramBin(label: "18-24", frequency: 150),
NrbHistogramBin(label: "25-34", frequency: 320),
NrbHistogramBin(label: "35-44", frequency: 280),
NrbHistogramBin(label: "45-54", frequency: 190),
NrbHistogramBin(label: "55+", frequency: 90),
],
),
),
],
),
),
const SizedBox(width: 24),
// Right Column: Data Grids
Expanded(
flex: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _SectionTitle("Regional Financial Report (Exportable)"),
const SizedBox(height: 12),
_buildGridContainer(
height: 380,
child: ReportMaker(
headers: _financialHeaders,
subHeaders: _financialSubHeaders,
leftColumn: _financialLeftColumn,
tableData: _financialData,
stickyHeaderLabel: "Region",
stickyHeaderBackgroundColor: primaryBrandColor,
stickyHeaderForegroundColor: Colors.white,
packageName: "com.inl.testapp",
apiKey: "fe2b22a6-fd19-4466-b2fa-ff7262a5993a",
enableDownload: true,
showDownloadFloatingButton: true,
reportName: "Financial_Report",
onDownloadCompleted: _handleDownloadSuccess,
),
),
const SizedBox(height: 24),
const _SectionTitle("Employee Performance (Editable Form)"),
const SizedBox(height: 12),
_buildGridContainer(
height: 300,
child: ReportMaker(
headers: _employeeHeaders,
subHeaders: _employeeSubHeaders,
leftColumn: _employeeLeftColumn,
tableData: _employeeData,
stickyHeaderLabel: "Employee",
stickyHeaderBackgroundColor: secondaryAccent,
stickyHeaderForegroundColor: Colors.white,
enableDownload: false,
showDownloadFloatingButton: false,
),
),
],
),
),
],
)
else
// Mobile Layout for Charts and Grids
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const _SectionTitle("Growth & Revenue Trends"),
const SizedBox(height: 12),
_buildChartCard(
title: "Monthly Revenue",
child: const NrbBarChart(
data: [120, 150, 180, 220, 300, 280],
labels: ["Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
height: 150,
width: double.infinity,
barColor: Color(0xFF0B7A3E),
),
),
const SizedBox(height: 16),
_buildChartCard(
title: "Active Users Trend",
child: const NrbLineChart(
data: [50, 55, 60, 80, 120, 110, 140, 180, 175, 210],
height: 150,
width: double.infinity,
showGrid: true,
lineColor: Color(0xFF1E88E5),
),
),
const SizedBox(height: 16),
// Multi-line chart for mobile (Day Mode Card)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Averages (Last 7 Days)",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87),
),
const SizedBox(height: 24),
NrbMultiLineChart(
series: [
NrbLineSeries(
data: [64000, 60000, 55000, 50000, 45000, 40000, 35000],
color: Colors.blueAccent,
label: "Qty",
),
NrbLineSeries(
data: [33000, 31000, 29000, 27000, 24000, 22000, 19000],
color: Colors.greenAccent,
label: "Lines",
),
],
xAxisLabels: const ["M", "T", "W", "T", "F", "S", "S"],
height: 180,
width: double.infinity,
),
],
),
),
const SizedBox(height: 16),
_buildChartCard(
title: "Sales Distribution",
child: Center(
child: NrbPieChart(
size: 160,
slices: [
NrbPieSlice(value: 45, color: const Color(0xFF5C6BC0), label: "Lighting"),
NrbPieSlice(value: 30, color: const Color(0xFF26A69A), label: "Accessories"),
NrbPieSlice(value: 25, color: const Color(0xFFFFA726), label: "Others"),
],
),
),
),
const SizedBox(height: 16),
_buildChartCard(
title: "Defect Correlation",
child: NrbScatterPlot(
height: 180,
points: [
NrbScatterPoint(1, 10), NrbScatterPoint(2, 12),
NrbScatterPoint(3, 15), NrbScatterPoint(4, 14),
NrbScatterPoint(5, 20), NrbScatterPoint(6, 18),
NrbScatterPoint(7, 25), NrbScatterPoint(8, 22),
NrbScatterPoint(9, 30),
],
pointColor: Colors.deepPurpleAccent,
),
),
const SizedBox(height: 16),
_buildChartCard(
title: "Age Distribution of Users",
child: NrbHistogram(
height: 160,
color: Colors.teal.shade400,
bins: [
NrbHistogramBin(label: "18-24", frequency: 150),
NrbHistogramBin(label: "25-34", frequency: 320),
NrbHistogramBin(label: "35-44", frequency: 280),
NrbHistogramBin(label: "45-54", frequency: 190),
NrbHistogramBin(label: "55+", frequency: 90),
],
),
),
const SizedBox(height: 32),
const Align(
alignment: Alignment.centerLeft,
child: _SectionTitle("Regional Financial Report (Exportable)"),
),
const SizedBox(height: 12),
_buildGridContainer(
height: 400,
child: ReportMaker(
headers: _financialHeaders,
subHeaders: _financialSubHeaders,
leftColumn: _financialLeftColumn,
tableData: _financialData,
stickyHeaderLabel: "Region",
stickyHeaderBackgroundColor: primaryBrandColor,
stickyHeaderForegroundColor: Colors.white,
packageName: "com.inl.testapp",
apiKey: "fe2b22a6-fd19-4466-b2fa-ff7262a5993a",
enableDownload: true,
showDownloadFloatingButton: true,
reportName: "Financial_Report",
onDownloadCompleted: _handleDownloadSuccess,
),
),
const SizedBox(height: 24),
const Align(
alignment: Alignment.centerLeft,
child: _SectionTitle("Employee Performance (Editable Form)"),
),
const SizedBox(height: 12),
_buildGridContainer(
height: 350,
child: ReportMaker(
headers: _employeeHeaders,
subHeaders: _employeeSubHeaders,
leftColumn: _employeeLeftColumn,
tableData: _employeeData,
stickyHeaderLabel: "Employee",
stickyHeaderBackgroundColor: secondaryAccent,
stickyHeaderForegroundColor: Colors.white,
enableDownload: false,
),
),
],
),
const SizedBox(height: 40), // Bottom padding
],
),
);
},
),
);
}
// --- HELPER WIDGETS ---
Widget _buildGridContainer({required double height, required Widget child}) {
return Container(
height: height,
width: double.infinity, // Ensure it takes full width available
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
clipBehavior: Clip.antiAlias,
child: child,
);
}
Widget _buildGaugeCard(String title, double percentage) {
return Container(
width: 140, // Matches KPI Card
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 70, // Matches Donut Chart Height
child: Center(
child: NrbGaugeChart(
value: percentage,
size: 100, // Scaled down to fit
strokeWidth: 12.0, // Thinner stroke for smaller size
showLabels: false, // Turn off outer labels so it fits the small card
segments: [
NrbGaugeSegment(startValue: 0, endValue: 50, color: const Color(0xFFF0716A)),
NrbGaugeSegment(startValue: 50, endValue: 75, color: const Color(0xFFFFCA3A)),
NrbGaugeSegment(startValue: 75, endValue: 100, color: const Color(0xFF67B28C)),
],
centerContent: Text(
"${percentage.toStringAsFixed(0)}%",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.grey.shade800,
),
),
),
),
),
const SizedBox(height: 16),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black54),
),
],
),
);
}
Widget _buildKpiCard(String title, double value, Color color) {
return Container(
width: 140, // Slightly narrower to fit 3 nicely
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NrbDonutChart(
value: value,
size: 70,
progressColor: color,
trackColor: Colors.grey.shade200,
strokeWidth: 8,
centerContent: Text(
"${value.toStringAsFixed(1)}%",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.grey.shade800,
),
),
),
const SizedBox(height: 16),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black54),
),
],
),
);
}
Widget _buildChartCard({required String title, required Widget child}) {
return Container(
width: double.infinity, // Charts span their column width
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha:0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87),
),
const SizedBox(height: 24),
child,
],
),
);
}
// --- DOWNLOAD HANDLER ---
Future<void> _handleDownloadSuccess(Uint8List bytes, String fileName) async {
if (!kIsWeb) {
bool hasPermission = false;
if (Platform.isAndroid) {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
if (androidInfo.version.sdkInt >= 33) {
hasPermission = true;
} else {
var status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
}
hasPermission = status.isGranted;
}
} else {
hasPermission = true;
}
if (hasPermission) {
try {
final directory = Directory('/storage/emulated/0/Download');
if (!await directory.exists()) {
await directory.create(recursive: true);
}
final file = File('${directory.path}/$fileName');
await file.writeAsBytes(bytes);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Report saved to: ${file.path}'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Save failed: $e'), backgroundColor: Colors.red),
);
}
}
}
}
}
}
class _SectionTitle extends StatelessWidget {
final String title;
const _SectionTitle(this.title);
@override
Widget build(BuildContext context) {
return Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
);
}
}