// ๐Ÿ“‹ ์„ ์ƒ๋‹˜์šฉ "์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ ๋Œ€์žฅ" ํ™”๋ฉด (UI ์ „์šฉ). ์˜ค๋Š˜ ์‹ ์ฒญ๋œ ๋ฐ˜์ถœ ์š”์ฒญ์„ ๋ณด์—ฌ์ฃผ๊ณ  ์Šน์ธ/๊ฑฐ์ ˆํ•œ๋‹ค. // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/device_checkout_ledger_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/device_checkout_ledger_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'title_pill.dart'; class DeviceCheckoutLedgerPage extends StatefulWidget { final String? teacherId; final String? teacherName; const DeviceCheckoutLedgerPage({super.key, this.teacherId, this.teacherName}); @override State createState() => _DeviceCheckoutLedgerPageState(); } class _DeviceCheckoutLedgerPageState extends State { final DeviceCheckoutLedgerController _controller = DeviceCheckoutLedgerController(); @override void initState() { super.initState(); _controller.init(); } @override void dispose() { _controller.dispose(); super.dispose(); } Future _approve(int id) async { final (_, message) = await _controller.approve( id, teacherId: widget.teacherId ?? '๊ฐ„ํŽธ์ธ์ฆ', teacherName: widget.teacherName ?? '๊ฐ„ํŽธ์ธ์ฆ ์„ ์ƒ', ); if (!mounted) return; AppNotice.show(context, message); } Future _reject(int id) async { final (_, message) = await _controller.reject(id); if (!mounted) return; AppNotice.show(context, message); } // ๐Ÿ–๏ธ [ํ•˜๋“œ์›จ์–ด ์ž๋™ ๊ฐ์ง€ ์ „๊นŒ์ง€ ์ž„์‹œ] ๊ธฐ๊ธฐ๋ฅผ ์‹ค์ œ๋กœ ๋Œ๋ ค๋ฐ›์•˜์„ ๋•Œ ๋ˆ„๋ฅด๋Š” ๋ฒ„ํŠผ. Future _confirmReturn(int id) async { final (_, message) = await _controller.confirmReturn(id); if (!mounted) return; AppNotice.show(context, message); } ({Color color, String label}) _statusInfo(String status) { switch (status) { case 'PENDING': return (color: Colors.orange, label: '๋Œ€๊ธฐ์ค‘'); case 'APPROVED': return (color: Colors.blue, label: '์Šน์ธ๋จ'); case 'RETURNED': return (color: Colors.grey, label: '๋ฐ˜๋‚ฉ์™„๋ฃŒ'); case 'REJECTED': return (color: Colors.red, label: '๊ฑฐ์ ˆ๋จ'); default: return (color: Colors.grey, label: status); } } String _timeRange(String start, String end) { // "YYYY-MM-DD HH:MM:SS" -> "HH:MM" String hm(String full) => full.length >= 16 ? full.substring(11, 16) : full; return '${hm(start)} ~ ${hm(end)}'; } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { final requests = _controller.requests; final pendingCount = requests .where((r) => r['status'] == 'PENDING') .length; return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, centerTitle: true, title: const TitlePill( child: Text( '์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ ๋Œ€์žฅ', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.white, ), ), ), actions: [ IconButton( icon: const Icon(Icons.refresh_rounded), onPressed: _controller.fetchAll, tooltip: '์ƒˆ๋กœ๊ณ ์นจ', ), ], ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : Column( children: [ if (pendingCount > 0) Container( width: double.infinity, margin: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), decoration: BoxDecoration( color: Colors.orange[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.orange[400]!), ), child: Text( '์Šน์ธ ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ์ด $pendingCount๊ฑด ์žˆ์Šต๋‹ˆ๋‹ค.', style: TextStyle( color: Colors.orange[900], fontWeight: FontWeight.bold, ), ), ), Expanded( child: requests.isEmpty ? const Center( child: Text( '์˜ค๋Š˜ ์‹ ์ฒญ๋œ ๋ฐ˜์ถœ ์š”์ฒญ์ด ์—†์Šต๋‹ˆ๋‹ค.', style: TextStyle(color: Colors.grey), ), ) : ListView.builder( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), itemCount: requests.length, itemBuilder: (context, index) { final r = requests[index]; final String status = r['status']; final info = _statusInfo(status); final bool isPending = status == 'PENDING'; return Card( elevation: 0, color: AppPalette.paper, margin: const EdgeInsets.only(bottom: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: isPending ? BorderSide(color: Colors.orange[300]!) : BorderSide(color: AppPalette.sage), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( '${r['studentName']} (${r['studentId']})', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 15, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: info.color.withValues( alpha: 0.12, ), borderRadius: BorderRadius.circular(20), ), child: Text( info.label, style: TextStyle( color: info.color, fontWeight: FontWeight.bold, fontSize: 12, ), ), ), ], ), const SizedBox(height: 6), Text( _timeRange( r['requestedStart'], r['requestedEnd'], ), style: TextStyle( color: Colors.grey[700], fontSize: 13, ), ), const SizedBox(height: 2), Text( r['purpose'], style: TextStyle( color: Colors.grey[700], fontSize: 13, ), ), if (status == 'APPROVED' && r['approvedByTeacherName'] != null) ...[ const SizedBox(height: 2), Text( '${r['approvedByTeacherName']} ์„ ์ƒ๋‹˜์ด ํ—ˆ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค', style: TextStyle( color: Colors.blue[700], fontSize: 12, fontWeight: FontWeight.w600, ), ), ], if (isPending) ...[ const SizedBox(height: 12), Row( children: [ Expanded( child: OutlinedButton( onPressed: _controller.isWorking ? null : () => _reject(r['id']), style: OutlinedButton.styleFrom( foregroundColor: Colors.red, side: const BorderSide( color: Colors.red, ), ), child: const Text('๊ฑฐ์ ˆ'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton( onPressed: _controller.isWorking ? null : () => _approve(r['id']), style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, ), child: const Text('์Šน์ธ'), ), ), ], ), ], if (status == 'APPROVED') ...[ const SizedBox(height: 12), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _controller.isWorking ? null : () => _confirmReturn(r['id']), style: OutlinedButton.styleFrom( foregroundColor: Colors.grey[800], side: BorderSide( color: Colors.grey[400]!, ), ), icon: const Icon( Icons .assignment_turned_in_outlined, size: 18, ), label: const Text('๊ธฐ๊ธฐ ๋ฐ˜๋‚ฉ ํ™•์ธ'), ), ), ], ], ), ), ); }, ), ), ], ), ); }, ); } }