104 lines
2.8 KiB
Dart
104 lines
2.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:skeletonizer/skeletonizer.dart';
|
|
|
|
/// Wraps each report chart widget in a themed Card with a title, loading
|
|
/// skeleton and error state handling. Exposes a [GlobalKey] on the inner
|
|
/// [RepaintBoundary] so the PDF exporter can capture the rendered chart
|
|
/// as a raster image.
|
|
class ReportCardWrapper extends StatelessWidget {
|
|
const ReportCardWrapper({
|
|
super.key,
|
|
required this.title,
|
|
required this.child,
|
|
this.isLoading = false,
|
|
this.error,
|
|
this.height,
|
|
this.repaintBoundaryKey,
|
|
});
|
|
|
|
/// Title displayed in the card header.
|
|
final String title;
|
|
|
|
/// The chart widget to render inside the card.
|
|
final Widget child;
|
|
|
|
/// Whether to display a loading skeleton.
|
|
final bool isLoading;
|
|
|
|
/// An error message to display instead of the chart.
|
|
final String? error;
|
|
|
|
/// Optional fixed height for the chart area.
|
|
final double? height;
|
|
|
|
/// Key attached to the inner [RepaintBoundary] used for PDF image capture.
|
|
final GlobalKey? repaintBoundaryKey;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colors = theme.colorScheme;
|
|
final text = theme.textTheme;
|
|
|
|
Widget body;
|
|
if (error != null) {
|
|
body = Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.error_outline, color: colors.error, size: 32),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
error!,
|
|
style: text.bodyMedium?.copyWith(color: colors.error),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
} else if (isLoading) {
|
|
body = Skeletonizer(
|
|
enabled: true,
|
|
child: Container(
|
|
height: height ?? 200,
|
|
decoration: BoxDecoration(
|
|
color: colors.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
body = child;
|
|
}
|
|
|
|
final cardContent = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 4),
|
|
child: Text(title, style: text.titleSmall),
|
|
),
|
|
const Divider(height: 1),
|
|
Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: height != null ? SizedBox(height: height, child: body) : body,
|
|
),
|
|
],
|
|
);
|
|
|
|
final card = Card(
|
|
// Rely on CardTheme for elevation (M2 exception in hybrid system).
|
|
child: cardContent,
|
|
);
|
|
|
|
if (repaintBoundaryKey != null) {
|
|
return RepaintBoundary(key: repaintBoundaryKey, child: card);
|
|
}
|
|
return card;
|
|
}
|
|
}
|