Major UI overhaul

This commit is contained in:
2026-02-10 23:11:45 +08:00
parent f4dea74394
commit 01c6b3537c
17 changed files with 2977 additions and 1219 deletions
+140 -80
View File
@@ -5,98 +5,158 @@ import 'package:go_router/go_router.dart';
import '../../models/office.dart';
import '../../providers/profile_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart';
class OfficesScreen extends ConsumerWidget {
class OfficesScreen extends ConsumerStatefulWidget {
const OfficesScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<OfficesScreen> createState() => _OfficesScreenState();
}
class _OfficesScreenState extends ConsumerState<OfficesScreen> {
final TextEditingController _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isAdmin = ref.watch(isAdminProvider);
final officesAsync = ref.watch(officesProvider);
return Scaffold(
body: ResponsiveBody(
maxWidth: 800,
child: !isAdmin
? const Center(child: Text('Admin access required.'))
: officesAsync.when(
data: (offices) {
if (offices.isEmpty) {
return const Center(child: Text('No offices found.'));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
child: Row(
children: [
Expanded(
child: Text(
'Office Management',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
),
TextButton.icon(
onPressed: () => context.go('/settings/users'),
icon: const Icon(Icons.group),
label: const Text('User access'),
),
],
return Stack(
children: [
ResponsiveBody(
maxWidth: double.infinity,
child: !isAdmin
? const Center(child: Text('Admin access required.'))
: officesAsync.when(
data: (offices) {
if (offices.isEmpty) {
return const Center(child: Text('No offices found.'));
}
final query = _searchController.text.trim().toLowerCase();
final filteredOffices = query.isEmpty
? offices
: offices
.where(
(office) =>
office.name.toLowerCase().contains(query) ||
office.id.toLowerCase().contains(query),
)
.toList();
final listBody = TasQAdaptiveList<Office>(
items: filteredOffices,
filterHeader: SizedBox(
width: 320,
child: TextField(
controller: _searchController,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Search name',
prefixIcon: Icon(Icons.search),
),
),
),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.only(bottom: 24),
itemCount: offices.length,
separatorBuilder: (_, index) =>
const SizedBox(height: 12),
itemBuilder: (context, index) {
final office = offices[index];
return ListTile(
leading: const Icon(Icons.apartment_outlined),
title: Text(office.name),
trailing: Wrap(
spacing: 8,
children: [
IconButton(
tooltip: 'Edit',
icon: const Icon(Icons.edit),
onPressed: () => _showOfficeDialog(
context,
ref,
office: office,
),
),
IconButton(
tooltip: 'Delete',
icon: const Icon(Icons.delete),
onPressed: () =>
_confirmDelete(context, ref, office),
),
],
),
);
},
columns: [
TasQColumn<Office>(
header: 'Office ID',
technical: true,
cellBuilder: (context, office) => Text(office.id),
),
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) =>
Center(child: Text('Failed to load offices: $error')),
TasQColumn<Office>(
header: 'Office Name',
cellBuilder: (context, office) => Text(office.name),
),
],
rowActions: (office) => [
IconButton(
tooltip: 'Edit',
icon: const Icon(Icons.edit),
onPressed: () =>
_showOfficeDialog(context, ref, office: office),
),
IconButton(
tooltip: 'Delete',
icon: const Icon(Icons.delete),
onPressed: () => _confirmDelete(context, ref, office),
),
],
mobileTileBuilder: (context, office, actions) {
return Card(
child: ListTile(
dense: true,
visualDensity: VisualDensity.compact,
leading: const Icon(Icons.apartment_outlined),
title: Text(office.name),
subtitle: MonoText('ID ${office.id}'),
trailing: Wrap(spacing: 8, children: actions),
),
);
},
);
return Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.center,
child: Text(
'Office Management',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () =>
context.go('/settings/users'),
icon: const Icon(Icons.group),
label: const Text('User access'),
),
),
],
),
),
Expanded(child: listBody),
],
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, _) =>
Center(child: Text('Failed to load offices: $error')),
),
),
if (isAdmin)
Positioned(
right: 16,
bottom: 16,
child: SafeArea(
child: FloatingActionButton.extended(
onPressed: () => _showOfficeDialog(context, ref),
icon: const Icon(Icons.add),
label: const Text('New Office'),
),
),
floatingActionButton: isAdmin
? FloatingActionButton.extended(
onPressed: () => _showOfficeDialog(context, ref),
icon: const Icon(Icons.add),
label: const Text('New Office'),
)
: null,
),
),
],
);
}