Reports
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user