Major UI overhaul
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user