* Improved Task and Ticket Detail Screen
* Made Office assignment searchable in Task and Ticket Creation and Edit Screen
This commit is contained in:
+222
-94
@@ -58,69 +58,29 @@ class AppScaffold extends ConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
Image.asset('assets/tasq_ico.png', width: 28, height: 28),
|
||||
const SizedBox(width: 8),
|
||||
Text('TasQ'),
|
||||
],
|
||||
title: _AppBarTitle(
|
||||
location: location,
|
||||
showRail: showRail,
|
||||
isExtended: isExtended,
|
||||
),
|
||||
actions: [
|
||||
if (isStandard)
|
||||
PopupMenuButton<int>(
|
||||
tooltip: 'Account',
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
context.go('/profile');
|
||||
} else if (value == 1) {
|
||||
ref.read(authControllerProvider).signOut();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => const [
|
||||
PopupMenuItem(value: 0, child: Text('My profile')),
|
||||
PopupMenuItem(value: 1, child: Text('Sign out')),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ProfileAvatar(
|
||||
fullName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
radius: 16,
|
||||
heroTag: 'profile-avatar',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(displayName),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.expand_more),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Profile',
|
||||
onPressed: () => context.go('/profile'),
|
||||
icon: ProfileAvatar(
|
||||
fullName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
radius: 16,
|
||||
heroTag: 'profile-avatar',
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Sign out',
|
||||
onPressed: () => ref.read(authControllerProvider).signOut(),
|
||||
icon: const Icon(Icons.logout),
|
||||
),
|
||||
],
|
||||
),
|
||||
_ProfileMenuAnchor(
|
||||
isStandard: isStandard,
|
||||
displayName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
onProfile: () => context.go('/profile'),
|
||||
onSignOut: () => ref.read(authControllerProvider).signOut(),
|
||||
),
|
||||
const _NotificationBell(),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: showRail
|
||||
? null
|
||||
@@ -279,43 +239,35 @@ class _NotificationBell extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final unreadCount = ref.watch(unreadNotificationsCountProvider);
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return IconButton(
|
||||
tooltip: 'Notifications',
|
||||
onPressed: () => context.go('/notifications'),
|
||||
icon: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
const Icon(Icons.notifications_outlined),
|
||||
Positioned(
|
||||
right: -3,
|
||||
top: -3,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOutBack,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: (child, animation) => ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
),
|
||||
child: unreadCount > 0
|
||||
? Container(
|
||||
key: const ValueKey('badge'),
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.error,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: cs.surface,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(key: ValueKey('no-badge')),
|
||||
final String? badgeLabel = unreadCount == 0
|
||||
? null
|
||||
: unreadCount > 99
|
||||
? '99+'
|
||||
: unreadCount.toString();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: IconButton(
|
||||
key: notificationBellKey,
|
||||
tooltip: 'Notifications',
|
||||
onPressed: () => context.go('/notifications'),
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOutBack,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: (child, animation) =>
|
||||
ScaleTransition(scale: animation, child: child),
|
||||
child: Badge(
|
||||
key: ValueKey(badgeLabel),
|
||||
isLabelVisible: unreadCount > 0,
|
||||
label: badgeLabel != null ? Text(badgeLabel) : null,
|
||||
child: Icon(
|
||||
unreadCount > 0
|
||||
? Icons.notifications
|
||||
: Icons.notifications_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -649,3 +601,179 @@ int _currentIndex(String location, List<NavItem> items) {
|
||||
final overflowIndex = items.indexWhere((item) => item.isOverflow);
|
||||
return overflowIndex == -1 ? 0 : overflowIndex;
|
||||
}
|
||||
|
||||
String _routeToTitle(String location) {
|
||||
final path = location.split('?').first;
|
||||
return switch (path) {
|
||||
'/dashboard' => 'Dashboard',
|
||||
'/attendance' => 'Attendance',
|
||||
'/tickets' => 'Tickets',
|
||||
'/tasks' => 'Tasks',
|
||||
'/it-service-requests' => 'IT Service Requests',
|
||||
'/notifications' => 'Notifications',
|
||||
'/whereabouts' => 'Whereabouts',
|
||||
'/workforce' => 'Workforce',
|
||||
'/reports' => 'Reports',
|
||||
'/announcements' => 'Announcements',
|
||||
'/profile' => 'My Profile',
|
||||
'/settings/users' => 'Users',
|
||||
'/settings/offices' => 'Offices',
|
||||
'/settings/teams' => 'IT Teams',
|
||||
_ => '',
|
||||
};
|
||||
}
|
||||
|
||||
// ── Adaptive AppBar title ────────────────────────────────────────────────────
|
||||
|
||||
class _AppBarTitle extends StatelessWidget {
|
||||
const _AppBarTitle({
|
||||
required this.location,
|
||||
required this.showRail,
|
||||
required this.isExtended,
|
||||
});
|
||||
|
||||
final String location;
|
||||
final bool showRail;
|
||||
final bool isExtended;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final tt = Theme.of(context).textTheme;
|
||||
final pageTitle = _routeToTitle(location);
|
||||
|
||||
// Shared page-name suffix: divider line + title text
|
||||
List<Widget> pageSuffix = pageTitle.isEmpty
|
||||
? []
|
||||
: [
|
||||
const SizedBox(width: 12),
|
||||
Container(width: 1, height: 28, color: cs.outlineVariant),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
pageTitle,
|
||||
style: tt.titleMedium?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// Mobile: compact logo + brand name, no page title
|
||||
if (!showRail) {
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Image.asset('assets/tasq_ico.png', width: 28, height: 28),
|
||||
const SizedBox(width: 8),
|
||||
const Text('TasQ'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Desktop (extended rail already shows brand): logo + page name only
|
||||
if (isExtended) {
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Image.asset('assets/tasq_ico.png', width: 28, height: 28),
|
||||
...pageSuffix,
|
||||
]);
|
||||
}
|
||||
|
||||
// Tablet (collapsed rail): logo + brand + page name
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Image.asset('assets/tasq_ico.png', width: 28, height: 28),
|
||||
const SizedBox(width: 8),
|
||||
const Text('TasQ'),
|
||||
...pageSuffix,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── M3 profile menu anchor ───────────────────────────────────────────────────
|
||||
|
||||
class _ProfileMenuAnchor extends StatelessWidget {
|
||||
const _ProfileMenuAnchor({
|
||||
required this.isStandard,
|
||||
required this.displayName,
|
||||
required this.avatarUrl,
|
||||
required this.onProfile,
|
||||
required this.onSignOut,
|
||||
});
|
||||
|
||||
final bool isStandard;
|
||||
final String displayName;
|
||||
final String? avatarUrl;
|
||||
final VoidCallback onProfile;
|
||||
final VoidCallback onSignOut;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final tt = Theme.of(context).textTheme;
|
||||
|
||||
return MenuAnchor(
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.person_outline),
|
||||
onPressed: onProfile,
|
||||
child: const Text('My Profile'),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.logout),
|
||||
onPressed: onSignOut,
|
||||
child: const Text('Sign out'),
|
||||
),
|
||||
],
|
||||
builder: (context, controller, _) {
|
||||
void toggle() =>
|
||||
controller.isOpen ? controller.close() : controller.open();
|
||||
|
||||
if (isStandard) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: InkWell(
|
||||
onTap: toggle,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
ProfileAvatar(
|
||||
fullName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
radius: 14,
|
||||
heroTag: 'profile-avatar',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
displayName,
|
||||
style: tt.labelLarge?.copyWith(
|
||||
color: cs.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.expand_more, size: 18, color: cs.onSecondaryContainer),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Admin / non-standard: circular avatar icon button
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: IconButton(
|
||||
tooltip: 'Account',
|
||||
onPressed: toggle,
|
||||
icon: ProfileAvatar(
|
||||
fullName: displayName,
|
||||
avatarUrl: avatarUrl,
|
||||
radius: 16,
|
||||
heroTag: 'profile-avatar',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user