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
+107 -119
View File
@@ -10,6 +10,8 @@ import '../../providers/profile_provider.dart';
import '../../providers/tasks_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../widgets/responsive_body.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/status_pill.dart';
class DashboardMetrics {
DashboardMetrics({
@@ -265,8 +267,63 @@ class DashboardScreen extends StatelessWidget {
return ResponsiveBody(
child: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 980;
final metricsColumn = Column(
final sections = <Widget>[
const SizedBox(height: 16),
_sectionTitle(context, 'IT Staff Pulse'),
const _StaffTable(),
const SizedBox(height: 20),
_sectionTitle(context, 'Core Daily KPIs'),
_cardGrid(context, [
_MetricCard(
title: 'New tickets today',
valueBuilder: (metrics) => metrics.newTicketsToday.toString(),
),
_MetricCard(
title: 'Closed today',
valueBuilder: (metrics) => metrics.closedToday.toString(),
),
_MetricCard(
title: 'Open tickets',
valueBuilder: (metrics) => metrics.openTickets.toString(),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'Task Flow'),
_cardGrid(context, [
_MetricCard(
title: 'Tasks created',
valueBuilder: (metrics) => metrics.tasksCreatedToday.toString(),
),
_MetricCard(
title: 'Tasks completed',
valueBuilder: (metrics) =>
metrics.tasksCompletedToday.toString(),
),
_MetricCard(
title: 'Open tasks',
valueBuilder: (metrics) => metrics.openTasks.toString(),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'TAT / Response'),
_cardGrid(context, [
_MetricCard(
title: 'Avg response',
valueBuilder: (metrics) => _formatDuration(metrics.avgResponse),
),
_MetricCard(
title: 'Avg triage',
valueBuilder: (metrics) => _formatDuration(metrics.avgTriage),
),
_MetricCard(
title: 'Longest response',
valueBuilder: (metrics) =>
_formatDuration(metrics.longestResponse),
),
]),
];
final content = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -284,113 +341,16 @@ class DashboardScreen extends StatelessWidget {
),
),
const _DashboardStatusBanner(),
_sectionTitle(context, 'Core Daily KPIs'),
_cardGrid(context, [
_MetricCard(
title: 'New tickets today',
valueBuilder: (metrics) => metrics.newTicketsToday.toString(),
),
_MetricCard(
title: 'Closed today',
valueBuilder: (metrics) => metrics.closedToday.toString(),
),
_MetricCard(
title: 'Open tickets',
valueBuilder: (metrics) => metrics.openTickets.toString(),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'TAT / Response'),
_cardGrid(context, [
_MetricCard(
title: 'Avg response',
valueBuilder: (metrics) =>
_formatDuration(metrics.avgResponse),
),
_MetricCard(
title: 'Avg triage',
valueBuilder: (metrics) => _formatDuration(metrics.avgTriage),
),
_MetricCard(
title: 'Longest response',
valueBuilder: (metrics) =>
_formatDuration(metrics.longestResponse),
),
]),
const SizedBox(height: 20),
_sectionTitle(context, 'Task Flow'),
_cardGrid(context, [
_MetricCard(
title: 'Tasks created',
valueBuilder: (metrics) =>
metrics.tasksCreatedToday.toString(),
),
_MetricCard(
title: 'Tasks completed',
valueBuilder: (metrics) =>
metrics.tasksCompletedToday.toString(),
),
_MetricCard(
title: 'Open tasks',
valueBuilder: (metrics) => metrics.openTasks.toString(),
),
]),
...sections,
],
);
final staffColumn = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
_sectionTitle(context, 'IT Staff Pulse'),
const _StaffTable(),
],
);
if (isWide) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.6,
),
child: metricsColumn,
),
const SizedBox(width: 20),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth * 0.35,
),
child: staffColumn,
),
],
),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 24),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
metricsColumn,
const SizedBox(height: 12),
staffColumn,
],
),
child: content,
),
),
);
@@ -415,21 +375,42 @@ class DashboardScreen extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final columns = width >= 900
? 3
: width >= 620
? 2
: 1;
if (width < 520) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (var i = 0; i < cards.length; i++) ...[
cards[i],
if (i < cards.length - 1) const SizedBox(height: 12),
],
],
);
}
final spacing = 12.0;
final cardWidth = (width - (columns - 1) * spacing) / columns;
return Wrap(
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
spacing: spacing,
runSpacing: spacing,
children: cards
.map((card) => SizedBox(width: cardWidth, child: card))
.toList(),
final minCardWidth = 220.0;
final totalWidth =
cards.length * minCardWidth + spacing * (cards.length - 1);
final fits = totalWidth <= width;
final cardWidth = fits
? (width - spacing * (cards.length - 1)) / cards.length
: minCardWidth;
final row = Row(
children: [
for (var i = 0; i < cards.length; i++) ...[
SizedBox(width: cardWidth, child: cards[i]),
if (i < cards.length - 1) const SizedBox(width: 12),
],
],
);
if (fits) {
return row;
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(width: totalWidth, child: row),
);
},
);
@@ -476,11 +457,12 @@ class _MetricCard extends ConsumerWidget {
error: (error, _) => 'Error',
);
return Container(
return AnimatedContainer(
duration: const Duration(milliseconds: 220),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
),
child: Column(
@@ -493,7 +475,7 @@ class _MetricCard extends ConsumerWidget {
).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
Text(
MonoText(
value,
style: Theme.of(
context,
@@ -587,7 +569,13 @@ class _StaffRow extends StatelessWidget {
child: Row(
children: [
Expanded(flex: 3, child: Text(row.name, style: valueStyle)),
Expanded(flex: 2, child: Text(row.status, style: valueStyle)),
Expanded(
flex: 2,
child: Align(
alignment: Alignment.centerLeft,
child: StatusPill(label: row.status),
),
),
Expanded(
flex: 2,
child: Text(