Files
meezi/mobile/meezi_app/lib/features/hr/attendance_screen.dart
T
soroush.asadi a85890f30a chore: Flutter mobile app, CI, and dev tooling
- mobile/: Flutter/Dart merchant mobile app skeleton
- .github/: GitHub Actions CI workflows
- .dockerignore: exclude host node_modules from build context
- .cursorrules: Cursor IDE project rules
- .claude/: Claude Code project settings and launch config

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-27 21:35:27 +03:30

166 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shamsi_date/shamsi_date.dart';
import '../../core/sync/sync_engine.dart';
import 'hr_api.dart';
import 'hr_providers.dart';
class AttendanceScreen extends ConsumerStatefulWidget {
const AttendanceScreen({super.key});
@override
ConsumerState<AttendanceScreen> createState() => _AttendanceScreenState();
}
class _AttendanceScreenState extends ConsumerState<AttendanceScreen> {
final _reasonController = TextEditingController();
Jalali? _leaveStart;
Jalali? _leaveEnd;
String? _message;
@override
void dispose() {
_reasonController.dispose();
super.dispose();
}
Future<void> _clock(bool isIn) async {
final session = ref.read(hrSessionProvider);
if (session == null) {
setState(() => _message = 'ابتدا وارد شوید');
return;
}
final api = ref.read(hrApiProvider);
final sync = ref.read(syncEngineProvider);
try {
if (isIn) {
await api.clockIn(cafeId: session.cafeId, employeeId: session.employeeId);
} else {
await api.clockOut(cafeId: session.cafeId, employeeId: session.employeeId);
}
ref.invalidate(todayShiftProvider);
setState(() => _message = isIn ? 'ورود ثبت شد' : 'خروج ثبت شد');
} catch (_) {
sync.enqueueAttendance(
action: isIn ? 'clock-in' : 'clock-out',
cafeId: session.cafeId,
employeeId: session.employeeId,
);
setState(() => _message = 'آفلاین ذخیره شد — پس از اتصال همگام‌سازی می‌شود');
}
}
Future<void> _submitLeave() async {
final session = ref.read(hrSessionProvider);
if (session == null || _leaveStart == null || _leaveEnd == null) return;
final api = ref.read(hrApiProvider);
try {
await api.submitLeave(
cafeId: session.cafeId,
employeeId: session.employeeId,
startDate: _formatDate(_leaveStart!),
endDate: _formatDate(_leaveEnd!),
reason: _reasonController.text,
);
setState(() => _message = 'درخواست مرخصی ثبت شد');
} catch (e) {
setState(() => _message = 'خطا در ثبت مرخصی');
}
}
String _formatDate(Jalali d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
@override
Widget build(BuildContext context) {
final shiftAsync = ref.watch(todayShiftProvider);
final todayJalali = Jalali.now();
return Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(title: const Text('حضور و غیاب')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'امروز: ${todayJalali.formatter.yyyyMMdd()}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
shiftAsync.when(
data: (shift) => Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('شیفت: ${shift?['label'] ?? ''}'),
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Text('شیفت امروز در دسترس نیست'),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: FilledButton(
onPressed: () => _clock(true),
child: const Text('ورود'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: () => _clock(false),
child: const Text('خروج'),
),
),
],
),
const Divider(height: 32),
Text('درخواست مرخصی', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
ListTile(
title: Text(_leaveStart == null ? 'از تاریخ' : _formatDate(_leaveStart!)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) setState(() => _leaveStart = Jalali.fromDateTime(picked));
},
),
ListTile(
title: Text(_leaveEnd == null ? 'تا تاریخ' : _formatDate(_leaveEnd!)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now().subtract(const Duration(days: 365)),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) setState(() => _leaveEnd = Jalali.fromDateTime(picked));
},
),
TextField(
controller: _reasonController,
decoration: const InputDecoration(labelText: 'دلیل'),
maxLines: 2,
),
const SizedBox(height: 12),
FilledButton(onPressed: _submitLeave, child: const Text('ثبت مرخصی')),
if (_message != null) ...[
const SizedBox(height: 16),
Text(_message!, style: TextStyle(color: Theme.of(context).colorScheme.primary)),
],
],
),
),
);
}
}