import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import '../../../app/theme.dart';
import '../../../core/identifiers/uuid.dart';
import '../../../core/auth/auth_store.dart';
import '../../../core/sync/master_data_sync_repository.dart';
import '../../../core/web/web_office_api.dart';
import '../data/employee_store.dart';
class EmployeesScreen extends StatelessWidget {
const EmployeesScreen({super.key});
Future<void> editEmployee(BuildContext context, [Employee? existing]) async {
final name = TextEditingController(text: existing?.name);
final email = TextEditingController(text: existing?.email);
var role = existing?.role ?? EmployeeRole.technician;
final roleOptions = existing?.role == EmployeeRole.owner
? const [EmployeeRole.owner]
: const [
EmployeeRole.office,
EmployeeRole.supervisor,
EmployeeRole.technician,
];
final saved = await showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(
existing == null ? 'Mitarbeiter anlegen' : 'Mitarbeiter bearbeiten',
),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: name,
decoration: const InputDecoration(
labelText: 'Name',
prefixIcon: Icon(Icons.person_outline_rounded),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 14),
TextField(
controller: email,
decoration: const InputDecoration(
labelText: 'E-Mail',
prefixIcon: Icon(Icons.mail_outline_rounded),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 14),
DropdownButtonFormField<EmployeeRole>(
initialValue: role,
decoration: const InputDecoration(
labelText: 'Rolle',
prefixIcon: Icon(Icons.badge_outlined),
border: OutlineInputBorder(),
),
items: roleOptions
.map(
(value) => DropdownMenuItem(
value: value,
child: Text(value.label),
),
)
.toList(),
onChanged: existing?.role == EmployeeRole.owner
? null
: (value) => value == null
? null
: setDialogState(() => role = value),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Abbrechen'),
),
FilledButton(
onPressed: () => name.text.trim().isEmpty
? null
: Navigator.pop(context, true),
child: const Text('Speichern'),
),
],
),
),
);
if (saved == true) {
await EmployeeStore.instance.save(
Employee(
id: existing?.id ?? newUuid(),
name: name.text.trim(),
email: email.text.trim(),
role: role,
active: existing?.active ?? true,
employeeNumber: existing?.employeeNumber ?? '',
phone: existing?.phone ?? '',
rowVersion: existing?.rowVersion ?? 0,
syncStatus: existing?.syncStatus ?? EntitySyncStatus.localOnly,
syncError: existing?.syncError,
),
);
}
name.dispose();
email.dispose();
}
Future<void> inviteEmployee(BuildContext context) async {
final firstName = TextEditingController();
final lastName = TextEditingController();
final email = TextEditingController();
final employeeNumber = TextEditingController();
final phone = TextEditingController();
var role = EmployeeRole.technician;
final saved = await showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Mitarbeiter einladen'),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: firstName,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Vorname',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: lastName,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Nachname',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: email,
onChanged: (_) => setState(() {}),
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'E-Mail',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: employeeNumber,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Personalnummer',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: phone,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Telefon (optional)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
DropdownButtonFormField<EmployeeRole>(
initialValue: role,
decoration: const InputDecoration(
labelText: 'Rolle',
border: OutlineInputBorder(),
),
items:
[
EmployeeRole.office,
EmployeeRole.supervisor,
EmployeeRole.technician,
]
.map(
(value) => DropdownMenuItem(
value: value,
child: Text(value.label),
),
)
.toList(),
onChanged: (value) =>
value == null ? null : setState(() => role = value),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Abbrechen'),
),
FilledButton(
onPressed:
firstName.text.trim().isEmpty ||
lastName.text.trim().isEmpty ||
email.text.trim().isEmpty ||
employeeNumber.text.trim().isEmpty
? null
: () => Navigator.pop(context, true),
child: const Text('Einladung erstellen'),
),
],
),
),
);
if (saved == true && context.mounted) {
try {
final result = await WebOfficeApi.instance.createInvitation({
'firstName': firstName.text.trim(),
'lastName': lastName.text.trim(),
'email': email.text.trim(),
'employeeNumber': employeeNumber.text.trim(),
'phone': phone.text.trim(),
'role': role.name,
});
final token = result['token'] as String? ?? '';
final link = Uri.base
.replace(fragment: 'invite=${Uri.encodeComponent(token)}')
.toString();
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Einladung erstellt'),
content: SelectableText(
'Sende diesen persönlichen Link an ${email.text.trim()}:\n\n$link\n\nDer Link ist sieben Tage gültig und kann nur einmal verwendet werden.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Schließen'),
),
],
),
);
await EmployeeStore.instance.initialize();
}
} on Object {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Die Einladung konnte nicht erstellt werden.'),
),
);
}
}
}
firstName.dispose();
lastName.dispose();
email.dispose();
employeeNumber.dispose();
phone.dispose();
}
Future<void> editPermissions(BuildContext context) async {
try {
final items = await WebOfficeApi.instance.listPermissions();
if (!context.mounted) return;
await showDialog<void>(
context: context,
builder: (dialogContext) => _PermissionsDialog(items: items),
);
} on Object {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Berechtigungen konnten nicht geladen werden.'),
),
);
}
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Mitarbeiter & Rollen'),
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
actions: [
if (kIsWeb &&
(AuthStore.instance.session?.role == 'owner' ||
AuthStore.instance.session?.role == 'office'))
IconButton(
tooltip: 'Berechtigungen verwalten',
onPressed: () => editPermissions(context),
icon: const Icon(Icons.security_outlined),
),
if (kIsWeb &&
(AuthStore.instance.session?.role == 'owner' ||
AuthStore.instance.session?.role == 'office'))
IconButton(
tooltip: 'Mitarbeiter einladen',
onPressed: () => inviteEmployee(context),
icon: const Icon(Icons.mail_outline_rounded),
),
],
),
floatingActionButton:
(AuthStore.instance.session == null ||
AuthStore.instance.session!.role == 'owner' ||
AuthStore.instance.session!.role == 'office')
? FloatingActionButton.extended(
onPressed: () => editEmployee(context),
icon: const Icon(Icons.person_add_alt_rounded),
label: const Text('Mitarbeiter'),
)
: null,
body: ListenableBuilder(
listenable: EmployeeStore.instance,
builder: (context, _) => ListView.separated(
padding: const EdgeInsets.all(24),
itemCount: EmployeeStore.instance.employees.length,
separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final employee = EmployeeStore.instance.employees[index];
return Card(
child: ListTile(
onTap:
AuthStore.instance.session == null ||
AuthStore.instance.session!.role == 'owner' ||
AuthStore.instance.session!.role == 'office'
? () => editEmployee(context, employee)
: null,
contentPadding: const EdgeInsets.all(18),
leading: CircleAvatar(
backgroundColor: employee.active
? AppColors.lime
: AppColors.canvas,
child: Text(
employee.name
.trim()
.split(' ')
.map((part) => part.isEmpty ? '' : part[0])
.take(2)
.join(),
style: const TextStyle(
color: AppColors.ink,
fontWeight: FontWeight.w800,
),
),
),
title: Text(
employee.name,
style: const TextStyle(fontWeight: FontWeight.w800),
),
subtitle: Text(
'${employee.role.label}${employee.email.isEmpty ? '' : ' · ${employee.email}'}\n${employee.syncStatus.label}${employee.syncError == null ? '' : ': ${employee.syncError}'}',
),
trailing: Switch(
value: employee.active,
onChanged:
AuthStore.instance.session == null ||
AuthStore.instance.session!.role == 'owner' ||
AuthStore.instance.session!.role == 'office'
? (value) =>
EmployeeStore.instance.setActive(employee, value)
: null,
),
),
);
},
),
),
);
}
class _PermissionsDialog extends StatefulWidget {
const _PermissionsDialog({required this.items});
final List<Map<String, dynamic>> items;
@override
State<_PermissionsDialog> createState() => _PermissionsDialogState();
}
class _PermissionsDialogState extends State<_PermissionsDialog> {
static const _roleLabels = {
'owner': 'Inhaber',
'office': 'Büro',
'supervisor': 'Bauleiter',
'technician': 'Monteur',
};
static const _permissionLabels = {
'tenant.manage': 'Betrieb verwalten',
'users.manage': 'Mitarbeiter verwalten',
'users.read': 'Mitarbeiter lesen',
'company_profile.read': 'Firmenprofil lesen',
'company_profile.update': 'Firmenprofil ändern',
'master_data.read': 'Stammdaten lesen',
'master_data.write': 'Stammdaten ändern',
'sync.read': 'Synchronisation lesen',
'sync.write': 'Synchronisation schreiben',
'reports.create': 'Nachweise erstellen',
'reports.read_all': 'Nachweise lesen',
'reports.update': 'Nachweise ändern',
'reports.finalize': 'Nachweise freigeben',
'reports.delete': 'Nachweise löschen',
'reports.send': 'Nachweise versenden',
'assets.read': 'Dateien lesen',
'assets.write': 'Dateien verwalten',
'permissions.manage': 'Berechtigungen verwalten',
'roles.manage': 'Rollen verwalten',
'audit.read': 'Änderungsprotokoll lesen',
};
late final List<Map<String, dynamic>> _items = widget.items
.map((item) => Map<String, dynamic>.from(item))
.toList();
String? _saving;
@override
Widget build(BuildContext context) {
final roles = ['owner', 'office', 'supervisor', 'technician'];
return AlertDialog(
title: const Text('Berechtigungen'),
content: SizedBox(
width: 560,
child: ListView(
shrinkWrap: true,
children: [
for (final role in roles) ...[
Padding(
padding: const EdgeInsets.only(top: 8, bottom: 4),
child: Text(
_roleLabels[role] ?? role,
style: const TextStyle(fontWeight: FontWeight.w800),
),
),
for (final item in _items.where((value) => value['role'] == role))
CheckboxListTile(
dense: true,
value: item['granted'] == true,
title: Text(
_permissionLabels[item['permission']] ??
'${item['permission']}',
),
subtitle: item['overridden'] == true
? const Text('Mandantenregel')
: null,
controlAffinity: ListTileControlAffinity.leading,
onChanged: role == 'owner' || _saving != null
? null
: (value) => _change(item, value ?? false),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Schließen'),
),
],
);
}
Future<void> _change(Map<String, dynamic> item, bool granted) async {
final key = '${item['role']}:${item['permission']}';
setState(() => _saving = key);
try {
final result = await WebOfficeApi.instance.updatePermission(
role: item['role'] as String,
permission: item['permission'] as String,
granted: granted,
);
setState(() {
item['granted'] = result['granted'] == true;
item['overridden'] = true;
});
} on Object {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Berechtigung konnte nicht gespeichert werden.'),
),
);
}
} finally {
if (mounted) setState(() => _saving = null);
}
}
}
