하교 처리 시 출석 현황 표시도 함께 초기화
하교 버튼을 누르면 반출 허용뿐 아니라, 그 시각을 기준선으로 삼아 대시보드의 "출석 완료" 표시도 미제출로 초기화되도록 함. 실제 출석 기록(attendance 테이블)은 삭제하지 않고, 대시보드가 "오늘 출석"으로 인정하는 기준 시각만 옮기는 방식이라 데이터 손실 없이 표시만 리셋된다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
List<dynamic> _roster = []; // 전체 학생 명단 (/api/users)
|
List<dynamic> _roster = []; // 전체 학생 명단 (/api/users)
|
||||||
List<dynamic> _logs = []; // 출석 로그 (/api/logs)
|
List<dynamic> _logs = []; // 출석 로그 (/api/logs)
|
||||||
Map<String, dynamic> _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active)
|
Map<String, dynamic> _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active)
|
||||||
|
String? _dismissedAt; // 오늘 가장 최근 하교 처리 시각 (/api/dismissal/latest). 이 시각 이후 기록만 "오늘 출석"으로 표시.
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용
|
bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용
|
||||||
@@ -54,11 +55,13 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
http.get(Uri.parse('$baseUrl/api/users')),
|
http.get(Uri.parse('$baseUrl/api/users')),
|
||||||
http.get(Uri.parse('$baseUrl/api/logs')),
|
http.get(Uri.parse('$baseUrl/api/logs')),
|
||||||
http.get(Uri.parse('$baseUrl/api/violations/active')),
|
http.get(Uri.parse('$baseUrl/api/violations/active')),
|
||||||
|
http.get(Uri.parse('$baseUrl/api/dismissal/latest')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final usersRes = results[0];
|
final usersRes = results[0];
|
||||||
final logsRes = results[1];
|
final logsRes = results[1];
|
||||||
final violationsRes = results[2];
|
final violationsRes = results[2];
|
||||||
|
final dismissalRes = results[3];
|
||||||
|
|
||||||
if (usersRes.statusCode == 200 && logsRes.statusCode == 200) {
|
if (usersRes.statusCode == 200 && logsRes.statusCode == 200) {
|
||||||
final usersData = jsonDecode(utf8.decode(usersRes.bodyBytes));
|
final usersData = jsonDecode(utf8.decode(usersRes.bodyBytes));
|
||||||
@@ -72,10 +75,17 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? dismissedAt;
|
||||||
|
if (dismissalRes.statusCode == 200) {
|
||||||
|
final dismissalData = jsonDecode(utf8.decode(dismissalRes.bodyBytes));
|
||||||
|
dismissedAt = dismissalData['dismissedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_roster = usersData['users'] ?? [];
|
_roster = usersData['users'] ?? [];
|
||||||
_logs = logsData['logs'] ?? [];
|
_logs = logsData['logs'] ?? [];
|
||||||
_activeViolationsByStudentId = activeViolations;
|
_activeViolationsByStudentId = activeViolations;
|
||||||
|
_dismissedAt = dismissedAt;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -86,6 +96,24 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🏫 하교 처리: 전체 반출 허용 + 오늘 출석 표시 기준선을 지금 시각으로 옮긴다.
|
||||||
|
Future<void> _dismissAll() async {
|
||||||
|
try {
|
||||||
|
final response = await http.post(Uri.parse('$baseUrl/api/dismiss'));
|
||||||
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(result['message'] ?? '하교 처리되었습니다.')),
|
||||||
|
);
|
||||||
|
_fetchAll();
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('❌ 하교 처리 실패: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 🚨 선생님이 특정 학생에게 지금부터 N분간 반출을 허용한다.
|
/// 🚨 선생님이 특정 학생에게 지금부터 N분간 반출을 허용한다.
|
||||||
Future<void> _allowRemoval(String studentId, int minutes) async {
|
Future<void> _allowRemoval(String studentId, int minutes) async {
|
||||||
try {
|
try {
|
||||||
@@ -222,7 +250,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_setPermissionWindow(720); // 12시간 (사실상 하교~다음날까지 허용)
|
_dismissAll();
|
||||||
},
|
},
|
||||||
child: const Text('하교 처리'),
|
child: const Text('하교 처리'),
|
||||||
),
|
),
|
||||||
@@ -231,17 +259,19 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 오늘 날짜 기준으로 학번별 "가장 최근 출석 기록"만 남긴 맵을 만든다.
|
/// "오늘 출석"의 기준선. 하교 처리를 했다면 그 시각 이후, 안 했다면 오늘 자정부터.
|
||||||
Map<String, Map<String, String>> get _todaysCheckInsByStudentId {
|
Map<String, Map<String, String>> get _todaysCheckInsByStudentId {
|
||||||
final String todayPrefix = DateTime.now().toIso8601String().substring(
|
// "YYYY-MM-DD HH:MM:SS" 형태의 문자열끼리는 그대로 비교해도 시간 순서가 맞는다.
|
||||||
0,
|
final String cutoff =
|
||||||
10,
|
_dismissedAt ??
|
||||||
); // "YYYY-MM-DD"
|
'${DateTime.now().toIso8601String().substring(0, 10)} 00:00:00';
|
||||||
final Map<String, Map<String, String>> result = {};
|
final Map<String, Map<String, String>> result = {};
|
||||||
|
|
||||||
for (final log in _logs) {
|
for (final log in _logs) {
|
||||||
final String time = log['time']?.toString() ?? '';
|
final String time = log['time']?.toString() ?? '';
|
||||||
if (!time.startsWith(todayPrefix)) continue; // 오늘 기록이 아니면 무시
|
if (time.isEmpty || time.compareTo(cutoff) <= 0) {
|
||||||
|
continue; // 하교 처리 시각(또는 오늘 자정) 이전 기록이면 무시
|
||||||
|
}
|
||||||
|
|
||||||
final String studentId = log['student_id']?.toString() ?? '';
|
final String studentId = log['student_id']?.toString() ?? '';
|
||||||
if (studentId.isEmpty) continue;
|
if (studentId.isEmpty) continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user