120 lines
4.9 KiB
Dart
120 lines
4.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../api_service.dart';
|
|
import '../theme.dart';
|
|
|
|
class TeamScreen extends StatefulWidget {
|
|
final ApiService api;
|
|
final VoidCallback onAuthError;
|
|
const TeamScreen({super.key, required this.api, required this.onAuthError});
|
|
|
|
@override
|
|
State<TeamScreen> createState() => _TeamScreenState();
|
|
}
|
|
|
|
class _TeamScreenState extends State<TeamScreen> {
|
|
List<TeamMember> _team = [];
|
|
List<LeaveRequest> _pending = [];
|
|
bool _loading = true;
|
|
bool _busy = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
try {
|
|
final t = await widget.api.team();
|
|
final p = await widget.api.teamLeave();
|
|
if (mounted) setState(() { _team = t; _pending = p; _loading = false; });
|
|
} on ApiException catch (e) {
|
|
if (e.status == 401) return widget.onAuthError();
|
|
if (mounted) setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _decide(String gid, bool approve) async {
|
|
setState(() => _busy = true);
|
|
try {
|
|
approve ? await widget.api.approve(gid) : await widget.api.reject(gid);
|
|
await _load();
|
|
} on ApiException catch (e) {
|
|
if (e.status == 401) return widget.onAuthError();
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message)));
|
|
} finally {
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
Color _sc(String? s) => statusColor(s);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_loading) return const Center(child: CircularProgressIndicator(color: AppColors.emerald));
|
|
return RefreshIndicator(
|
|
onRefresh: _load,
|
|
color: AppColors.emerald,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
if (_pending.isNotEmpty) ...[
|
|
const Text('Pending approvals', style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.navy)),
|
|
const SizedBox(height: 8),
|
|
..._pending.map((r) => Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(children: [
|
|
Expanded(
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(r.employeeName ?? '—', style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.navy)),
|
|
Text('${r.leaveTypeName ?? 'Leave'} · ${r.days ?? ''}d · ${r.fromDate} → ${r.toDate}',
|
|
style: const TextStyle(fontSize: 12, color: AppColors.muted)),
|
|
]),
|
|
),
|
|
IconButton(
|
|
onPressed: _busy ? null : () => _decide(r.gid, true),
|
|
icon: const Icon(Icons.check_circle_outline, color: AppColors.emerald),
|
|
),
|
|
IconButton(
|
|
onPressed: _busy ? null : () => _decide(r.gid, false),
|
|
icon: const Icon(Icons.cancel_outlined, color: AppColors.danger),
|
|
),
|
|
]),
|
|
),
|
|
)),
|
|
const SizedBox(height: 16),
|
|
],
|
|
const Text('Team today', style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.navy)),
|
|
const SizedBox(height: 8),
|
|
if (_team.isEmpty)
|
|
const Text('No team members.', style: TextStyle(color: AppColors.muted))
|
|
else
|
|
..._team.map((m) => Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ListTile(
|
|
leading: CircleAvatar(
|
|
backgroundColor: AppColors.emerald.withValues(alpha: 0.12),
|
|
child: Text(_initials(m.employeeName), style: const TextStyle(color: AppColors.emerald, fontWeight: FontWeight.bold, fontSize: 13)),
|
|
),
|
|
title: Text(m.employeeName, style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
subtitle: Text('In ${m.checkIn ?? '—'} · Out ${m.checkOut ?? '—'}${m.hours != null ? ' · ${m.hours}h' : ''}'),
|
|
trailing: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(color: _sc(m.status).withValues(alpha: 0.12), borderRadius: BorderRadius.circular(20)),
|
|
child: Text((m.status).replaceAll('_', ' '), style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: _sc(m.status))),
|
|
),
|
|
),
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _initials(String name) {
|
|
final p = name.trim().split(RegExp(r'\s+')).where((x) => x.isNotEmpty).toList();
|
|
return p.isEmpty ? '?' : p.take(2).map((x) => x[0].toUpperCase()).join();
|
|
}
|
|
}
|