import 'dart:math' as math; import 'package:flutter/material.dart'; import '../theme/app_typography.dart'; import 'mono_text.dart'; class TasQColumn { const TasQColumn({ required this.header, required this.cellBuilder, this.technical = false, }); final String header; final Widget Function(BuildContext context, T item) cellBuilder; final bool technical; } typedef TasQMobileTileBuilder = Widget Function(BuildContext context, T item, List actions); typedef TasQRowActions = List Function(T item); typedef TasQRowTap = void Function(T item); class TasQAdaptiveList extends StatelessWidget { const TasQAdaptiveList({ super.key, required this.items, required this.columns, required this.mobileTileBuilder, this.rowActions, this.onRowTap, this.rowsPerPage = 25, this.tableHeader, this.filterHeader, this.summaryDashboard, }); final List items; final List> columns; final TasQMobileTileBuilder mobileTileBuilder; final TasQRowActions? rowActions; final TasQRowTap? onRowTap; final int rowsPerPage; final Widget? tableHeader; final Widget? filterHeader; final Widget? summaryDashboard; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final isMobile = constraints.maxWidth < 600; final hasBoundedHeight = constraints.hasBoundedHeight; if (isMobile) { final listView = ListView.separated( padding: const EdgeInsets.only(bottom: 24), itemCount: items.length, separatorBuilder: (context, index) => const SizedBox(height: 12), itemBuilder: (context, index) { final item = items[index]; final actions = rowActions?.call(item) ?? const []; return mobileTileBuilder(context, item, actions); }, ); final shrinkWrappedList = ListView.separated( padding: const EdgeInsets.only(bottom: 24), itemCount: items.length, separatorBuilder: (context, index) => const SizedBox(height: 12), itemBuilder: (context, index) { final item = items[index]; final actions = rowActions?.call(item) ?? const []; return mobileTileBuilder(context, item, actions); }, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), ); final summarySection = summaryDashboard == null ? null : [ SizedBox(width: double.infinity, child: summaryDashboard!), const SizedBox(height: 12), ]; final filterSection = filterHeader == null ? null : [ ExpansionTile( title: const Text('Filters'), children: [ Padding( padding: const EdgeInsets.only(bottom: 8), child: filterHeader!, ), ], ), const SizedBox(height: 8), ]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ...?summarySection, ...?filterSection, if (hasBoundedHeight) Expanded(child: listView), if (!hasBoundedHeight) shrinkWrappedList, ], ), ); } final dataSource = _TasQTableSource( context: context, items: items, columns: columns, rowActions: rowActions, onRowTap: onRowTap, ); final contentWidth = constraints.maxWidth * 0.8; final tableWidth = math.max( contentWidth, (columns.length + (rowActions == null ? 0 : 1)) * 140.0, ); final effectiveRowsPerPage = math.min( rowsPerPage, math.max(1, items.length), ); final tableWidget = SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox( width: tableWidth, child: PaginatedDataTable( header: tableHeader, rowsPerPage: effectiveRowsPerPage, columnSpacing: 20, horizontalMargin: 16, showCheckboxColumn: false, headingRowColor: WidgetStateProperty.resolveWith( (states) => Theme.of(context).colorScheme.surfaceContainer, ), columns: [ for (final column in columns) DataColumn(label: Text(column.header)), if (rowActions != null) const DataColumn(label: Text('Actions')), ], source: dataSource, ), ), ); final summarySection = summaryDashboard == null ? null : [ SizedBox(width: contentWidth, child: summaryDashboard!), const SizedBox(height: 12), ]; final filterSection = filterHeader == null ? null : [filterHeader!, const SizedBox(height: 12)]; return SingleChildScrollView( primary: hasBoundedHeight, child: Center( child: SizedBox( width: contentWidth, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [...?summarySection, ...?filterSection, tableWidget], ), ), ), ); }, ); } } class _TasQTableSource extends DataTableSource { _TasQTableSource({ required this.context, required this.items, required this.columns, required this.rowActions, required this.onRowTap, }); final BuildContext context; final List items; final List> columns; final TasQRowActions? rowActions; final TasQRowTap? onRowTap; @override DataRow? getRow(int index) { if (index >= items.length) return null; final item = items[index]; final cells = []; for (final column in columns) { final widget = column.cellBuilder(context, item); cells.add(DataCell(_applyTechnicalStyle(context, widget, column))); } if (rowActions != null) { final actions = rowActions!.call(item); cells.add( DataCell(Row(mainAxisSize: MainAxisSize.min, children: actions)), ); } return DataRow( onSelectChanged: onRowTap == null ? null : (_) => onRowTap!(item), cells: cells, ); } @override bool get isRowCountApproximate => false; @override int get rowCount => items.length; @override int get selectedRowCount => 0; } Widget _applyTechnicalStyle( BuildContext context, Widget child, TasQColumn column, ) { if (!column.technical) return child; if (child is Text && child.data != null) { return MonoText( child.data ?? '', style: child.style, maxLines: child.maxLines, overflow: child.overflow, textAlign: child.textAlign, ); } final mono = AppMonoText.of(context).body; return DefaultTextStyle.merge(style: mono, child: child); }