diff --git a/lib/ui/teacher_attendance_page.dart b/lib/ui/teacher_attendance_page.dart index 81375c4..613159e 100644 --- a/lib/ui/teacher_attendance_page.dart +++ b/lib/ui/teacher_attendance_page.dart @@ -205,66 +205,36 @@ class _TeacherAttendancePageState extends State { return Scaffold( backgroundColor: AppPalette.mist, + // πŸͺΆ κ²€μ • λ°°λ„ˆ λŒ€μ‹  κ°€μš΄λ°μ— λ‘₯κ·Ό 제λͺ© μ•Œμ•½λ§Œ λ„μš°κ³ , λ‚˜λ¨Έμ§€ κΈ°λŠ₯은 μ „λΆ€ + // "μžμŠ΅μ‹€ μ‹œκ°„ μ„€μ • 메뉴"(λŸ°μΉ˜νŒ¨λ“œ μŠ€νƒ€μΌ μ˜€λ²„λ ˆμ΄) ν•˜λ‚˜λ‘œ λͺ¨μ€λ‹€. + // ν°μ—μ„œλŠ” μ‚¬μ΄λ“œλ°”κ°€ μ—†μœΌλ‹ˆ μ—¬κΈ° μ•„μ΄μ½˜μœΌλ‘œλ„ 같은 메뉴λ₯Ό μ—΄ 수 있게 λ‘”λ‹€. appBar: AppBar( - title: const Text( - 'μ‹€μ‹œκ°„ μΆœμ„ ν˜„ν™©', - style: TextStyle(fontWeight: FontWeight.bold), - ), - backgroundColor: AppPalette.ink, - foregroundColor: Colors.white, + backgroundColor: Colors.transparent, + foregroundColor: AppPalette.ink, elevation: 0, + centerTitle: true, + title: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: AppPalette.ink, + borderRadius: BorderRadius.circular(999), + ), + child: const Text( + 'μ‹€μ‹œκ°„ μΆœμ„ ν˜„ν™©', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ), actions: [ IconButton( - onPressed: (_controller.isLoading || _controller.isRefreshing) - ? null - : _controller.manualRefresh, - icon: _controller.isRefreshing - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.refresh_rounded), - tooltip: 'μƒˆλ‘œκ³ μΉ¨', + icon: const Icon(Icons.more_horiz_rounded), + tooltip: 'μžμŠ΅μ‹€ μ‹œκ°„ μ„€μ • 메뉴', + onPressed: _showTimeSettingsMenu, ), - TextButton.icon( - onPressed: _showAttendanceTimeDialog, - icon: const Icon( - Icons.access_time_rounded, - color: Colors.white, - ), - label: Text( - _controller.attendanceTime != null - ? 'μΆœμ„μ‹œκ°„ ${_controller.attendanceTime}' - : 'μžμŠ΅μ‹€ μΆœμ„μ‹œκ°„ μ„€μ •', - style: const TextStyle(color: Colors.white), - ), - ), - TextButton.icon( - onPressed: _showPermissionWindowDialog, - icon: const Icon(Icons.timer_outlined, color: Colors.white), - label: const Text( - '반좜 ν—ˆμš© μ‹œκ°„ μ„€μ •', - style: TextStyle(color: Colors.white), - ), - ), - TextButton.icon( - onPressed: _showDismissalConfirmDialog, - icon: const Icon(Icons.school_rounded, color: Colors.white), - label: const Text('ν•˜κ΅', style: TextStyle(color: Colors.white)), - ), - IconButton( - onPressed: _showTestResetConfirmDialog, - icon: const Icon( - Icons.bug_report_outlined, - color: Colors.white70, - ), - tooltip: 'ν…ŒμŠ€νŠΈμš©: ν—ˆμš©μ‹œκ°„ μ΄ˆκΈ°ν™”', - ), - const SizedBox(width: 8), + const SizedBox(width: 4), ], ), body: _controller.isLoading @@ -567,11 +537,49 @@ class _TeacherAttendancePageState extends State { _sidebarStatPill('λ―ΈμΆœμ„ ν•™μƒμˆ˜', absent, 'ABSENT'), const SizedBox(height: 20), _sidebarFilterButton(), + const SizedBox(height: 10), + _sidebarTimeSettingsButton(), ], ), ); } + Widget _sidebarTimeSettingsButton() { + return InkWell( + onTap: _showTimeSettingsMenu, + borderRadius: BorderRadius.circular(28), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), + decoration: BoxDecoration( + color: AppPalette.paper, + borderRadius: BorderRadius.circular(28), + border: Border.all(color: AppPalette.sage), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.access_time_rounded, + size: 18, + color: AppPalette.ink, + ), + const SizedBox(width: 8), + const Flexible( + child: Text( + 'μžμŠ΅μ‹€ μ‹œκ°„ μ„€μ • 메뉴', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + color: AppPalette.ink, + ), + ), + ), + ], + ), + ), + ); + } + Widget _sidebarStatPill(String label, int count, String filterValue) { final bool selected = _controller.filterType == filterValue; return InkWell( @@ -705,6 +713,42 @@ class _TeacherAttendancePageState extends State { ); } + // πŸš€ "μžμŠ΅μ‹€ μ‹œκ°„ μ„€μ • 메뉴" β€” μƒˆλ‘œκ³ μΉ¨/μΆœμ„μ‹œκ°„/λ°˜μΆœν—ˆμš©/ν•˜κ΅/ν…ŒμŠ€νŠΈλ¦¬μ…‹μ„ + // μ „λΆ€ μ—¬κΈ° ν•˜λ‚˜λ‘œ λͺ¨μ•„μ„œ, 메인 λŒ€μ‹œλ³΄λ“œμ˜ λ‘œκ·Έμ•„μ›ƒ/μ„€μ • 메뉴와 λ˜‘κ°™μ€ + // λŸ°μΉ˜νŒ¨λ“œ μŠ€νƒ€μΌ(λ°°κ²½ λΈ”λŸ¬ μœ μ§€) μ˜€λ²„λ ˆμ΄λ‘œ λ„μš΄λ‹€. + Future _showTimeSettingsMenu() async { + final action = await showLaunchpadMenu( + context: context, + builder: (context) => Center( + child: _TimeSettingsMenuCard( + isRefreshing: _controller.isRefreshing, + attendanceTimeLabel: _controller.attendanceTime != null + ? 'μΆœμ„μ‹œκ°„ ${_controller.attendanceTime}' + : 'μžμŠ΅μ‹€ μΆœμ„μ‹œκ°„ μ„€μ •', + onPick: (value) => Navigator.pop(context, value), + ), + ), + ); + if (!mounted || action == null) return; + switch (action) { + case 'refresh': + _controller.manualRefresh(); + break; + case 'attendance_time': + _showAttendanceTimeDialog(); + break; + case 'permission_window': + _showPermissionWindowDialog(); + break; + case 'dismissal': + _showDismissalConfirmDialog(); + break; + case 'test_reset': + _showTestResetConfirmDialog(); + break; + } + } + // πŸͺ‘ μ’Œμ„ν‘œμ²˜λŸΌ μ΄˜μ΄˜ν•˜κ²Œ 학생 ν•œ λͺ…씩을 μž‘μ€ 칸에 λ‹΄λŠ” κ·Έλ¦¬λ“œ. Widget _buildDenseSeatGrid(List> students) { if (students.isEmpty) { @@ -915,3 +959,116 @@ class _TeacherAttendancePageState extends State { ); } } + +// πŸš€ "μžμŠ΅μ‹€ μ‹œκ°„ μ„€μ • 메뉴" μΉ΄λ“œ β€” 메인 λŒ€μ‹œλ³΄λ“œ λ‘œκ·Έμ•„μ›ƒ/μ„€μ • 메뉴와 같은 ν†€μœΌλ‘œ 톡일. +class _TimeSettingsMenuCard extends StatelessWidget { + final bool isRefreshing; + final String attendanceTimeLabel; + final ValueChanged onPick; + + const _TimeSettingsMenuCard({ + required this.isRefreshing, + required this.attendanceTimeLabel, + required this.onPick, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: AppPalette.paper, + elevation: 8, + shadowColor: Colors.black.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(20), + child: IntrinsicWidth( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 220), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TimeMenuRow( + icon: isRefreshing ? null : Icons.refresh_rounded, + label: 'μƒˆλ‘œκ³ μΉ¨', + onTap: () => onPick('refresh'), + ), + const Divider(height: 1, color: AppPalette.sage), + _TimeMenuRow( + icon: Icons.access_time_rounded, + label: attendanceTimeLabel, + onTap: () => onPick('attendance_time'), + ), + const Divider(height: 1, color: AppPalette.sage), + _TimeMenuRow( + icon: Icons.timer_outlined, + label: '반좜 ν—ˆμš© μ‹œκ°„ μ„€μ •', + onTap: () => onPick('permission_window'), + ), + const Divider(height: 1, color: AppPalette.sage), + _TimeMenuRow( + icon: Icons.school_rounded, + label: 'ν•˜κ΅ 처리', + onTap: () => onPick('dismissal'), + ), + const Divider(height: 1, color: AppPalette.sage), + _TimeMenuRow( + icon: Icons.bug_report_outlined, + label: 'ν…ŒμŠ€νŠΈμš©: ν—ˆμš©μ‹œκ°„ μ΄ˆκΈ°ν™”', + dim: true, + onTap: () => onPick('test_reset'), + ), + ], + ), + ), + ), + ); + } +} + +class _TimeMenuRow extends StatelessWidget { + final IconData? icon; + final String label; + final VoidCallback onTap; + final bool dim; + + const _TimeMenuRow({ + required this.icon, + required this.label, + required this.onTap, + this.dim = false, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 20, + height: 20, + child: icon == null + ? const CircularProgressIndicator(strokeWidth: 2) + : Icon( + icon, + size: 20, + color: dim ? Colors.grey : AppPalette.ink, + ), + ), + const SizedBox(width: 12), + Flexible( + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.w600, + color: dim ? Colors.grey[600] : AppPalette.ink, + ), + ), + ), + ], + ), + ), + ); + } +}