Implementert admin-funksjoner: Dashboard, Ansattliste, Avvikshåndtering og Profil-oppdatering

This commit is contained in:
steinhelge
2025-11-24 21:17:29 +01:00
parent feda00ac83
commit 237d56066b
7 changed files with 578 additions and 45 deletions
@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../providers/admin_provider.dart';
import '../../providers/auth_provider.dart';
import '../../providers/time_provider.dart';
import '../../models/deviation.dart';
class DeviationListScreen extends ConsumerStatefulWidget {
const DeviationListScreen({super.key});
@override
ConsumerState<DeviationListScreen> createState() => _DeviationListScreenState();
}
class _DeviationListScreenState extends ConsumerState<DeviationListScreen> {
bool _onlyUnacknowledged = true;
@override
Widget build(BuildContext context) {
final deviationsAsync = ref.watch(organizationDeviationsProvider(_onlyUnacknowledged));
return Scaffold(
appBar: AppBar(
title: const Text('Avvikshåndtering'),
actions: [
IconButton(
icon: Icon(_onlyUnacknowledged ? Icons.filter_alt : Icons.filter_alt_off),
onPressed: () {
setState(() {
_onlyUnacknowledged = !_onlyUnacknowledged;
});
},
tooltip: _onlyUnacknowledged ? 'Vis alle' : 'Vis kun åpne',
),
],
),
body: deviationsAsync.when(
data: (deviations) {
if (deviations.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle_outline, size: 64, color: Colors.green),
const SizedBox(height: 16),
Text(
_onlyUnacknowledged ? 'Ingen åpne avvik!' : 'Ingen avvik funnet',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
);
}
return ListView.builder(
itemCount: deviations.length,
itemBuilder: (context, index) {
final deviation = deviations[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ExpansionTile(
leading: Icon(
Icons.warning,
color: _getSeverityColor(deviation.severity),
),
title: Text(
deviation.description,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
DateFormat('dd.MM.yyyy HH:mm').format(deviation.detectedAt),
),
trailing: deviation.acknowledgedAt != null
? const Icon(Icons.check_circle, color: Colors.green)
: null,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_DetailRow(label: 'Type', value: _getTypeDisplayName(deviation.type)),
if (deviation.metadata.actualValue != null)
_DetailRow(
label: 'Faktisk verdi',
value: '${deviation.metadata.actualValue}',
),
if (deviation.metadata.limitValue != null)
_DetailRow(
label: 'Grenseverdi',
value: '${deviation.metadata.limitValue}',
),
const SizedBox(height: 16),
if (deviation.acknowledgedAt == null)
FilledButton(
onPressed: () => _showAcknowledgeDialog(context, deviation),
child: const Text('Kvitter ut avvik'),
)
else
Text(
'Kvittert ut av ${deviation.acknowledgedBy ?? "ukjent"} den ${DateFormat('dd.MM.yyyy HH:mm').format(deviation.acknowledgedAt!)}',
style: const TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
),
],
),
),
],
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Feil: $error')),
),
);
}
Future<void> _showAcknowledgeDialog(BuildContext context, Deviation deviation) async {
final commentController = TextEditingController();
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Kvitter ut avvik'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Er du sikker på at du vil kvittere ut dette avviket?'),
const SizedBox(height: 16),
TextField(
controller: commentController,
decoration: const InputDecoration(
labelText: 'Kommentar (valgfritt)',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Avbryt'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Kvitter ut'),
),
],
),
);
if (confirmed == true) {
try {
final user = ref.read(currentUserProvider);
if (user == null) return;
final timeService = ref.read(timeServiceProvider);
await timeService.acknowledgeDeviation(
deviationId: deviation.id,
userId: user.uid,
comment: commentController.text.isNotEmpty ? commentController.text : null,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Avvik kvittert ut')),
);
// Refresh list
ref.invalidate(organizationDeviationsProvider);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Feil: $e')),
);
}
}
}
}
Color _getSeverityColor(DeviationSeverity severity) {
switch (severity) {
case DeviationSeverity.warning:
return Colors.orange;
case DeviationSeverity.violation:
return Colors.red;
}
}
String _getTypeDisplayName(DeviationType type) {
switch (type) {
case DeviationType.dailyMax:
return 'Daglig maksgrense';
case DeviationType.weeklyMax:
return 'Ukentlig maksgrense';
case DeviationType.dailyRest:
return 'Daglig hviletid';
case DeviationType.weeklyRest:
return 'Ukentlig hviletid';
case DeviationType.averageExceeded:
return 'Gjennomsnittsberegning overskredet';
case DeviationType.nightWork:
return 'Nattarbeid';
}
}
}
class _DetailRow extends StatelessWidget {
final String label;
final String value;
const _DetailRow({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.grey)),
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
);
}
}