diff --git a/lib/history_screen.dart b/lib/history_screen.dart index e3c7e7c..1a048f5 100644 --- a/lib/history_screen.dart +++ b/lib/history_screen.dart @@ -1,14 +1,522 @@ import 'package:flutter/material.dart'; -class HistoryScreen extends StatelessWidget { +class UsageHistory { + final String date; + final String startTime; + final String endTime; + final int duration; + final String startStation; + final String endStation; + final String status; + final bool isSanitized; + final bool isDried; + + UsageHistory({ + required this.date, + required this.startTime, + required this.endTime, + required this.duration, + required this.startStation, + required this.endStation, + required this.status, + required this.isSanitized, + required this.isDried, + }); +} + +class HistoryScreen extends StatefulWidget { const HistoryScreen({super.key}); + @override + State createState() => _HistoryScreenState(); +} + +class _HistoryScreenState extends State { + static const Color kBackgroundColor = Color(0xFF27292B); + static const Color kCardColor = Color(0xFF30343B); + static const Color kAccentColor = Colors.white; + static const Color kRedAccent = Color(0xFFFF5252); + + int _selectedFilterIndex = 0; + + final List _allHistoryList = [ + UsageHistory( + date: '2025.11.25 (Tue)', + startTime: '18:30', + endTime: '19:15', + duration: 45, + startStation: 'STATION A - GU.UNIV', + endStation: 'STATION B - CITY HALL', + status: 'RETURNED', + isSanitized: true, + isDried: true, + ), + UsageHistory( + date: '2025.11.23 (Sun)', + startTime: '14:00', + endTime: '15:30', + duration: 90, + startStation: 'STATION B - CITY HALL', + endStation: 'STATION B - CITY HALL', + status: 'RETURNED', + isSanitized: true, + isDried: false, + ), + UsageHistory( + date: '2025.11.06 (Thu)', + startTime: '09:00', + endTime: '09:30', + duration: 30, + startStation: 'STATION A - GU.UNIV', + endStation: 'STATION C - PARK', + status: 'RETURNED', + isSanitized: true, + isDried: true, + ), + UsageHistory( + date: '2025.09.20 (Sat)', + startTime: '20:00', + endTime: '21:00', + duration: 60, + startStation: 'STATION C - PARK', + endStation: 'STATION B - CITY HALL', + status: 'RETURNED', + isSanitized: true, + isDried: true, + ), + ]; + + List get _filteredList { + if (_selectedFilterIndex == 0) { + return _allHistoryList; + } + + final now = DateTime(2025, 11, 26); + + return _allHistoryList.where((history) { + String dateStr = history.date.split(' ')[0]; + List parts = dateStr.split('.'); + DateTime historyDate = DateTime( + int.parse(parts[0]), + int.parse(parts[1]), + int.parse(parts[2]) + ); + + int difference = now + .difference(historyDate) + .inDays; + + if (_selectedFilterIndex == 1) { + return difference <= 7; + } else if (_selectedFilterIndex == 2) { + return difference <= 30; + } + return true; + }).toList(); + } + @override Widget build(BuildContext context) { - return const Center( + final currentList = _filteredList; + + return Scaffold( + backgroundColor: kBackgroundColor, + appBar: AppBar( + title: const Text('HISTORY', style: TextStyle(fontWeight: FontWeight + .bold, fontSize: 16, color: Colors.white)), + backgroundColor: kBackgroundColor, + scrolledUnderElevation: 0, + elevation: 0, + centerTitle: false, + actions: [ + IconButton(icon: const Icon(Icons.filter_list, color: Colors.white), + onPressed: () {}), + ], + ), + body: Column( + children: [ + _buildFilterTabs(), + Expanded( + child: currentList.isEmpty + ? const Center( + child: Text("기록이 없습니다.", style: TextStyle(color: Colors.grey))) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: currentList.length, + itemBuilder: (context, index) { + final history = currentList[index]; + + bool showHeader = true; + if (index > 0 && currentList[index - 1].date == history.date) { + showHeader = false; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showHeader) _buildDateHeader(history.date), + _buildHistoryCard(history), + ], + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildFilterTabs() { + final filters = ["전체", "지난 7일", "지난 30일"]; + return Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: filters.length, + separatorBuilder: (c, i) => const SizedBox(width: 10), + itemBuilder: (context, index) { + final isSelected = _selectedFilterIndex == index; + return ChoiceChip( + label: Text(filters[index]), + selected: isSelected, + onSelected: (selected) { + setState(() => _selectedFilterIndex = index); + }, + backgroundColor: kCardColor, + selectedColor: kAccentColor, + checkmarkColor: Colors.black, + labelStyle: TextStyle( + color: isSelected ? Colors.black : Colors.white, + fontWeight: FontWeight.bold, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), side: BorderSide.none), + ); + }, + ), + ); + } + + Widget _buildDateHeader(String date) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), child: Text( - 'HISTORY 기록 목록 화면', - style: TextStyle(color: Colors.white70, fontSize: 24), + date, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold + ), + ), + ); + } + + Widget _buildHistoryCard(UsageHistory history) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: kCardColor, + borderRadius: BorderRadius.circular(16), + ), + child: InkWell( + onTap: () => _showHistoryDetail(context, history), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${history.startTime} - ${history.endTime}', + style: const TextStyle(color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${history.duration} min', + style: const TextStyle(color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w600), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + _buildDot(Colors.grey), + const SizedBox(width: 8), + Expanded(child: Text(history.startStation, + style: const TextStyle(color: Colors.white, fontSize: 13), + overflow: TextOverflow.ellipsis)), + ], + ), + Container( + margin: const EdgeInsets.only(left: 3), + height: 10, + decoration: const BoxDecoration( + border: Border( + left: BorderSide(color: Colors.grey, width: 1)), + ), + ), + Row( + children: [ + _buildDot(kRedAccent), + const SizedBox(width: 8), + Expanded(child: Text(history.endStation, + style: const TextStyle(color: Colors.white, fontSize: 13), + overflow: TextOverflow.ellipsis)), + ], + ), + const SizedBox(height: 12), + const Divider(color: Colors.white, thickness: 0.5,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (history.isSanitized) + _buildDot(kRedAccent), + if (history.isSanitized) + const SizedBox(width: 8), + Text( + history.isSanitized ? "살균 완료" : "Pending", + style: TextStyle( + color: history.isSanitized ? Colors.white : Colors + .white, + fontSize: 14, + fontWeight: FontWeight.bold), + ), + ], + ), + const Text( + "VIEW DETAILS >", + style: TextStyle(color: Colors.white, fontSize: 11), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildDot(Color color) { + return Container(width: 8, + height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle)); + } + + void _showHistoryDetail(BuildContext context, UsageHistory history) { + int cost = history.duration * 100; + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + return Container( + height: MediaQuery + .of(context) + .size + .height * 0.75, + decoration: const BoxDecoration( + color: kBackgroundColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + const SizedBox(height: 12), + Container(width: 40, + height: 4, + decoration: BoxDecoration(color: Colors.grey[600], + borderRadius: BorderRadius.circular(2))), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("HISTORY DETAILS", style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text( + history.date, + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 30), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration(color: kCardColor, + borderRadius: BorderRadius.circular(16)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("이용 시간", style: TextStyle( + color: Colors.white, fontSize: 12)), + const SizedBox(height: 4), + Text("${history.duration} min", + style: const TextStyle(color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold)), + ], + ), + Container( + width: 1, height: 40, color: Colors.white24), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("이용 금액", style: TextStyle( + color: Colors.white, fontSize: 12)), + const SizedBox(height: 4), + Text("$cost ₩", style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold)), + ], + ), + ], + ), + ), + const SizedBox(height: 30), + const Text("TIMELINE", style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 1.0)), + const SizedBox(height: 20), + _buildTimelineItem( + history.startTime, "대여 시작", history.startStation, + isFirst: true, isCompleted: true), + _buildTimelineItem( + history.endTime, "반납 완료", history.endStation, + isCompleted: true), + _buildTimelineItem( + history.endTime, "살균 시작", "Auto-Cleaning System", + isCompleted: true), + _buildTimelineItem( + "15 min later", "살균 완료", "Ready for next user", + isLast: true, isCompleted: history.isSanitized), + const SizedBox(height: 30), + const Text("CONDITION", style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Row( + children: [ + Expanded(child: _buildConditionCard( + "살균", history.isSanitized, + Icons.sentiment_satisfied_alt)), + const SizedBox(width: 12), + Expanded(child: _buildConditionCard( + "건조", history.isDried, + Icons.sentiment_very_satisfied)), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildTimelineItem(String time, String title, String subtitle, + {bool isFirst = false, bool isLast = false, bool isCompleted = false}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 60, + child: Text( + time, style: const TextStyle(color: Colors.white, fontSize: 12)), + ), + Column( + children: [ + Icon( + isCompleted ? Icons.check_circle : Icons.radio_button_unchecked, + color: isCompleted ? kRedAccent : Colors.grey, size: 20), + if (!isLast) + Container(width: 2, + height: 40, + color: isCompleted + ? kRedAccent.withValues(alpha: 0.5) + : Colors.grey[800]), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle( + color: isCompleted ? Colors.white : Colors.grey, + fontWeight: FontWeight.bold, + fontSize: 14)), + const SizedBox(height: 4), + Text(subtitle, + style: TextStyle(color: Colors.white, fontSize: 12)), + const SizedBox(height: 24), + ], + ), + ), + ], + ); + } + + Widget _buildConditionCard(String title, bool isDone, IconData icon) { + return Container( + height: 120, + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: kCardColor, + border: Border.all( + color: isDone ? Colors.white : Colors.transparent, width: 1.0), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: Colors.white, size: 28), + const SizedBox(height: 8), + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + isDone ? "Completed" : "In Progress", + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14 + ), + ), + ], ), ); } diff --git a/lib/login_screen.dart b/lib/login_screen.dart new file mode 100644 index 0000000..264ce4a --- /dev/null +++ b/lib/login_screen.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'main.dart'; + +class LoginScreen extends StatelessWidget { + final TextEditingController _idController = TextEditingController(); + final TextEditingController _pwController = TextEditingController(); + + LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF1E1E1E), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon(Icons.polymer, size: 80, color: Colors.white), + const SizedBox(height: 20), + const Text( + 'METAQLAB' + '', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 60), + + _buildTextField('User ID', Icons.person, controller: _idController), + const SizedBox(height: 16), + _buildTextField('Password', Icons.lock, isObscure: true, controller: _pwController), + + const SizedBox(height: 40), + + ElevatedButton( + onPressed: () { + String inputId = _idController.text; + String inputPw = _pwController.text; + + if (inputId == 'user' && inputPw == '1234') { + print('로그인 성공!'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const HomeScreen()), + ); + } else { + print('로그인 실패'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('아이디 또는 비밀번호가 틀렸습니다.'), + backgroundColor: Colors.redAccent, + duration: Duration(seconds: 2), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'LOGIN', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + + const SizedBox(height: 20), + TextButton( + onPressed: () {}, + child: const Text('Forgot Password?', style: TextStyle(color: Colors.grey)), + ), + ], + ), + ), + ); + } + + Widget _buildTextField(String hint, IconData icon, {bool isObscure = false, required TextEditingController controller}) { + return TextField( + controller: controller, + obscureText: isObscure, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + prefixIcon: Icon(icon, color: Colors.grey), + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + filled: true, + fillColor: const Color(0xFF2C2C2E), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 012c0a4..44f6fe2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:smarthelmet_app/control_screen.dart'; import 'package:smarthelmet_app/history_screen.dart'; import 'package:smarthelmet_app/rent_return_screen.dart'; import 'package:smarthelmet_app/settings_screen.dart'; +import 'package:smarthelmet_app/login_screen.dart'; void main() { runApp(const SmartHelmetApp()); @@ -32,7 +33,7 @@ class SmartHelmetApp extends StatelessWidget { bodyMedium: TextStyle(color: Colors.white70, fontWeight: FontWeight.w400), ), ), - home: const HomeScreen(), + home: LoginScreen(), ); } } diff --git a/lib/rent_return_screen.dart b/lib/rent_return_screen.dart index 70ee8d8..b313843 100644 --- a/lib/rent_return_screen.dart +++ b/lib/rent_return_screen.dart @@ -24,7 +24,6 @@ class RentReturnScreen extends StatelessWidget { Widget build(BuildContext context) { final LatLng centerLocation = const LatLng(35.1595, 126.8526); - final List stations = [ StationInfo( name: 'STATION A - GU.UNIV', @@ -49,7 +48,7 @@ class RentReturnScreen extends StatelessWidget { ]; return Scaffold( - backgroundColor: const Color(0xFF1E1E1E), + backgroundColor: const Color(0xFF27292B), appBar: AppBar( title: const Text( 'RENT/ RETURN', @@ -94,7 +93,7 @@ class RentReturnScreen extends StatelessWidget { Expanded( flex: 45, child: Container( - color: const Color(0xFF1E1E1E), + color: const Color(0xFF27292B), padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -132,26 +131,43 @@ class RentReturnScreen extends StatelessWidget { margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: const Color(0xFF2C2C2E), - borderRadius: BorderRadius.circular(16), + color: const Color(0xFF30343B), + borderRadius: BorderRadius.circular(10), ), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( + SizedBox( width: 80, height: 80, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey[800], - borderRadius: BorderRadius.circular(12), - ), - child: Image.asset( - 'assets/images/storage.png', - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) => const Icon(Icons.inventory_2, color: Colors.white54), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(5), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: const Color(0xFF2D2F33), + borderRadius: BorderRadius.circular(5) + ), + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Image.asset( + 'assets/images/storage.png', + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.inventory_2, color: Colors.white54), + ), + ), + ], + ), ), ), const SizedBox(width: 12), @@ -210,7 +226,7 @@ class RentReturnScreen extends StatelessWidget { backgroundColor: Colors.white, foregroundColor: Colors.black, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(5), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), minimumSize: const Size(80, 40), @@ -230,7 +246,7 @@ class RentReturnScreen extends StatelessWidget { padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF1E1E1E), - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start,