계획 변경에 따라 저울/아두이노 기반 픽업·반납·무단반출 감지를 없애고 순수 시간 기반 알림만 남김 (백엔드는 별도 저장소에서 이미 배포 완료). - 승인 시 현재 로그인한 선생님의 teacherId/teacherName을 서버에 함께 전송하여 저장하고, 대장에 "OOO 선생님이 허용했습니다" 표시 - 반납 시간 초과 알림은 승인한 선생님에게만 전송 (서버가 해당 선생님의 등록된 기기를 못 찾으면 전체 선생님에게 대체 발송) - 대장 카드의 상태 표시에서 IN_USE/RETURNED(픽업 대기) 문구 제거 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
296 lines
11 KiB
Dart
296 lines
11 KiB
Dart
// 👨🏫 교사 대시보드. 실시간 출석 확인, 학생 계정 관리 화면으로 가는 메뉴만 담당한다.
|
|
import 'package:flutter/material.dart';
|
|
import 'device_checkout_ledger_page.dart';
|
|
import 'login_screen.dart';
|
|
import 'teacher_attendance_page.dart';
|
|
import 'teacher_student_management_page.dart';
|
|
|
|
// ==========================================
|
|
// 👨🏫 4. 선생님 대시보드 (기존 3초 타이머 완벽 유지)
|
|
// ==========================================
|
|
class TeacherDashboard extends StatefulWidget {
|
|
final String? teacherId;
|
|
final String? teacherName;
|
|
|
|
const TeacherDashboard({super.key, this.teacherId, this.teacherName});
|
|
|
|
@override
|
|
State<TeacherDashboard> createState() => _TeacherDashboardState();
|
|
}
|
|
|
|
class _TeacherDashboardState extends State<TeacherDashboard> {
|
|
// 🧼 [수정] 사용하지 않던 _isLoading 변수를 삭제하여 경고를 완벽히 해결했습니다!
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// 다른 화면에서 null이 넘어왔을 때를 대비한 안전망 방탄 코드
|
|
final String displayName = widget.teacherName ?? "간편인증 선생";
|
|
final String displayId = widget.teacherId ?? "간편인증";
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.grey[100],
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'👨🏫 TEACHER PORTAL',
|
|
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1.2),
|
|
),
|
|
centerTitle: true,
|
|
backgroundColor: Colors.green[700], // 교사 전용 그린 테마 컬러
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.logout_rounded),
|
|
onPressed: () => Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const LoginScreen()),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 💳 [상단 배너 가이드] StudentDashboard와 100% 일치하는 프로필 카드 레이아웃
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green[700],
|
|
borderRadius: const BorderRadius.only(
|
|
bottomLeft: Radius.circular(32),
|
|
bottomRight: Radius.circular(32),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: CircleAvatar(
|
|
radius: 30,
|
|
backgroundColor: Colors.green[50],
|
|
child: Icon(
|
|
Icons.admin_panel_settings_rounded,
|
|
size: 32,
|
|
color: Colors.green[700],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 18),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'$displayName 님',
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'교직원 번호: $displayId | 교사 권한 활성화됨',
|
|
style: TextStyle(
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// 🏷️ [세로 바 타이틀 인디케이터] 구조 일치화
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 4,
|
|
height: 18,
|
|
decoration: BoxDecoration(
|
|
color: Colors.green[700],
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
const Text(
|
|
'스마트 교사용 관리 메뉴',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 📊 [그리드 레이아웃 메뉴] 시후의 카드 컴포넌트 스타일 적용
|
|
LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
// 🖥️ 웹(넓은 화면)은 한 줄에 5개씩, 폰(좁은 화면)은 기존 2개 그대로.
|
|
final bool isWide = constraints.maxWidth >= 800;
|
|
return Center(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(maxWidth: isWide ? 1300 : 700),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: GridView.count(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
crossAxisCount: isWide ? 5 : 2,
|
|
crossAxisSpacing: 16,
|
|
mainAxisSpacing: 16,
|
|
childAspectRatio: 0.95,
|
|
children: [
|
|
_buildModernCard(
|
|
icon: Icons.assignment_turned_in_rounded,
|
|
title: '실시간 출석 확인',
|
|
subtitle: '학생 제출 로그 모니터링',
|
|
color: Colors.blue,
|
|
onTap: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
const TeacherAttendancePage(),
|
|
),
|
|
),
|
|
),
|
|
_buildModernCard(
|
|
icon: Icons.manage_accounts_rounded,
|
|
title: '학생 계정 관리',
|
|
subtitle: '계정 추가 및 강제 리셋',
|
|
color: Colors.orange,
|
|
onTap: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
const TeacherStudentManagementPage(),
|
|
),
|
|
),
|
|
),
|
|
_buildModernCard(
|
|
icon: Icons.tablet_mac_rounded,
|
|
title: '스마트기기 반출 대장',
|
|
subtitle: '패드 반출 신청 승인/거절',
|
|
color: Colors.teal,
|
|
onTap: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => DeviceCheckoutLedgerPage(
|
|
teacherId: widget.teacherId,
|
|
teacherName: widget.teacherName,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 32),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 💎 [시후 대시보드 전용 카드 위젯 이식 완료]
|
|
Widget _buildModernCard({
|
|
required IconData icon,
|
|
required String title,
|
|
required String subtitle,
|
|
required Color color,
|
|
required VoidCallback onTap,
|
|
bool isActionButton = false,
|
|
bool isLoading = false,
|
|
}) {
|
|
return InkWell(
|
|
onTap: isLoading ? null : onTap,
|
|
borderRadius: BorderRadius.circular(24),
|
|
child: Ink(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.04),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Icon(icon, color: color, size: 28),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
if (isLoading)
|
|
const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
else if (isActionButton)
|
|
Icon(
|
|
Icons.touch_app_rounded,
|
|
size: 16,
|
|
color: color.withValues(alpha: 0.5),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[500],
|
|
height: 1.2,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|