a85890f30a
- 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>
166 lines
5.9 KiB
Dart
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)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|