init
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user