feat: add more detailed history and group per day
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kooltab2/views/widgets/closed_tab_card.dart';
|
||||
import 'package:kooltab2/views/widgets/open_tab_history_card.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../app/router.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/closed_tab.dart';
|
||||
import '../utils/navigation.dart';
|
||||
import '../viewmodels/history_view_model.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
@@ -134,33 +137,15 @@ class _HistoryScreenViewState extends State<HistoryScreenView> with RouteAware {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: viewModel.closedTabs.isEmpty
|
||||
child:
|
||||
viewModel.openTabs.isEmpty && viewModel.closedTabs.isEmpty
|
||||
? _EmptyState()
|
||||
: ListView.separated(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||
itemCount:
|
||||
viewModel.closedTabs.length +
|
||||
(viewModel.hasMore || viewModel.isLoadingMore
|
||||
? 1
|
||||
: 0),
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == viewModel.closedTabs.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final closedTab = viewModel.closedTabs[index];
|
||||
|
||||
return ClosedTabCard(closedTab: closedTab);
|
||||
},
|
||||
: _HistoryList(
|
||||
openTabs: viewModel.openTabs,
|
||||
closedTabs: viewModel.closedTabs,
|
||||
hasMore: viewModel.hasMore,
|
||||
isLoadingMore: viewModel.isLoadingMore,
|
||||
scrollController: _scrollController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -171,6 +156,85 @@ class _HistoryScreenViewState extends State<HistoryScreenView> with RouteAware {
|
||||
}
|
||||
}
|
||||
|
||||
class _HistoryList extends StatelessWidget {
|
||||
final List<BarTab> openTabs;
|
||||
final List<ClosedTab> closedTabs;
|
||||
final bool hasMore;
|
||||
final bool isLoadingMore;
|
||||
final ScrollController scrollController;
|
||||
|
||||
const _HistoryList({
|
||||
required this.openTabs,
|
||||
required this.closedTabs,
|
||||
required this.hasMore,
|
||||
required this.isLoadingMore,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final openSectionCount = openTabs.isEmpty ? 0 : openTabs.length + 1;
|
||||
final closedSectionCount = closedTabs.isEmpty ? 0 : closedTabs.length + 1;
|
||||
final loadingCount = hasMore || isLoadingMore ? 1 : 0;
|
||||
|
||||
return ListView.separated(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||
itemCount: openSectionCount + closedSectionCount + loadingCount,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
if (openTabs.isNotEmpty) {
|
||||
if (index == 0) {
|
||||
return _HistorySectionHeader(title: l10n.openTabs);
|
||||
}
|
||||
|
||||
if (index <= openTabs.length) {
|
||||
return OpenTabHistoryCard(tab: openTabs[index - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
final closedIndex = index - openSectionCount;
|
||||
if (closedTabs.isNotEmpty) {
|
||||
if (closedIndex == 0) {
|
||||
return _HistorySectionHeader(title: l10n.closedTabs);
|
||||
}
|
||||
|
||||
if (closedIndex <= closedTabs.length) {
|
||||
return ClosedTabCard(closedTab: closedTabs[closedIndex - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2.5)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistorySectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _HistorySectionHeader({required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 2),
|
||||
child: Text(
|
||||
title.toUpperCase(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.7,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomerDropdown extends StatelessWidget {
|
||||
static const _allSentinel = r'$__all__$';
|
||||
|
||||
@@ -194,95 +258,134 @@ class _CustomerDropdown extends StatelessWidget {
|
||||
onSelected: (value) {
|
||||
onSelected(value == _allSentinel ? null : value);
|
||||
},
|
||||
offset: const Offset(0, 44),
|
||||
tooltip: l10n.customer,
|
||||
padding: EdgeInsets.zero,
|
||||
offset: const Offset(0, 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
color: Theme.of(context).cardTheme.color ?? scheme.surface,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: _allSentinel,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isFiltered ? Icons.people_outline : Icons.people_rounded,
|
||||
size: 18,
|
||||
color: isFiltered ? null : scheme.primary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
l10n.allCustomers,
|
||||
style: TextStyle(
|
||||
fontWeight: isFiltered ? FontWeight.w400 : FontWeight.w700,
|
||||
color: isFiltered ? null : scheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
height: 50,
|
||||
child: _CustomerMenuRow(
|
||||
icon: Icons.people_outline_rounded,
|
||||
label: l10n.allCustomers,
|
||||
selected: !isFiltered,
|
||||
),
|
||||
),
|
||||
if (customerNames.isNotEmpty) const PopupMenuDivider(height: 1),
|
||||
...customerNames.map(
|
||||
(name) => PopupMenuItem<String>(
|
||||
value: name,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
name == selectedCustomer
|
||||
? Icons.person_rounded
|
||||
: Icons.person_outline_rounded,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(name, overflow: TextOverflow.ellipsis)),
|
||||
],
|
||||
height: 50,
|
||||
child: _CustomerMenuRow(
|
||||
icon: name == selectedCustomer
|
||||
? Icons.person_rounded
|
||||
: Icons.person_outline_rounded,
|
||||
label: name,
|
||||
selected: name == selectedCustomer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: scheme.onSurface.withValues(alpha: 0.12)),
|
||||
color: isFiltered ? scheme.primary.withValues(alpha: 0.08) : null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_rounded,
|
||||
size: 18,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 144, maxWidth: 220),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isFiltered
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.5),
|
||||
? scheme.primary.withValues(alpha: 0.35)
|
||||
: scheme.onSurface.withValues(alpha: 0.12),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
selectedCustomer ?? l10n.customer,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: isFiltered
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.5),
|
||||
color: isFiltered ? scheme.primary.withValues(alpha: 0.08) : null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isFiltered
|
||||
? Icons.filter_alt_rounded
|
||||
: Icons.people_outline_rounded,
|
||||
size: 18,
|
||||
color: isFiltered
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedCustomer ?? l10n.allCustomers,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: isFiltered
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.65),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.arrow_drop_down_rounded,
|
||||
size: 18,
|
||||
color: isFiltered
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down_rounded,
|
||||
size: 18,
|
||||
color: isFiltered
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomerMenuRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool selected;
|
||||
|
||||
const _CustomerMenuRow({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: selected
|
||||
? scheme.primary
|
||||
: scheme.onSurface.withValues(alpha: 0.65),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
color: selected ? scheme.primary : scheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (selected) ...[
|
||||
const SizedBox(width: 10),
|
||||
Icon(Icons.check_rounded, size: 18, color: scheme.primary),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user