This commit is contained in:
athul rd
2026-08-10 17:01:30 +05:30
commit 19a79d6126
16 changed files with 1790 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../api_service.dart';
import '../theme.dart';
class LeaveScreen extends StatefulWidget {
final ApiService api;
final VoidCallback onAuthError;
const LeaveScreen({super.key, required this.api, required this.onAuthError});
@override
State<LeaveScreen> createState() => _LeaveScreenState();
}
class _LeaveScreenState extends State<LeaveScreen> {
List<LeaveBalance> _balances = [];
List<LeaveRequest> _requests = [];
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final b = await widget.api.leaveBalances();
final r = await widget.api.myLeave();
if (mounted) setState(() { _balances = b; _requests = r; _loading = false; });
} on ApiException catch (e) {
if (e.status == 401) return widget.onAuthError();
if (mounted) setState(() => _loading = false);
}
}
Future<void> _apply() async {
final types = await widget.api.leaveTypes().catchError((_) => <LeaveType>[]);
if (!mounted || types.isEmpty) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('No leave types set up')));
return;
}
final ok = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
builder: (_) => _ApplySheet(api: widget.api, types: types),
);
if (ok == true) _load();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: _apply, backgroundColor: AppColors.emerald,
icon: const Icon(Icons.add), label: const Text('Apply'),
),
backgroundColor: Colors.transparent,
body: _loading
? const Center(child: CircularProgressIndicator(color: AppColors.emerald))
: RefreshIndicator(
onRefresh: _load,
color: AppColors.emerald,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 90),
children: [
const Text('Balances', style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.navy)),
const SizedBox(height: 8),
if (_balances.isEmpty)
const Text('No leave types.', style: TextStyle(color: AppColors.muted))
else
..._balances.map((b) => Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(b.leaveTypeName, style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text('${b.used} used of ${b.allocated}'),
trailing: Text('${b.available}',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: AppColors.emerald)),
),
)),
const SizedBox(height: 16),
const Text('My requests', style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.navy)),
const SizedBox(height: 8),
if (_requests.isEmpty)
const Text('No requests yet.', style: TextStyle(color: AppColors.muted))
else
..._requests.map((r) => Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text('${r.leaveTypeName ?? 'Leave'} · ${r.days ?? ''} day(s)'),
subtitle: Text('${r.fromDate}${r.toDate}${r.reason != null ? '\n${r.reason}' : ''}'),
isThreeLine: r.reason != null,
trailing: _statusChip(r.status),
),
)),
],
),
),
);
}
}
Widget _statusChip(String s) {
final c = s == 'APPROVED' ? AppColors.emerald : s == 'REJECTED' ? AppColors.danger : AppColors.gold;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: c.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(20)),
child: Text(s, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: c)),
);
}
class _ApplySheet extends StatefulWidget {
final ApiService api;
final List<LeaveType> types;
const _ApplySheet({required this.api, required this.types});
@override
State<_ApplySheet> createState() => _ApplySheetState();
}
class _ApplySheetState extends State<_ApplySheet> {
String? _type;
DateTime? _from, _to;
final _reason = TextEditingController();
bool _busy = false;
Future<void> _pick(bool from) async {
final d = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now().subtract(const Duration(days: 30)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (d != null) setState(() => from ? _from = d : _to = d);
}
Future<void> _submit() async {
if (_type == null || _from == null || _to == null) return;
setState(() => _busy = true);
final fmt = DateFormat('yyyy-MM-dd');
try {
await widget.api.applyLeave(_type!, fmt.format(_from!), fmt.format(_to!), _reason.text);
if (mounted) Navigator.pop(context, true);
} on ApiException catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message)));
setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
final fmt = DateFormat('dd MMM yyyy');
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
const Text('Apply for leave', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.navy)),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
initialValue: _type,
decoration: const InputDecoration(labelText: 'Leave type'),
items: widget.types.map((t) => DropdownMenuItem(value: t.gid, child: Text(t.name))).toList(),
onChanged: (v) => setState(() => _type = v),
),
const SizedBox(height: 12),
Row(children: [
Expanded(child: OutlinedButton(onPressed: () => _pick(true), child: Text(_from == null ? 'From' : fmt.format(_from!)))),
const SizedBox(width: 10),
Expanded(child: OutlinedButton(onPressed: () => _pick(false), child: Text(_to == null ? 'To' : fmt.format(_to!)))),
]),
const SizedBox(height: 12),
TextField(controller: _reason, decoration: const InputDecoration(labelText: 'Reason'), maxLines: 2),
const SizedBox(height: 16),
FilledButton(
onPressed: _busy ? null : _submit,
child: _busy
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Text('Submit request'),
),
]),
),
);
}
}