Enhanced material design 3 implementation

This commit is contained in:
2026-03-20 15:15:38 +08:00
parent 27ebb89052
commit 74197c525d
26 changed files with 1345 additions and 515 deletions
+19 -8
View File
@@ -11,6 +11,7 @@ import 'package:flutter_quill/flutter_quill.dart' as quill;
import '../../services/ai_service.dart';
import '../../utils/snackbar.dart';
import '../../widgets/app_page_header.dart';
import '../../widgets/gemini_animated_text_field.dart';
/// A simple admin-only web page allowing the upload of a new APK and the
@@ -478,13 +479,20 @@ class _AppUpdateScreenState extends ConsumerState<AppUpdateScreen> {
}
return Scaffold(
appBar: AppBar(title: const Text('APK Update Uploader')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const AppPageHeader(
title: 'App Update',
subtitle: 'Upload and manage APK releases',
),
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@@ -899,6 +907,9 @@ class _AppUpdateScreenState extends ConsumerState<AppUpdateScreen> {
),
),
),
);
),
],
),
);
}
}
+108 -102
View File
@@ -6,6 +6,8 @@ import '../../models/office.dart';
import '../../providers/profile_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../providers/services_provider.dart';
import '../../widgets/app_page_header.dart';
import '../../widgets/app_state_view.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
@@ -39,114 +41,118 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
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.'));
}
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const AppPageHeader(
title: 'Office Management',
subtitle: 'Create and manage office locations',
),
Expanded(
child: officesAsync.when(
data: (offices) {
if (offices.isEmpty) {
return const AppEmptyView(
icon: Icons.apartment_outlined,
title: 'No offices yet',
subtitle:
'Create an office to start assigning users and schedules.',
);
}
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 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),
),
),
),
columns: [
TasQColumn<Office>(
header: 'Office ID',
technical: true,
cellBuilder: (context, office) => Text(office.id),
),
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),
),
);
},
onRequestRefresh: () {
// For server-side pagination, update the query provider
ref.read(officesQueryProvider.notifier).state =
const OfficeQuery(offset: 0, limit: 50);
},
onPageChanged: (firstRow) {
ref
.read(officesQueryProvider.notifier)
.update((q) => q.copyWith(offset: firstRow));
},
isLoading: false,
);
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),
return TasQAdaptiveList<Office>(
items: filteredOffices,
filterHeader: SizedBox(
width: 320,
child: TextField(
controller: _searchController,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Search name',
prefixIcon: Icon(Icons.search),
),
),
),
columns: [
TasQColumn<Office>(
header: 'Office ID',
technical: true,
cellBuilder: (context, office) =>
Text(office.id),
),
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),
),
);
},
onRequestRefresh: () {
ref.read(officesQueryProvider.notifier).state =
const OfficeQuery(offset: 0, limit: 50);
},
onPageChanged: (firstRow) {
ref
.read(officesQueryProvider.notifier)
.update((q) => q.copyWith(offset: firstRow));
},
isLoading: false,
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, _) => AppErrorView(
error: error,
onRetry: () => ref.invalidate(officesProvider),
),
Expanded(child: listBody),
],
);
},
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (error, _) =>
Center(child: Text('Failed to load offices: $error')),
),
),
],
),
),
if (isAdmin)
+25 -22
View File
@@ -14,6 +14,8 @@ import '../../theme/app_surfaces.dart';
import '../../providers/user_offices_provider.dart';
import '../../utils/app_time.dart';
import '../../widgets/app_page_header.dart';
import '../../widgets/app_state_view.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart';
@@ -105,7 +107,14 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
assignmentsAsync.error ??
messagesAsync.error ??
'Unknown error';
return Center(child: Text('Failed to load data: $error'));
return AppErrorView(
error: error,
onRetry: () {
ref.invalidate(profilesProvider);
ref.invalidate(officesProvider);
ref.invalidate(userOfficesProvider);
},
);
}
final profiles = profilesAsync.valueOrNull ?? [];
@@ -124,7 +133,11 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
}
if (profiles.isEmpty) {
return const Center(child: Text('No users found.'));
return const AppEmptyView(
icon: Icons.people_outline,
title: 'No users found',
subtitle: 'Users who sign up will appear here.',
);
}
final query = _searchController.text.trim().toLowerCase();
@@ -269,26 +282,16 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
isLoading: false,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.center,
child: Text(
'User Management',
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
),
),
const SizedBox(height: 16),
Expanded(child: listBody),
],
),
return Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const AppPageHeader(
title: 'User Management',
subtitle: 'Manage user roles and office assignments',
),
Expanded(child: listBody),
],
);
}