diff --git a/lib/main.dart b/lib/main.dart index b5f4e77..fcf9542 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,25 +1,32 @@ - - import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'screens/splash_screen.dart'; import 'screens/home/home_screen.dart'; import 'screens/chat/chat_screen.dart'; import 'services/navigation_service.dart'; import 'services/supabase_service.dart'; import 'services/ai_service.dart'; +import 'theme/theme_controller.dart'; +import 'theme/app_themes.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - + try { await SupabaseService().initialize(); - await AIService().initialize(); } catch (e) { debugPrint('Error initializing services: $e'); } - - runApp(const MyApp()); + + final themeController = await ThemeController.create(); + + runApp( + ChangeNotifierProvider.value( + value: themeController, + child: const MyApp(), + ), + ); } class MyApp extends StatelessWidget { @@ -27,31 +34,15 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final themeController = context.watch(); return MaterialApp( title: 'Ell-ena', debugShowCheckedModeBanner: false, navigatorKey: NavigationService().navigatorKey, navigatorObservers: [AppRouteObserver.instance], - theme: ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - primaryColor: Colors.green.shade400, - scaffoldBackgroundColor: const Color(0xFF1A1A1A), - colorScheme: ColorScheme.dark( - primary: Colors.green.shade400, - secondary: Colors.green.shade700, - surface: const Color(0xFF2A2A2A), - background: const Color(0xFF1A1A1A), - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - bodyLarge: TextStyle(fontSize: 16, letterSpacing: 0.5), - ), - ), + theme: lightTheme, + darkTheme: darkTheme, + themeMode: themeController.flutterThemeMode, home: const SplashScreen(), onGenerateRoute: (settings) { if (settings.name == '/') { diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart index c8cf293..315d599 100644 --- a/lib/screens/auth/login_screen.dart +++ b/lib/screens/auth/login_screen.dart @@ -220,18 +220,23 @@ class _LoginScreenState extends State // OR divider Row( children: [ - Expanded(child: Divider(color: Colors.grey.shade700)), + Expanded( + child: + Divider(color: Theme.of(context).dividerColor)), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'OR', style: TextStyle( - color: Colors.grey.shade500, + color: + Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), ), - Expanded(child: Divider(color: Colors.grey.shade700)), + Expanded( + child: + Divider(color: Theme.of(context).dividerColor)), ], ), const SizedBox(height: 24), @@ -250,7 +255,7 @@ class _LoginScreenState extends State ), ), style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, + foregroundColor: Theme.of(context).colorScheme.onSurface, side: BorderSide(color: Colors.green.shade400, width: 2), padding: const EdgeInsets.symmetric( vertical: 16, @@ -267,7 +272,9 @@ class _LoginScreenState extends State children: [ Text( 'Don\'t have an account? ', - style: TextStyle(color: Colors.grey.shade400), + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurfaceVariant), ), TextButton( onPressed: () { diff --git a/lib/screens/auth/signup_screen.dart b/lib/screens/auth/signup_screen.dart index 1074b6d..cd4eca3 100644 --- a/lib/screens/auth/signup_screen.dart +++ b/lib/screens/auth/signup_screen.dart @@ -217,7 +217,7 @@ class _SignupScreenState extends State Tab(text: 'Create the Team'), ], labelColor: Colors.green.shade400, - unselectedLabelColor: Colors.grey, + unselectedLabelColor: Theme.of(context).colorScheme.onSurfaceVariant, indicatorColor: Colors.green.shade400, indicatorSize: TabBarIndicatorSize.tab, ), @@ -403,18 +403,18 @@ class _SignupScreenState extends State // OR divider Row( children: [ - Expanded(child: Divider(color: Colors.grey.shade700)), + Expanded(child: Divider(color: Theme.of(context).dividerColor)), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'OR', style: TextStyle( - color: Colors.grey.shade500, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), ), - Expanded(child: Divider(color: Colors.grey.shade700)), + Expanded(child: Divider(color: Theme.of(context).dividerColor)), ], ), const SizedBox(height: 24), @@ -434,7 +434,7 @@ class _SignupScreenState extends State ), ), style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, + foregroundColor: Theme.of(context).colorScheme.onSurface, side: BorderSide(color: Colors.green.shade400, width: 2), padding: const EdgeInsets.symmetric( vertical: 16, diff --git a/lib/screens/auth/team_selection_dialog.dart b/lib/screens/auth/team_selection_dialog.dart index 18602bb..eefed3a 100644 --- a/lib/screens/auth/team_selection_dialog.dart +++ b/lib/screens/auth/team_selection_dialog.dart @@ -125,40 +125,42 @@ class _TeamSelectionDialogState extends State { barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( - backgroundColor: const Color(0xFF2A2A2A), - title: const Text( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( 'Team Created Successfully!', - style: TextStyle(color: Colors.white), + style: Theme.of(context).textTheme.titleLarge, ), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( + Text( 'Your Team ID is:', - style: TextStyle(color: Colors.grey), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), + color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(8), ), child: Text( teamId, - style: const TextStyle( + style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2, - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, ), ), ), const SizedBox(height: 16), - const Text( + Text( 'Share this ID with your team members so they can join your team.', - style: TextStyle(color: Colors.grey), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], @@ -183,21 +185,22 @@ class _TeamSelectionDialogState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => false, // Prevent dismissal + return PopScope( + canPop: false, child: AlertDialog( - backgroundColor: const Color(0xFF2A2A2A), - title: const Text( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( 'Complete Your Setup', - style: TextStyle(color: Colors.white), + style: Theme.of(context).textTheme.titleLarge, ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( + Text( 'Choose how you want to proceed:', - style: TextStyle(color: Colors.grey), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 24), // Toggle between Join and Create @@ -315,9 +318,11 @@ class _OptionCard extends StatelessWidget { decoration: BoxDecoration( color: isSelected ? Colors.green.withOpacity(0.2) - : const Color(0xFF1A1A1A), + : Theme.of(context).colorScheme.surfaceContainerLow, border: Border.all( - color: isSelected ? Colors.green : Colors.grey.shade800, + color: isSelected + ? Colors.green + : Theme.of(context).colorScheme.outline, width: 2, ), borderRadius: BorderRadius.circular(12), @@ -327,14 +332,18 @@ class _OptionCard extends StatelessWidget { children: [ Icon( icon, - color: isSelected ? Colors.green : Colors.grey, + color: isSelected + ? Colors.green + : Theme.of(context).colorScheme.onSurfaceVariant, size: 32, ), const SizedBox(height: 8), Text( title, style: TextStyle( - color: isSelected ? Colors.white : Colors.grey, + color: isSelected + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontSize: 14, ), diff --git a/lib/screens/auth/verify_otp_screen.dart b/lib/screens/auth/verify_otp_screen.dart index e5fc5ac..7ef72e8 100644 --- a/lib/screens/auth/verify_otp_screen.dart +++ b/lib/screens/auth/verify_otp_screen.dart @@ -10,7 +10,7 @@ class VerifyOTPScreen extends StatefulWidget { final String email; final String verifyType; // 'signup_join', 'signup_create', or 'reset_password' final Map userData; - + const VerifyOTPScreen({ super.key, required this.email, @@ -59,7 +59,7 @@ class _VerifyOTPScreenState extends State { type: widget.verifyType, userData: widget.userData, ); - + if (result['success']) { // Handle successful verification based on verify type if (widget.verifyType == 'signup_create') { @@ -114,7 +114,7 @@ class _VerifyOTPScreenState extends State { } } } - + Future _resendCode() async { setState(() { _isLoading = true; @@ -126,7 +126,7 @@ class _VerifyOTPScreenState extends State { widget.email, type: widget.verifyType, ); - + if (result['success']) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -137,32 +137,34 @@ class _VerifyOTPScreenState extends State { } else { setState(() { String errorMsg = result['error'] ?? 'Failed to resend code'; - + // Make the error message more user-friendly if (errorMsg.contains('Rate limit')) { errorMsg = 'Too many attempts. Please try again later.'; - } else if (errorMsg.contains('not found') || errorMsg.contains('Invalid email')) { + } else if (errorMsg.contains('not found') || + errorMsg.contains('Invalid email')) { errorMsg = 'Email address not found or invalid.'; } - + _errorMessage = errorMsg; }); } } catch (e) { setState(() { String errorMsg = e.toString(); - + // Make the error message more user-friendly if (errorMsg.contains('Rate limit')) { errorMsg = 'Too many attempts. Please try again later.'; - } else if (errorMsg.contains('not found') || errorMsg.contains('Invalid email')) { + } else if (errorMsg.contains('not found') || + errorMsg.contains('Invalid email')) { errorMsg = 'Email address not found or invalid.'; } else if (errorMsg.contains('Assertion failed')) { errorMsg = 'Unable to resend code. Please go back and try again.'; } else { errorMsg = 'An error occurred. Please try again.'; } - + _errorMessage = errorMsg; }); } finally { @@ -173,7 +175,7 @@ class _VerifyOTPScreenState extends State { } } } - + // Show dialog with the generated team ID void _showTeamIdDialog(String teamId) { showDialog( @@ -181,23 +183,25 @@ class _VerifyOTPScreenState extends State { barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( - backgroundColor: const Color(0xFF2A2A2A), - title: const Text( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( 'Team Created Successfully!', - style: TextStyle(color: Colors.white), + style: Theme.of(context).textTheme.titleLarge, ), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( + Text( 'Your Team ID is:', - style: TextStyle(color: Colors.grey), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 16), Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( - color: const Color(0xFF1A1A1A), + color: Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(8), ), child: Row( @@ -205,11 +209,11 @@ class _VerifyOTPScreenState extends State { children: [ Text( teamId, - style: const TextStyle( + style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 2, - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, ), ), IconButton( @@ -228,9 +232,10 @@ class _VerifyOTPScreenState extends State { ), ), const SizedBox(height: 16), - const Text( + Text( 'Share this ID with your team members so they can join your team.', - style: TextStyle(color: Colors.grey), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], @@ -279,18 +284,16 @@ class _VerifyOTPScreenState extends State { keyboardType: TextInputType.number, maxLength: 1, textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, ), decoration: InputDecoration( counterText: '', filled: true, - fillColor: const Color(0xFF2A2A2A), border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), - borderSide: BorderSide.none, ), ), onChanged: (value) { @@ -319,7 +322,8 @@ class _VerifyOTPScreenState extends State { children: [ Text( 'Didn\'t receive the code? ', - style: TextStyle(color: Colors.grey.shade400), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), ), TextButton( onPressed: _isLoading ? null : _resendCode, @@ -336,4 +340,4 @@ class _VerifyOTPScreenState extends State { ], ); } -} \ No newline at end of file +} diff --git a/lib/screens/calendar/calendar_screen.dart b/lib/screens/calendar/calendar_screen.dart index a0f0212..3b0fd59 100644 --- a/lib/screens/calendar/calendar_screen.dart +++ b/lib/screens/calendar/calendar_screen.dart @@ -12,7 +12,7 @@ import '../../widgets/custom_widgets.dart'; import '../meetings/meeting_detail_screen.dart'; import '../tasks/task_detail_screen.dart'; import '../tickets/ticket_detail_screen.dart'; -import '../chat/chat_screen.dart'; +import '../chat/chat_screen.dart'; class CalendarScreen extends StatefulWidget { const CalendarScreen({super.key}); @@ -31,13 +31,13 @@ class _CalendarScreenState extends State { bool _isLoading = true; String? _currentUserId; bool _isAdmin = false; - + // Cache keys static const String _tasksKey = 'calendar_tasks'; static const String _ticketsKey = 'calendar_tickets'; static const String _meetingsKey = 'calendar_meetings'; static const String _lastFetchTimeKey = 'calendar_last_fetch_time'; - + // Cache duration (5 minutes) static const Duration _cacheDuration = Duration(minutes: 5); @@ -47,19 +47,19 @@ class _CalendarScreenState extends State { _selectedDay = _focusedDay; _loadCurrentUserInfo(); } - + Future _loadCurrentUserInfo() async { setState(() { _isLoading = true; }); - + try { final userProfile = await _supabaseService.getCurrentUserProfile(); if (userProfile != null) { _currentUserId = _supabaseService.client.auth.currentUser?.id; _isAdmin = userProfile['role'] == 'admin'; } - + await _loadEventsWithCache(); } catch (e) { debugPrint('Error loading user info: $e'); @@ -70,47 +70,47 @@ class _CalendarScreenState extends State { } } } - + // Check if cache is valid Future _isCacheValid() async { try { final prefs = await SharedPreferences.getInstance(); final lastFetchTimeStr = prefs.getString(_lastFetchTimeKey); - + if (lastFetchTimeStr == null) return false; - + final lastFetchTime = DateTime.parse(lastFetchTimeStr); final now = DateTime.now(); - + return now.difference(lastFetchTime) < _cacheDuration; } catch (e) { debugPrint('Error checking cache validity: $e'); return false; } } - + // Load events from cache or network Future _loadEventsWithCache() async { try { if (!mounted) return; - + setState(() { _isLoading = true; }); - + // Clear existing events _events.clear(); - + // Check if cache is valid final isCacheValid = await _isCacheValid(); - + if (isCacheValid) { // Load from cache await _loadEventsFromCache(); } else { // Load from network await _loadEventsFromNetwork(); - + // Save to cache await _saveEventsToCache(); } @@ -126,39 +126,36 @@ class _CalendarScreenState extends State { } } } - + // Load events from SharedPreferences cache Future _loadEventsFromCache() async { try { final prefs = await SharedPreferences.getInstance(); - + // Load tasks final tasksJson = prefs.getString(_tasksKey); if (tasksJson != null) { final tasks = List>.from( - jsonDecode(tasksJson).map((x) => Map.from(x)) - ); + jsonDecode(tasksJson).map((x) => Map.from(x))); _processTasksData(tasks); } - + // Load tickets final ticketsJson = prefs.getString(_ticketsKey); if (ticketsJson != null) { final tickets = List>.from( - jsonDecode(ticketsJson).map((x) => Map.from(x)) - ); + jsonDecode(ticketsJson).map((x) => Map.from(x))); _processTicketsData(tickets); } - + // Load meetings final meetingsJson = prefs.getString(_meetingsKey); if (meetingsJson != null) { final meetings = List>.from( - jsonDecode(meetingsJson).map((x) => Map.from(x)) - ); + jsonDecode(meetingsJson).map((x) => Map.from(x))); _processMeetingsData(meetings); } - + debugPrint('Events loaded from cache'); } catch (e) { debugPrint('Error loading events from cache: $e'); @@ -166,21 +163,22 @@ class _CalendarScreenState extends State { await _loadEventsFromNetwork(); } } - + // Save events to SharedPreferences cache Future _saveEventsToCache() async { try { final prefs = await SharedPreferences.getInstance(); - + // Save last fetch time - await prefs.setString(_lastFetchTimeKey, DateTime.now().toIso8601String()); - + await prefs.setString( + _lastFetchTimeKey, DateTime.now().toIso8601String()); + // Tasks, tickets, and meetings are saved in their respective methods } catch (e) { debugPrint('Error saving events to cache: $e'); } } - + // Load events from network Future _loadEventsFromNetwork() async { try { @@ -190,51 +188,50 @@ class _CalendarScreenState extends State { _loadTickets(), _loadMeetings(), ]); - + debugPrint('Events loaded from network'); } catch (e) { debugPrint('Error loading events from network: $e'); } } - + // Load tasks Future _loadTasks() async { try { final tasks = await _supabaseService.getTasks(); - + // Filter tasks for current user (created by or assigned to) final filteredTasks = tasks.where((task) { final createdBy = task['created_by']; final assignedTo = task['assigned_to']; - return _isAdmin || - createdBy == _currentUserId || - assignedTo == _currentUserId || - assignedTo == null; + return _isAdmin || + createdBy == _currentUserId || + assignedTo == _currentUserId || + assignedTo == null; }).toList(); - + // Process tasks data _processTasksData(filteredTasks); - + // Save to cache final prefs = await SharedPreferences.getInstance(); await prefs.setString(_tasksKey, jsonEncode(filteredTasks)); - } catch (e) { debugPrint('Error loading tasks: $e'); } } - + // Process tasks data void _processTasksData(List> tasks) { for (var task in tasks) { if (task['due_date'] != null) { final dueDate = DateTime.parse(task['due_date']); final dateOnly = DateTime(dueDate.year, dueDate.month, dueDate.day); - + if (!_events.containsKey(dateOnly)) { _events[dateOnly] = []; } - + _events[dateOnly]!.add(CalendarEvent( title: task['title'] ?? 'Untitled Task', startTime: const TimeOfDay(hour: 23, minute: 0), @@ -245,89 +242,92 @@ class _CalendarScreenState extends State { } } } - + // Load tickets Future _loadTickets() async { try { final tickets = await _supabaseService.getTickets(); - + // Filter tickets for current user (created by or assigned to) final filteredTickets = tickets.where((ticket) { final createdBy = ticket['created_by']; final assignedTo = ticket['assigned_to']; - return _isAdmin || - createdBy == _currentUserId || - assignedTo == _currentUserId || - assignedTo == null; + return _isAdmin || + createdBy == _currentUserId || + assignedTo == _currentUserId || + assignedTo == null; }).toList(); - + // Process tickets data _processTicketsData(filteredTickets); - + // Save to cache final prefs = await SharedPreferences.getInstance(); await prefs.setString(_ticketsKey, jsonEncode(filteredTickets)); - } catch (e) { debugPrint('Error loading tickets: $e'); } } - + // Process tickets data void _processTicketsData(List> tickets) { for (var ticket in tickets) { if (ticket['created_at'] != null) { final createdAt = DateTime.parse(ticket['created_at']); - final dateOnly = DateTime(createdAt.year, createdAt.month, createdAt.day); - + final dateOnly = + DateTime(createdAt.year, createdAt.month, createdAt.day); + if (!_events.containsKey(dateOnly)) { _events[dateOnly] = []; } - + _events[dateOnly]!.add(CalendarEvent( title: ticket['title'] ?? 'Untitled Ticket', startTime: TimeOfDay(hour: createdAt.hour, minute: createdAt.minute), - endTime: TimeOfDay(hour: createdAt.hour + 1, minute: createdAt.minute), + endTime: + TimeOfDay(hour: createdAt.hour + 1, minute: createdAt.minute), type: EventType.ticket, id: ticket['id'], )); } } } - + // Load meetings Future _loadMeetings() async { try { final meetings = await _supabaseService.getMeetings(); - + // Process meetings data (all meetings are visible to everyone) _processMeetingsData(meetings); - + // Save to cache final prefs = await SharedPreferences.getInstance(); await prefs.setString(_meetingsKey, jsonEncode(meetings)); - } catch (e) { debugPrint('Error loading meetings: $e'); } } - + // Process meetings data void _processMeetingsData(List> meetings) { for (var meeting in meetings) { if (meeting['meeting_date'] != null) { final meetingDate = DateTime.parse(meeting['meeting_date']); - final dateOnly = DateTime(meetingDate.year, meetingDate.month, meetingDate.day); - + final dateOnly = + DateTime(meetingDate.year, meetingDate.month, meetingDate.day); + if (!_events.containsKey(dateOnly)) { _events[dateOnly] = []; } - + // For meetings, assume 1 hour duration _events[dateOnly]!.add(CalendarEvent( title: meeting['title'] ?? 'Untitled Meeting', - startTime: TimeOfDay(hour: meetingDate.hour, minute: meetingDate.minute), - endTime: TimeOfDay(hour: meetingDate.hour + 1, minute: meetingDate.minute), + startTime: + TimeOfDay(hour: meetingDate.hour, minute: meetingDate.minute), + endTime: + TimeOfDay(hour: meetingDate.hour + 1, minute: meetingDate.minute), type: EventType.meeting, id: meeting['id'], )); @@ -340,118 +340,123 @@ class _CalendarScreenState extends State { return _events[normalizedDay] ?? []; } -@override -Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - body: _isLoading - ? const CalendarLoadingSkeleton() - : SafeArea( - child: Column( - children: [ - const SizedBox(height: 8), - _buildCalendar(), - const SizedBox(height: 12), - Expanded(child: _buildTimeScale()), - ], + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: _isLoading + ? const CalendarLoadingSkeleton() + : SafeArea( + child: Column( + children: [ + const SizedBox(height: 8), + _buildCalendar(), + const SizedBox(height: 12), + Expanded(child: _buildTimeScale()), + ], + ), ), - ), - ); -} + ); + } -Widget _buildCalendar() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), - borderRadius: BorderRadius.circular(20), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TableCalendar( - firstDay: DateTime.utc(2024, 1, 1), - lastDay: DateTime.utc(2025, 12, 31), - focusedDay: _focusedDay, - calendarFormat: _calendarFormat, - selectedDayPredicate: (day) => isSameDay(_selectedDay, day), - eventLoader: _getEventsForDay, - onDaySelected: (selectedDay, focusedDay) { - setState(() { - _selectedDay = selectedDay; - _focusedDay = focusedDay; - }); - }, - onFormatChanged: (format) { - setState(() { - _calendarFormat = format; - }); - }, - calendarBuilders: CalendarBuilders( - markerBuilder: (context, date, events) { - if (events.isEmpty) return const SizedBox.shrink(); - - return Positioned( - bottom: 1, - child: Container( - height: 16, - width: events.length > 3 ? 35 : (events.length * 8 + 10), - decoration: BoxDecoration( - color: Colors.green.withOpacity(0.3), - borderRadius: BorderRadius.circular(10), - ), - child: Center( - child: Text( - '${events.length}', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, + Widget _buildCalendar() { + final colorScheme = Theme.of(context).colorScheme; + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TableCalendar( + firstDay: DateTime.utc(2024, 1, 1), + lastDay: DateTime.utc(2025, 12, 31), + focusedDay: _focusedDay, + calendarFormat: _calendarFormat, + selectedDayPredicate: (day) => isSameDay(_selectedDay, day), + eventLoader: _getEventsForDay, + onDaySelected: (selectedDay, focusedDay) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + }, + onFormatChanged: (format) { + setState(() { + _calendarFormat = format; + }); + }, + calendarBuilders: CalendarBuilders( + markerBuilder: (context, date, events) { + if (events.isEmpty) return const SizedBox.shrink(); + final isLight = Theme.of(context).brightness == Brightness.light; + final markerOpacity = isLight ? 0.55 : 0.3; + return Positioned( + bottom: 1, + child: Container( + height: 16, + width: events.length > 3 ? 35 : (events.length * 8 + 10), + decoration: BoxDecoration( + color: Colors.green.withOpacity(markerOpacity), + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + '${events.length}', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 10, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - ); - }, - ), - calendarStyle: const CalendarStyle( - defaultTextStyle: TextStyle(color: Colors.white), - weekendTextStyle: TextStyle(color: Colors.white70), - selectedTextStyle: TextStyle(color: Colors.black), - todayTextStyle: TextStyle(color: Colors.black), - outsideTextStyle: TextStyle(color: Colors.white38), - selectedDecoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, + ); + }, ), - todayDecoration: BoxDecoration( - color: Colors.greenAccent, - shape: BoxShape.circle, + calendarStyle: CalendarStyle( + defaultTextStyle: TextStyle(color: colorScheme.onSurface), + weekendTextStyle: TextStyle(color: colorScheme.onSurfaceVariant), + selectedTextStyle: TextStyle(color: colorScheme.onPrimary), + todayTextStyle: TextStyle(color: colorScheme.onSurface), + outsideTextStyle: + TextStyle(color: colorScheme.onSurface.withOpacity(0.38)), + selectedDecoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + todayDecoration: BoxDecoration( + color: Colors.green.withOpacity(0.8), + shape: BoxShape.circle, + ), + markersMaxCount: 0, + markerDecoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), ), - markersMaxCount: 0, - markerDecoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, + headerStyle: HeaderStyle( + formatButtonVisible: false, + titleCentered: true, + titleTextStyle: TextStyle( + color: colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + leftChevronIcon: + Icon(Icons.chevron_left, color: colorScheme.onSurface), + rightChevronIcon: + Icon(Icons.chevron_right, color: colorScheme.onSurface), ), - ), - headerStyle: const HeaderStyle( - formatButtonVisible: false, - titleCentered: true, - titleTextStyle: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, + daysOfWeekStyle: DaysOfWeekStyle( + weekdayStyle: TextStyle(color: colorScheme.onSurface), + weekendStyle: TextStyle(color: colorScheme.onSurfaceVariant), ), - leftChevronIcon: Icon(Icons.chevron_left, color: Colors.white), - rightChevronIcon: Icon(Icons.chevron_right, color: Colors.white), - ), - daysOfWeekStyle: const DaysOfWeekStyle( - weekdayStyle: TextStyle(color: Colors.white), - weekendStyle: TextStyle(color: Colors.white70), ), ), - ), - ); -} + ); + } Widget _buildTimeScale() { return ListView.builder( @@ -461,9 +466,10 @@ Widget _buildCalendar() { final hour = index; final time = TimeOfDay(hour: hour, minute: 0); final events = _getEventsForHour(hour); - + // Calculate dynamic height based on number of events (minimum 60) - final double timeSlotHeight = events.isEmpty ? 60 : max(60, events.length * 40.0); + final double timeSlotHeight = + events.isEmpty ? 60 : max(60, events.length * 40.0); return InkWell( onTap: () { @@ -479,7 +485,9 @@ Widget _buildCalendar() { width: 50, child: Text( '${time.format(context).toLowerCase()}', - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12), ), ), const SizedBox(width: 8), @@ -488,7 +496,7 @@ Widget _buildCalendar() { decoration: BoxDecoration( border: Border( top: BorderSide( - color: Colors.grey.shade800, + color: Theme.of(context).colorScheme.outline, width: 0.5, ), ), @@ -563,7 +571,7 @@ Widget _buildCalendar() { Future _handleEventTap(CalendarEvent event) async { dynamic result; - + switch (event.type) { case EventType.meeting: // Navigate to meeting detail screen @@ -593,7 +601,7 @@ Widget _buildCalendar() { ); break; } - + // Refresh events if something was updated if (result == true) { await _loadEventsWithCache(); // Use cache loading @@ -604,17 +612,16 @@ Widget _buildCalendar() { showDialog( context: context, builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: Text( 'Create at ${selectedTime.format(context)}', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), content: Column( mainAxisSize: MainAxisSize.min, @@ -686,9 +693,9 @@ Widget _buildCalendar() { Future _handleCreate(EventType type, TimeOfDay selectedTime) async { Navigator.of(context).pop(); // Close dialog - + if (_selectedDay == null) return; - + final selectedDateTime = DateTime( _selectedDay!.year, _selectedDay!.month, @@ -696,9 +703,9 @@ Widget _buildCalendar() { selectedTime.hour, selectedTime.minute, ); - + dynamic result; - + switch (type) { case EventType.meeting: result = await Navigator.push( @@ -725,7 +732,7 @@ Widget _buildCalendar() { ); break; } - + // Refresh events if something was created if (result == true) { await _loadEventsWithCache(); // Use cache loading @@ -735,9 +742,9 @@ Widget _buildCalendar() { // Handle creation with AI assistant void _handleCreateWithAI(TimeOfDay selectedTime) { Navigator.of(context).pop(); // Close dialog - + if (_selectedDay == null) return; - + final selectedDateTime = DateTime( _selectedDay!.year, _selectedDay!.month, @@ -745,19 +752,19 @@ Widget _buildCalendar() { selectedTime.hour, selectedTime.minute, ); - + // Format the date for the AI final formattedDate = DateFormat('yyyy-MM-dd').format(selectedDateTime); final formattedTime = selectedTime.format(context); - final message = 'I need to create a task for $formattedDate at $formattedTime'; - + final message = + 'I need to create a task for $formattedDate at $formattedTime'; + // Use the NavigationService to navigate to the chat screen Navigator.push( context, MaterialPageRoute( - builder: (context) => ChatScreen( - arguments: {'initial_message': message} - ), + builder: (context) => + ChatScreen(arguments: {'initial_message': message}), ), ); } diff --git a/lib/screens/chat/chat_screen.dart b/lib/screens/chat/chat_screen.dart index d0f8ba7..614739b 100644 --- a/lib/screens/chat/chat_screen.dart +++ b/lib/screens/chat/chat_screen.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:ell_ena/services/ai_service.dart'; import 'package:ell_ena/services/supabase_service.dart'; @@ -11,7 +10,7 @@ import 'package:speech_to_text/speech_to_text.dart' as stt; class ChatScreen extends StatefulWidget { final Map? arguments; - + const ChatScreen({super.key, this.arguments}); @override @@ -27,11 +26,11 @@ class _ChatScreenState extends State with TickerProviderStateMixin { late AnimationController _waveformController; late final stt.SpeechToText _speech; bool _speechAvailable = false; - + // Services final AIService _aiService = AIService(); final SupabaseService _supabaseService = SupabaseService(); - + // Team members for assignment List> _teamMembers = []; List> _userTasks = []; @@ -44,19 +43,20 @@ class _ChatScreenState extends State with TickerProviderStateMixin { vsync: this, duration: const Duration(milliseconds: 1000), )..repeat(); - + _initializeServices(); _initSpeech(); - + // Handle initial message if provided WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.arguments != null && + if (widget.arguments != null && widget.arguments!.containsKey('initial_message') && widget.arguments!['initial_message'] is String) { // Set a small delay to ensure services are initialized Future.delayed(const Duration(milliseconds: 1000), () { if (mounted) { - _messageController.text = widget.arguments!['initial_message'] as String; + _messageController.text = + widget.arguments!['initial_message'] as String; _sendMessage(); } }); @@ -66,8 +66,8 @@ class _ChatScreenState extends State with TickerProviderStateMixin { Future _initSpeech() async { _speech = stt.SpeechToText(); - _speechAvailable = await _speech.initialize( - onStatus: (status) { + _speechAvailable = await _speech.initialize( + onStatus: (status) { if (status == 'done' || status == 'notListening') { if (mounted) { setState(() => _isListening = false); @@ -77,8 +77,8 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } } } - }, - onError: (error) { + }, + onError: (error) { setState(() => _isListening = false); if (mounted) { setState(() => _isListening = false); @@ -86,35 +86,36 @@ class _ChatScreenState extends State with TickerProviderStateMixin { Navigator.of(context).pop(); } } - }, - ); + }, + ); if (mounted) setState(() {}); } - + Future _initializeServices() async { try { if (!_aiService.isInitialized) { await _aiService.initialize(); } - + if (!_supabaseService.isInitialized) { await _supabaseService.initialize(); } - + if (_supabaseService.isInitialized) { final userProfile = await _supabaseService.getCurrentUserProfile(); if (userProfile != null && userProfile['team_id'] != null) { await _loadTeamMembers(userProfile['team_id']); - + await _loadUserTasksAndTickets(); } } - + setState(() { _messages.add( ChatMessage( - text: "Hello! I'm Ell-ena, your AI assistant. How can I help you today?", + text: + "Hello! I'm Ell-ena, your AI assistant. How can I help you today?", isUser: false, timestamp: DateTime.now(), ), @@ -124,7 +125,7 @@ class _ChatScreenState extends State with TickerProviderStateMixin { debugPrint('Error initializing services: $e'); } } - + Future _loadTeamMembers(String teamId) async { try { final members = await _supabaseService.getTeamMembers(teamId); @@ -137,13 +138,14 @@ class _ChatScreenState extends State with TickerProviderStateMixin { debugPrint('Error loading team members: $e'); } } - + Future _loadUserTasksAndTickets() async { try { final tasks = await _supabaseService.getTasks(filterByAssignment: true); - - final tickets = await _supabaseService.getTickets(filterByAssignment: true); - + + final tickets = + await _supabaseService.getTickets(filterByAssignment: true); + if (mounted) { setState(() { _userTasks = tasks; @@ -165,9 +167,10 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } super.dispose(); } - + // Build the listening animation dialog Widget _buildListeningDialog() { + final colorScheme = Theme.of(context).colorScheme; return Dialog( backgroundColor: Colors.transparent, elevation: 0, @@ -176,11 +179,11 @@ class _ChatScreenState extends State with TickerProviderStateMixin { height: 200, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.3), + color: colorScheme.shadow.withOpacity(0.3), blurRadius: 10, spreadRadius: 2, ), @@ -213,7 +216,8 @@ class _ChatScreenState extends State with TickerProviderStateMixin { height: 100 * _waveformController.value, decoration: BoxDecoration( shape: BoxShape.circle, - color: Colors.green.withOpacity(0.2 * (1 - _waveformController.value)), + color: Colors.green.withOpacity( + 0.2 * (1 - _waveformController.value)), ), ), // Middle ripple @@ -222,7 +226,8 @@ class _ChatScreenState extends State with TickerProviderStateMixin { height: 70 * _waveformController.value, decoration: BoxDecoration( shape: BoxShape.circle, - color: Colors.green.withOpacity(0.3 * (1 - _waveformController.value)), + color: Colors.green.withOpacity( + 0.3 * (1 - _waveformController.value)), ), ), // Inner circle with mic icon @@ -247,10 +252,10 @@ class _ChatScreenState extends State with TickerProviderStateMixin { ), ), const SizedBox(height: 20), - const Text( + Text( 'Listening...', style: TextStyle( - color: Colors.white, + color: colorScheme.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), @@ -259,7 +264,7 @@ class _ChatScreenState extends State with TickerProviderStateMixin { Text( 'Tap anywhere to cancel', style: TextStyle( - color: Colors.grey.shade400, + color: colorScheme.onSurfaceVariant, fontSize: 12, ), ), @@ -284,21 +289,21 @@ class _ChatScreenState extends State with TickerProviderStateMixin { }); _scrollToBottom(); - + try { final chatHistory = _getChatHistoryForAI(); - + final response = await _aiService.generateChatResponse( - userMessage, + userMessage, chatHistory, _teamMembers, userTasks: _userTasks, userTickets: _userTickets, ); - + if (response['type'] == 'function_call') { await _handleFunctionCall( - response['function_name'], + response['function_name'], response['arguments'], response['raw_response'], ); @@ -330,12 +335,12 @@ class _ChatScreenState extends State with TickerProviderStateMixin { _scrollToBottom(); } - + List> _getChatHistoryForAI() { - final recentMessages = _messages.length > 10 - ? _messages.sublist(_messages.length - 10) + final recentMessages = _messages.length > 10 + ? _messages.sublist(_messages.length - 10) : _messages; - + return recentMessages.map((message) { return { "role": message.isUser ? "user" : "assistant", @@ -343,9 +348,9 @@ class _ChatScreenState extends State with TickerProviderStateMixin { }; }).toList(); } - + Future _handleFunctionCall( - String functionName, + String functionName, Map arguments, String rawResponse, ) async { @@ -358,12 +363,15 @@ class _ChatScreenState extends State with TickerProviderStateMixin { ), ); }); - + _scrollToBottom(); - + try { - Map result = {'success': false, 'error': 'Function not implemented'}; - + Map result = { + 'success': false, + 'error': 'Function not implemented' + }; + // Execute the appropriate function based on the function name switch (functionName) { case 'create_task': @@ -387,7 +395,7 @@ class _ChatScreenState extends State with TickerProviderStateMixin { default: result = {'success': false, 'error': 'Unknown function'}; } - + // Get a user-friendly response from the AI final responseMessage = await _aiService.handleToolResponse( functionName: functionName, @@ -395,7 +403,7 @@ class _ChatScreenState extends State with TickerProviderStateMixin { rawResponse: rawResponse, result: result, ); - + // Add the response to the chat setState(() { _messages.add( @@ -405,12 +413,12 @@ class _ChatScreenState extends State with TickerProviderStateMixin { timestamp: DateTime.now(), ), ); - + // Add card if successful for creation functions - if (result['success'] == true && - (functionName == 'create_task' || - functionName == 'create_ticket' || - functionName == 'create_meeting')) { + if (result['success'] == true && + (functionName == 'create_task' || + functionName == 'create_ticket' || + functionName == 'create_meeting')) { _messages.add( ChatMessage( text: _getCardText(functionName, arguments, result), @@ -422,10 +430,10 @@ class _ChatScreenState extends State with TickerProviderStateMixin { ), ); } - + _isProcessing = false; }); - + // Refresh tasks and tickets if we just queried them if (functionName == 'query_tasks' || functionName == 'query_tickets') { _loadUserTasksAndTickets(); @@ -435,7 +443,8 @@ class _ChatScreenState extends State with TickerProviderStateMixin { setState(() { _messages.add( ChatMessage( - text: "Sorry, I encountered an error while processing your request.", + text: + "Sorry, I encountered an error while processing your request.", isUser: false, timestamp: DateTime.now(), ), @@ -443,20 +452,21 @@ class _ChatScreenState extends State with TickerProviderStateMixin { _isProcessing = false; }); } - + _scrollToBottom(); } - + // Create a task using the Supabase service - Future> _createTask(Map arguments) async { + Future> _createTask( + Map arguments) async { try { if (!_supabaseService.isInitialized) { return {'success': false, 'error': 'Service not initialized'}; } - + final title = arguments['title'] as String; final description = arguments['description'] as String?; - + // Parse due date if provided DateTime? dueDate; if (arguments['due_date'] != null) { @@ -466,31 +476,35 @@ class _ChatScreenState extends State with TickerProviderStateMixin { debugPrint('Error parsing due date: $e'); } } - + // Get assigned user ID if provided String? assignedToUserId; final assignedTo = arguments['assigned_to'] as String?; - + if (assignedTo != null && assignedTo.isNotEmpty) { // Check if the value is already a valid UUID - final uuidPattern = RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false); - + final uuidPattern = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + caseSensitive: false); + if (uuidPattern.hasMatch(assignedTo)) { // It's already a UUID assignedToUserId = assignedTo; } else { // Try to find the user by name final matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase() == assignedTo.toLowerCase(), + (member) => + member['full_name'].toString().toLowerCase() == + assignedTo.toLowerCase(), orElse: () => {}, ); - + if (matchingMember.isNotEmpty && matchingMember['id'] != null) { assignedToUserId = matchingMember['id']; } } } - + // Create the task final result = await _supabaseService.createTask( title: title, @@ -498,50 +512,55 @@ class _ChatScreenState extends State with TickerProviderStateMixin { dueDate: dueDate, assignedToUserId: assignedToUserId, ); - + return result; } catch (e) { debugPrint('Error creating task: $e'); return {'success': false, 'error': e.toString()}; } } - + // Create a ticket using the Supabase service - Future> _createTicket(Map arguments) async { + Future> _createTicket( + Map arguments) async { try { if (!_supabaseService.isInitialized) { return {'success': false, 'error': 'Service not initialized'}; } - + final title = arguments['title'] as String; final description = arguments['description'] as String?; final priority = arguments['priority'] as String; final category = arguments['category'] as String; - + // Get assigned user ID if provided String? assignedToUserId; final assignedTo = arguments['assigned_to'] as String?; - + if (assignedTo != null && assignedTo.isNotEmpty) { // Check if the value is already a valid UUID - final uuidPattern = RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false); - + final uuidPattern = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + caseSensitive: false); + if (uuidPattern.hasMatch(assignedTo)) { // It's already a UUID assignedToUserId = assignedTo; } else { // Try to find the user by name final matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase() == assignedTo.toLowerCase(), + (member) => + member['full_name'].toString().toLowerCase() == + assignedTo.toLowerCase(), orElse: () => {}, ); - + if (matchingMember.isNotEmpty && matchingMember['id'] != null) { assignedToUserId = matchingMember['id']; } } } - + // Create the ticket final result = await _supabaseService.createTicket( title: title, @@ -550,24 +569,25 @@ class _ChatScreenState extends State with TickerProviderStateMixin { category: category, assignedToUserId: assignedToUserId, ); - + return result; } catch (e) { debugPrint('Error creating ticket: $e'); return {'success': false, 'error': e.toString()}; } } - + // Create a meeting using the Supabase service - Future> _createMeeting(Map arguments) async { + Future> _createMeeting( + Map arguments) async { try { if (!_supabaseService.isInitialized) { return {'success': false, 'error': 'Service not initialized'}; } - + final title = arguments['title'] as String; final description = arguments['description'] as String?; - + // Parse meeting date DateTime meetingDate; try { @@ -576,9 +596,9 @@ class _ChatScreenState extends State with TickerProviderStateMixin { debugPrint('Error parsing meeting date: $e'); return {'success': false, 'error': 'Invalid meeting date format'}; } - + final meetingUrl = arguments['meeting_url'] as String?; - + // Create the meeting final result = await _supabaseService.createMeeting( title: title, @@ -586,49 +606,59 @@ class _ChatScreenState extends State with TickerProviderStateMixin { meetingDate: meetingDate, meetingUrl: meetingUrl, ); - + return result; } catch (e) { debugPrint('Error creating meeting: $e'); return {'success': false, 'error': e.toString()}; } } + // Query tasks based on filters - Future> _queryTasks(Map arguments) async { + Future> _queryTasks( + Map arguments) async { try { if (!_supabaseService.isInitialized) { return {'success': false, 'error': 'Service not initialized'}; } - + final status = arguments['status'] as String?; final dueDate = arguments['due_date'] as String?; final assignedToMe = arguments['assigned_to_me'] as bool? ?? false; - final assignedToTeamMember = arguments['assigned_to_team_member'] as String?; - + final assignedToTeamMember = + arguments['assigned_to_team_member'] as String?; + // Find team member ID if name was provided String? teamMemberId; if (assignedToTeamMember != null && assignedToTeamMember.isNotEmpty) { // Check if it's already a UUID - final uuidPattern = RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false); - + final uuidPattern = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + caseSensitive: false); + if (uuidPattern.hasMatch(assignedToTeamMember)) { teamMemberId = assignedToTeamMember; } else { // Try to find by name - more flexible matching // First try exact match var matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase() == assignedToTeamMember.toLowerCase(), + (member) => + member['full_name'].toString().toLowerCase() == + assignedToTeamMember.toLowerCase(), orElse: () => {}, ); - + // If no exact match, try partial match if (matchingMember.isEmpty) { matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase().contains(assignedToTeamMember.toLowerCase()), + (member) => member['full_name'] + .toString() + .toLowerCase() + .contains(assignedToTeamMember.toLowerCase()), orElse: () => {}, ); } - + // Try matching first name only if (matchingMember.isEmpty) { for (var member in _teamMembers) { @@ -640,20 +670,23 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } } } - + if (matchingMember.isNotEmpty && matchingMember['id'] != null) { teamMemberId = matchingMember['id']; - debugPrint('Found team member: ${matchingMember['full_name']} with ID: $teamMemberId'); + debugPrint( + 'Found team member: ${matchingMember['full_name']} with ID: $teamMemberId'); } else { - debugPrint('Could not find team member with name: $assignedToTeamMember'); - debugPrint('Available team members: ${_teamMembers.map((m) => m['full_name']).join(', ')}'); + debugPrint( + 'Could not find team member with name: $assignedToTeamMember'); + debugPrint( + 'Available team members: ${_teamMembers.map((m) => m['full_name']).join(', ')}'); } } } - + // Get tasks with filters List> tasks; - + if (teamMemberId != null) { // For team member queries, we need to manually filter results tasks = await _supabaseService.getTasks( @@ -661,11 +694,10 @@ class _ChatScreenState extends State with TickerProviderStateMixin { filterByStatus: status != null && status != 'all' ? status : null, filterByDueDate: dueDate, ); - + // Filter by assigned team member - tasks = tasks.where((task) => - task['assigned_to'] == teamMemberId - ).toList(); + tasks = + tasks.where((task) => task['assigned_to'] == teamMemberId).toList(); } else { tasks = await _supabaseService.getTasks( filterByAssignment: assignedToMe, @@ -673,14 +705,14 @@ class _ChatScreenState extends State with TickerProviderStateMixin { filterByDueDate: dueDate, ); } - + // Update local cache if (mounted) { setState(() { _userTasks = tasks; }); } - + return { 'success': true, 'tasks': tasks, @@ -691,43 +723,52 @@ class _ChatScreenState extends State with TickerProviderStateMixin { return {'success': false, 'error': e.toString()}; } } - + // Query tickets based on filters - Future> _queryTickets(Map arguments) async { + Future> _queryTickets( + Map arguments) async { try { if (!_supabaseService.isInitialized) { return {'success': false, 'error': 'Service not initialized'}; } - + final status = arguments['status'] as String?; final priority = arguments['priority'] as String?; final assignedToMe = arguments['assigned_to_me'] as bool? ?? false; - final assignedToTeamMember = arguments['assigned_to_team_member'] as String?; - + final assignedToTeamMember = + arguments['assigned_to_team_member'] as String?; + // Find team member ID if name was provided String? teamMemberId; if (assignedToTeamMember != null && assignedToTeamMember.isNotEmpty) { // Check if it's already a UUID - final uuidPattern = RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false); - + final uuidPattern = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + caseSensitive: false); + if (uuidPattern.hasMatch(assignedToTeamMember)) { teamMemberId = assignedToTeamMember; } else { // Try to find by name - more flexible matching // First try exact match var matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase() == assignedToTeamMember.toLowerCase(), + (member) => + member['full_name'].toString().toLowerCase() == + assignedToTeamMember.toLowerCase(), orElse: () => {}, ); - + // If no exact match, try partial match if (matchingMember.isEmpty) { matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase().contains(assignedToTeamMember.toLowerCase()), + (member) => member['full_name'] + .toString() + .toLowerCase() + .contains(assignedToTeamMember.toLowerCase()), orElse: () => {}, ); } - + // Try matching first name only if (matchingMember.isEmpty) { for (var member in _teamMembers) { @@ -739,47 +780,52 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } } } - + if (matchingMember.isNotEmpty && matchingMember['id'] != null) { teamMemberId = matchingMember['id']; - debugPrint('Found team member: ${matchingMember['full_name']} with ID: $teamMemberId'); + debugPrint( + 'Found team member: ${matchingMember['full_name']} with ID: $teamMemberId'); } else { - debugPrint('Could not find team member with name: $assignedToTeamMember'); - debugPrint('Available team members: ${_teamMembers.map((m) => m['full_name']).join(', ')}'); + debugPrint( + 'Could not find team member with name: $assignedToTeamMember'); + debugPrint( + 'Available team members: ${_teamMembers.map((m) => m['full_name']).join(', ')}'); } } } - + // Get tickets with filters List> tickets; - + if (teamMemberId != null) { // For team member queries, we need to manually filter results tickets = await _supabaseService.getTickets( filterByAssignment: false, // Don't filter by current user filterByStatus: status != null && status != 'all' ? status : null, - filterByPriority: priority != null && priority != 'all' ? priority : null, + filterByPriority: + priority != null && priority != 'all' ? priority : null, ); - + // Filter by assigned team member - tickets = tickets.where((ticket) => - ticket['assigned_to'] == teamMemberId - ).toList(); + tickets = tickets + .where((ticket) => ticket['assigned_to'] == teamMemberId) + .toList(); } else { tickets = await _supabaseService.getTickets( filterByAssignment: assignedToMe, filterByStatus: status != null && status != 'all' ? status : null, - filterByPriority: priority != null && priority != 'all' ? priority : null, + filterByPriority: + priority != null && priority != 'all' ? priority : null, ); } - + // Update local cache if (mounted) { setState(() { _userTickets = tickets; }); } - + return { 'success': true, 'tickets': tickets, @@ -790,14 +836,15 @@ class _ChatScreenState extends State with TickerProviderStateMixin { return {'success': false, 'error': e.toString()}; } } - + // Modify an existing task, ticket, or meeting - Future> _modifyItem(Map arguments) async { + Future> _modifyItem( + Map arguments) async { try { if (!_supabaseService.isInitialized) { return {'success': false, 'error': 'Service not initialized'}; } - + final itemType = arguments['item_type'] as String; final itemId = arguments['item_id'] as String; final title = arguments['title'] as String?; @@ -807,13 +854,15 @@ class _ChatScreenState extends State with TickerProviderStateMixin { final priority = arguments['priority'] as String?; final meetingDate = arguments['meeting_date'] as String?; final assignedTo = arguments['assigned_to'] as String?; - + // Get assigned user ID if provided String? assignedToUserId; if (assignedTo != null && assignedTo.isNotEmpty) { // Check if the value is already a valid UUID - final uuidPattern = RegExp(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', caseSensitive: false); - + final uuidPattern = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + caseSensitive: false); + if (uuidPattern.hasMatch(assignedTo)) { // It's already a UUID assignedToUserId = assignedTo; @@ -821,18 +870,23 @@ class _ChatScreenState extends State with TickerProviderStateMixin { // Try to find by name - more flexible matching // First try exact match var matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase() == assignedTo.toLowerCase(), + (member) => + member['full_name'].toString().toLowerCase() == + assignedTo.toLowerCase(), orElse: () => {}, ); - + // If no exact match, try partial match if (matchingMember.isEmpty) { matchingMember = _teamMembers.firstWhere( - (member) => member['full_name'].toString().toLowerCase().contains(assignedTo.toLowerCase()), + (member) => member['full_name'] + .toString() + .toLowerCase() + .contains(assignedTo.toLowerCase()), orElse: () => {}, ); } - + // Try matching first name only if (matchingMember.isEmpty) { for (var member in _teamMembers) { @@ -844,25 +898,28 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } } } - + if (matchingMember.isNotEmpty && matchingMember['id'] != null) { assignedToUserId = matchingMember['id']; - debugPrint('Found team member: ${matchingMember['full_name']} with ID: $assignedToUserId'); + debugPrint( + 'Found team member: ${matchingMember['full_name']} with ID: $assignedToUserId'); } else { debugPrint('Could not find team member with name: $assignedTo'); - debugPrint('Available team members: ${_teamMembers.map((m) => m['full_name']).join(', ')}'); + debugPrint( + 'Available team members: ${_teamMembers.map((m) => m['full_name']).join(', ')}'); } } } - + // Prepare update data based on item type Map updateData = {}; - + if (title != null) updateData['title'] = title; if (description != null) updateData['description'] = description; if (status != null) updateData['status'] = status; - if (assignedToUserId != null) updateData['assigned_to'] = assignedToUserId; - + if (assignedToUserId != null) + updateData['assigned_to'] = assignedToUserId; + // Add type-specific fields switch (itemType) { case 'task': @@ -875,11 +932,11 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } } break; - + case 'ticket': if (priority != null) updateData['priority'] = priority; break; - + case 'meeting': if (meetingDate != null) { try { @@ -890,18 +947,21 @@ class _ChatScreenState extends State with TickerProviderStateMixin { } } break; - + default: return {'success': false, 'error': 'Invalid item type'}; } - + if (updateData.isEmpty) { return {'success': false, 'error': 'No changes specified'}; } - + // Update the item in the database - Map result = {'success': false, 'error': 'No changes made'}; - + Map result = { + 'success': false, + 'error': 'No changes made' + }; + switch (itemType) { case 'task': // For tasks, we need to handle different update methods based on what's changing @@ -911,13 +971,13 @@ class _ChatScreenState extends State with TickerProviderStateMixin { status: status, ); } - + // Handle other task updates as needed // Note: This is a simplified implementation - for a complete solution, // you would need to add methods to SupabaseService to handle all fields - + break; - + case 'ticket': // For tickets, we need to handle different update methods based on what's changing if (status != null) { @@ -931,45 +991,46 @@ class _ChatScreenState extends State with TickerProviderStateMixin { priority: priority, ); } - + // Handle other ticket updates as needed - + break; - + case 'meeting': // For meetings, we need to get the current meeting details first - final meetingDetails = await _supabaseService.getMeetingDetails(itemId); + final meetingDetails = + await _supabaseService.getMeetingDetails(itemId); if (meetingDetails != null) { // Update with new values, keeping existing ones if not provided final updatedMeeting = await _supabaseService.updateMeeting( meetingId: itemId, title: title ?? meetingDetails['title'], description: description ?? meetingDetails['description'], - meetingDate: meetingDate != null - ? DateTime.parse(meetingDate) - : DateTime.parse(meetingDetails['meeting_date']), + meetingDate: meetingDate != null + ? DateTime.parse(meetingDate) + : DateTime.parse(meetingDetails['meeting_date']), meetingUrl: meetingDetails['meeting_url'], ); - + result = updatedMeeting; } break; - + default: return {'success': false, 'error': 'Invalid item type'}; } - + if (result['success'] == true) { _loadUserTasksAndTickets(); } - + return result; } catch (e) { debugPrint('Error modifying item: $e'); return {'success': false, 'error': e.toString()}; } } - + String _getCardType(String functionName) { switch (functionName) { case 'create_task': @@ -982,9 +1043,10 @@ class _ChatScreenState extends State with TickerProviderStateMixin { return 'generic'; } } - + // Get card text based on function name and arguments - String _getCardText(String functionName, Map arguments, Map result) { + String _getCardText(String functionName, Map arguments, + Map result) { switch (functionName) { case 'create_task': return arguments['title'] ?? 'New Task'; @@ -1011,28 +1073,37 @@ class _ChatScreenState extends State with TickerProviderStateMixin { void _navigateToItem(ChatMessage message) { try { - if (message.cardType == 'task' && message.cardData != null && message.cardData!['task'] != null) { + if (message.cardType == 'task' && + message.cardData != null && + message.cardData!['task'] != null) { // Navigate to task detail screen Navigator.push( context, MaterialPageRoute( - builder: (context) => TaskDetailScreen(taskId: message.cardData!['task']['id']), + builder: (context) => + TaskDetailScreen(taskId: message.cardData!['task']['id']), ), ); - } else if (message.cardType == 'ticket' && message.cardData != null && message.cardData!['ticket'] != null) { + } else if (message.cardType == 'ticket' && + message.cardData != null && + message.cardData!['ticket'] != null) { // Navigate to ticket detail screen Navigator.push( context, MaterialPageRoute( - builder: (context) => TicketDetailScreen(ticketId: message.cardData!['ticket']['id']), + builder: (context) => + TicketDetailScreen(ticketId: message.cardData!['ticket']['id']), ), ); - } else if (message.cardType == 'meeting' && message.cardData != null && message.cardData!['meeting'] != null) { + } else if (message.cardType == 'meeting' && + message.cardData != null && + message.cardData!['meeting'] != null) { // Navigate to meeting detail screen Navigator.push( context, MaterialPageRoute( - builder: (context) => MeetingDetailScreen(meetingId: message.cardData!['meeting']['id']), + builder: (context) => MeetingDetailScreen( + meetingId: message.cardData!['meeting']['id']), ), ); } else { @@ -1058,7 +1129,8 @@ class _ChatScreenState extends State with TickerProviderStateMixin { Future _toggleListening() async { if (!_speechAvailable) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Speech recognition not available on this device')), + const SnackBar( + content: Text('Speech recognition not available on this device')), ); return; } @@ -1067,22 +1139,22 @@ class _ChatScreenState extends State with TickerProviderStateMixin { setState(() => _isListening = false); return; } - + // Show the listening animation dialog if (mounted) { showDialog( context: context, barrierDismissible: true, builder: (context) => _buildListeningDialog(), - ).then((_) { - // Stop listening if dialog was dismissed - if (_isListening && _speech.isListening) { - _speech.stop(); - setState(() => _isListening = false); - } - } ); + ).then((_) { + // Stop listening if dialog was dismissed + if (_isListening && _speech.isListening) { + _speech.stop(); + setState(() => _isListening = false); + } + }); } - + setState(() => _isListening = true); await _speech.listen( onResult: (result) { @@ -1101,17 +1173,18 @@ class _ChatScreenState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Column( children: [ Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: colorScheme.shadow.withOpacity(0.2), offset: const Offset(0, 2), blurRadius: 4, ), @@ -1131,21 +1204,22 @@ class _ChatScreenState extends State with TickerProviderStateMixin { child: const Icon(Icons.smart_toy, color: Colors.green), ), const SizedBox(width: 12), - const Column( + Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Chat with Ell-ena', style: TextStyle( - color: Colors.white, + color: colorScheme.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( 'Your AI Assistant', - style: TextStyle(color: Colors.grey, fontSize: 14), + style: TextStyle( + color: colorScheme.onSurfaceVariant, fontSize: 14), ), ], ), @@ -1167,68 +1241,68 @@ class _ChatScreenState extends State with TickerProviderStateMixin { ), ), Expanded( - child: - _messages.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.chat_bubble_outline, - size: 64, - color: Colors.grey.shade700, + child: _messages.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat_bubble_outline, + size: 64, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + 'Start a conversation with Ell-ena', + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: 16, ), - const SizedBox(height: 16), - Text( - 'Start a conversation with Ell-ena', - style: TextStyle( - color: Colors.grey.shade500, - fontSize: 16, + ), + ], + ), + ) + : ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: _messages.length + (_isProcessing ? 1 : 0), + itemBuilder: (context, index) { + if (_isProcessing && index == _messages.length) { + // Show typing indicator + return Align( + alignment: Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(20), ), - ), - ], - ), - ) - : ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(16), - itemCount: _messages.length + (_isProcessing ? 1 : 0), - itemBuilder: (context, index) { - if (_isProcessing && index == _messages.length) { - // Show typing indicator - return Align( - alignment: Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 8), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(20), - ), - child: LoadingAnimationWidget.staggeredDotsWave( - color: Colors.green, - size: 24, - ), + child: LoadingAnimationWidget.staggeredDotsWave( + color: Colors.green, + size: 24, ), - ); - } - - final message = _messages[index]; - if (message.isCard == true) { - return _ItemCard( - message: message, - onViewItem: () => _navigateToItem(message), - ); - } - return _ChatBubble(message: message); - }, - ), + ), + ); + } + + final message = _messages[index]; + if (message.isCard == true) { + return _ItemCard( + message: message, + onViewItem: () => _navigateToItem(message), + ); + } + return _ChatBubble(message: message); + }, + ), ), Container( padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Color(0xFF2D2D2D), - borderRadius: BorderRadius.only( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), @@ -1239,15 +1313,16 @@ class _ChatScreenState extends State with TickerProviderStateMixin { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: Colors.grey.shade800, + color: colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(24), ), child: TextField( controller: _messageController, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( + style: TextStyle(color: colorScheme.onSurface), + decoration: InputDecoration( hintText: 'Type your message...', - hintStyle: TextStyle(color: Colors.grey), + hintStyle: + TextStyle(color: colorScheme.onSurfaceVariant), border: InputBorder.none, ), onSubmitted: (_) => _sendMessage(), @@ -1308,57 +1383,65 @@ class _ChatBubble extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final bubbleTextColor = + message.isUser ? Colors.white : colorScheme.onSurface; + final bubbleColor = + message.isUser ? Colors.green : colorScheme.surfaceVariant; return Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: Row( - mainAxisAlignment: message.isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: + message.isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!message.isUser) _buildAvatar(isUser: false), + if (!message.isUser) _buildAvatar(context, isUser: false), const SizedBox(width: 8), Flexible( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - color: message.isUser ? Colors.green : Colors.grey.shade800, + color: bubbleColor, borderRadius: BorderRadius.circular(20), ), - child: _buildFormattedText(message.text), + child: + _buildFormattedText(context, message.text, bubbleTextColor), ), ), const SizedBox(width: 8), - if (message.isUser) _buildAvatar(isUser: true), + if (message.isUser) _buildAvatar(context, isUser: true), ], ), ); } - + // Build formatted text that handles markdown-like formatting - Widget _buildFormattedText(String text) { + Widget _buildFormattedText( + BuildContext context, String text, Color textColor) { // Check if text contains formatting indicators - final containsFormatting = text.contains('*') || - text.contains('•') || - text.contains('📅') || - text.contains('🕒'); - + final containsFormatting = text.contains('*') || + text.contains('•') || + text.contains('📅') || + text.contains('🕒'); + if (!containsFormatting) { // Simple text without formatting - return Text(text, style: const TextStyle(color: Colors.white)); + return Text(text, style: TextStyle(color: textColor)); } - + // Split the text by lines to handle each line separately final lines = text.split('\n'); final widgets = []; - + for (int i = 0; i < lines.length; i++) { final line = lines[i]; - + // Handle empty lines if (line.isEmpty) { widgets.add(const SizedBox(height: 8)); continue; } - + // Handle headers (lines with asterisks) if (line.startsWith('*') && line.endsWith('*')) { final content = line.substring(1, line.length - 1).trim(); @@ -1367,8 +1450,8 @@ class _ChatBubble extends StatelessWidget { padding: const EdgeInsets.only(bottom: 4), child: Text( content, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: textColor, fontWeight: FontWeight.bold, fontSize: 16, ), @@ -1384,12 +1467,14 @@ class _ChatBubble extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('•', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + Text('•', + style: TextStyle( + color: textColor, fontWeight: FontWeight.bold)), const SizedBox(width: 8), Expanded( child: Text( line.substring(1).trim(), - style: const TextStyle(color: Colors.white), + style: TextStyle(color: textColor), ), ), ], @@ -1405,8 +1490,9 @@ class _ChatBubble extends StatelessWidget { child: Text( line, style: TextStyle( - color: Colors.white, - fontWeight: line.startsWith('📅') ? FontWeight.bold : FontWeight.normal, + color: textColor, + fontWeight: + line.startsWith('📅') ? FontWeight.bold : FontWeight.normal, fontSize: line.startsWith('📅') ? 16 : 14, ), ), @@ -1422,8 +1508,8 @@ class _ChatBubble extends StatelessWidget { padding: const EdgeInsets.only(top: 8, bottom: 4), child: Text( content, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: textColor, fontWeight: FontWeight.bold, ), ), @@ -1437,7 +1523,7 @@ class _ChatBubble extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8), child: Container( height: 1, - color: Colors.grey.shade600, + color: Theme.of(context).dividerColor, ), ), ); @@ -1449,35 +1535,36 @@ class _ChatBubble extends StatelessWidget { padding: const EdgeInsets.only(bottom: 4), child: Text( line, - style: const TextStyle(color: Colors.white), + style: TextStyle(color: textColor), ), ), ); } } - + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: widgets, ); } - - Widget _buildAvatar({required bool isUser}) { + + Widget _buildAvatar(BuildContext context, {required bool isUser}) { + final colorScheme = Theme.of(context).colorScheme; return Container( width: 36, height: 36, decoration: BoxDecoration( - color: isUser ? Colors.green.shade700 : Colors.grey.shade700, + color: isUser ? Colors.green.shade700 : colorScheme.surfaceVariant, shape: BoxShape.circle, border: Border.all( - color: isUser ? Colors.green.shade300 : Colors.grey.shade500, + color: isUser ? Colors.green.shade300 : colorScheme.outline, width: 1, ), ), child: Center( child: Icon( isUser ? Icons.person : Icons.smart_toy, - color: Colors.white, + color: isUser ? Colors.white : colorScheme.onSurface, size: 20, ), ), @@ -1496,7 +1583,7 @@ class _ItemCard extends StatelessWidget { // Set icon and title based on card type IconData icon; String title; - + switch (message.cardType) { case 'task': icon = Icons.task_alt; @@ -1515,10 +1602,11 @@ class _ItemCard extends StatelessWidget { title = 'Item Created'; } + final colorScheme = Theme.of(context).colorScheme; return Container( margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.green.withOpacity(0.3), width: 1), ), @@ -1549,12 +1637,16 @@ class _ItemCard extends StatelessWidget { if (message.cardType == 'task' || message.cardType == 'ticket') Text( 'Created just now', - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, fontSize: 12), ), - if (message.cardType == 'meeting' && message.cardData != null && message.cardData!['meeting'] != null) + if (message.cardType == 'meeting' && + message.cardData != null && + message.cardData!['meeting'] != null) Text( _formatDate(message.cardData!['meeting']['meeting_date']), - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, fontSize: 12), ), ], ), @@ -1566,7 +1658,7 @@ class _ItemCard extends StatelessWidget { children: [ Text( message.text, - style: const TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: colorScheme.onSurface, fontSize: 16), ), const SizedBox(height: 16), Row( @@ -1608,10 +1700,10 @@ class _ItemCard extends StatelessWidget { ), ); } - + String _formatDate(String? dateString) { if (dateString == null) return ''; - + try { final date = DateTime.parse(dateString); return DateFormat('MMM d, yyyy • h:mm a').format(date); diff --git a/lib/screens/home/dashboard_screen.dart b/lib/screens/home/dashboard_screen.dart index 4a3dca4..79a319f 100644 --- a/lib/screens/home/dashboard_screen.dart +++ b/lib/screens/home/dashboard_screen.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import '../../widgets/custom_widgets.dart'; import 'package:fl_chart/fl_chart.dart'; @@ -56,20 +55,19 @@ class _DashboardScreenState extends State Future _onRefresh() async { await _loadData(); } - + void _showTeamSwitcher() { showDialog( context: context, builder: (context) { return AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), - title: const Text( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( 'Switch Team', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), content: SizedBox( width: double.maxFinite, @@ -79,32 +77,34 @@ class _DashboardScreenState extends State itemBuilder: (context, index) { final team = _userTeams[index]; final isCurrentTeam = team['id'] == _currentTeamId; - + + final scheme = Theme.of(context).colorScheme; return ListTile( title: Text( team['name'] ?? 'Team', style: TextStyle( - color: Colors.white, - fontWeight: isCurrentTeam ? FontWeight.bold : FontWeight.normal, + color: scheme.onSurface, + fontWeight: + isCurrentTeam ? FontWeight.bold : FontWeight.normal, ), ), subtitle: Text( 'Team Code: ${team['team_code'] ?? 'N/A'}', style: TextStyle( - color: Colors.grey.shade400, + color: scheme.onSurfaceVariant, fontSize: 12, ), ), leading: CircleAvatar( - backgroundColor: isCurrentTeam - ? Colors.green.shade400 + backgroundColor: isCurrentTeam + ? Colors.green.shade400 : Colors.grey.shade700, child: Text( (team['name'] as String? ?? 'T')[0].toUpperCase(), style: const TextStyle(color: Colors.white), ), ), - trailing: isCurrentTeam + trailing: isCurrentTeam ? Icon(Icons.check, color: Colors.green.shade400) : null, onTap: () { @@ -124,7 +124,8 @@ class _DashboardScreenState extends State }, child: Text( 'Cancel', - style: TextStyle(color: Colors.grey.shade400), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), ), ), ], @@ -132,16 +133,16 @@ class _DashboardScreenState extends State }, ); } - + Future _switchTeam(String teamId) async { try { setState(() { _isLoading = true; }); - + final supa = SupabaseService(); final result = await supa.switchTeam(teamId); - + if (result['success'] == true) { // Reload data with new team await _loadData(); @@ -161,7 +162,7 @@ class _DashboardScreenState extends State setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error switching team: $e'), @@ -182,23 +183,25 @@ class _DashboardScreenState extends State // Get user profile with team information final profile = await supa.getCurrentUserProfile(forceRefresh: true); - + // Set username _userName = (profile?['full_name'] as String?)?.trim(); - + // Set current team if (profile != null && profile['team_id'] != null) { _currentTeamId = profile['team_id']; _currentTeamName = profile['teams']?['name'] ?? 'My Team'; } - + // Fetch all teams associated with the user's email final userEmail = profile?['email'] as String?; if (userEmail != null) { try { final teamsResponse = await supa.getUserTeams(userEmail); - if (teamsResponse['success'] == true && teamsResponse['teams'] != null) { - _userTeams = List>.from(teamsResponse['teams']); + if (teamsResponse['success'] == true && + teamsResponse['teams'] != null) { + _userTeams = + List>.from(teamsResponse['teams']); } } catch (e) { debugPrint('Error fetching user teams: $e'); @@ -222,12 +225,15 @@ class _DashboardScreenState extends State final meetings = List>.from(results[2] as List); _tasksTotal = tasks.length; - _tasksInProgress = tasks.where((t) => t['status'] == 'in_progress').length; + _tasksInProgress = + tasks.where((t) => t['status'] == 'in_progress').length; _tasksCompleted = tasks.where((t) => t['status'] == 'completed').length; // Build completion series for last 7 days final now = DateTime.now(); - final Map dayIndexToCompleted = {for (var i = 0; i < 7; i++) i: 0}; + final Map dayIndexToCompleted = { + for (var i = 0; i < 7; i++) i: 0 + }; for (final t in tasks) { if (t['status'] == 'completed') { final ts = (t['updated_at'] ?? t['created_at'])?.toString(); @@ -235,7 +241,8 @@ class _DashboardScreenState extends State final updated = DateTime.tryParse(ts); if (updated != null) { final diffDays = now - .difference(DateTime(updated.year, updated.month, updated.day)) + .difference( + DateTime(updated.year, updated.month, updated.day)) .inDays; if (diffDays >= 0 && diffDays < 7) { final idx = 6 - diffDays; // earlier days on the left @@ -251,29 +258,42 @@ class _DashboardScreenState extends State ); _ticketsOpen = tickets.where((t) => t['status'] == 'open').length; - _ticketsInProgress = tickets.where((t) => t['status'] == 'in_progress').length; + _ticketsInProgress = + tickets.where((t) => t['status'] == 'in_progress').length; _ticketsResolved = tickets.where((t) => t['status'] == 'resolved').length; // Upcoming meetings (next 14 days) final upcoming = >[]; for (final m in meetings) { final md = DateTime.tryParse(m['meeting_date']?.toString() ?? ''); - if (md != null && md.isAfter(now.subtract(const Duration(days: 1))) && md.isBefore(now.add(const Duration(days: 14)))) { + if (md != null && + md.isAfter(now.subtract(const Duration(days: 1))) && + md.isBefore(now.add(const Duration(days: 14)))) { upcoming.add(m); } } upcoming.sort((a, b) { - final ad = DateTime.tryParse(a['meeting_date']?.toString() ?? '') ?? now; - final bd = DateTime.tryParse(b['meeting_date']?.toString() ?? '') ?? now; + final ad = + DateTime.tryParse(a['meeting_date']?.toString() ?? '') ?? now; + final bd = + DateTime.tryParse(b['meeting_date']?.toString() ?? '') ?? now; return ad.compareTo(bd); }); // legacy meetings list no longer used // Recent - tasks.sort((a, b) => (DateTime.tryParse((b['updated_at'] ?? b['created_at'])?.toString() ?? '') ?? now) - .compareTo(DateTime.tryParse((a['updated_at'] ?? a['created_at'])?.toString() ?? '') ?? now)); - tickets.sort((a, b) => (DateTime.tryParse((b['updated_at'] ?? b['created_at'])?.toString() ?? '') ?? now) - .compareTo(DateTime.tryParse((a['updated_at'] ?? a['created_at'])?.toString() ?? '') ?? now)); + tasks.sort((a, b) => (DateTime.tryParse( + (b['updated_at'] ?? b['created_at'])?.toString() ?? '') ?? + now) + .compareTo(DateTime.tryParse( + (a['updated_at'] ?? a['created_at'])?.toString() ?? '') ?? + now)); + tickets.sort((a, b) => (DateTime.tryParse( + (b['updated_at'] ?? b['created_at'])?.toString() ?? '') ?? + now) + .compareTo(DateTime.tryParse( + (a['updated_at'] ?? a['created_at'])?.toString() ?? '') ?? + now)); _recentTasks = tasks.take(3).toList(); _recentTickets = tickets.take(3).toList(); @@ -281,7 +301,9 @@ class _DashboardScreenState extends State final List> items = []; for (final m in meetings) { final dt = DateTime.tryParse(m['meeting_date']?.toString() ?? ''); - if (dt != null && dt.isAfter(now.subtract(const Duration(days: 1))) && dt.isBefore(now.add(const Duration(days: 14)))) { + if (dt != null && + dt.isAfter(now.subtract(const Duration(days: 1))) && + dt.isBefore(now.add(const Duration(days: 14)))) { items.add({ 'type': 'meeting', 'id': m['id'], @@ -296,7 +318,9 @@ class _DashboardScreenState extends State if (t['status'] == 'completed') continue; final due = DateTime.tryParse(t['due_date']?.toString() ?? ''); if (due != null) { - final sameDay = due.year == now.year && due.month == now.month && due.day == now.day; + final sameDay = due.year == now.year && + due.month == now.month && + due.day == now.day; if (sameDay) { items.add({ 'type': 'task', @@ -313,9 +337,12 @@ class _DashboardScreenState extends State for (final tk in tickets) { // Tickets don't have due_date in schema; approximate with created today and open/in_progress final created = DateTime.tryParse(tk['created_at']?.toString() ?? ''); - final isActionable = tk['status'] == 'open' || tk['status'] == 'in_progress'; + final isActionable = + tk['status'] == 'open' || tk['status'] == 'in_progress'; if (created != null && isActionable) { - final sameDay = created.year == now.year && created.month == now.month && created.day == now.day; + final sameDay = created.year == now.year && + created.month == now.month && + created.day == now.day; if (sameDay) { items.add({ 'type': 'ticket', @@ -329,7 +356,8 @@ class _DashboardScreenState extends State } } } - items.sort((a, b) => (a['at'] as DateTime).compareTo(b['at'] as DateTime)); + items + .sort((a, b) => (a['at'] as DateTime).compareTo(b['at'] as DateTime)); _upcomingItems = items.take(8).toList(); if (mounted) { @@ -349,204 +377,209 @@ class _DashboardScreenState extends State @override Widget build(BuildContext context) { if (_isLoading) { - return const Scaffold( - backgroundColor: Color(0xFF1A1A1A), + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: DashboardLoadingSkeleton(), ); } return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: RefreshIndicator( onRefresh: _onRefresh, color: Colors.green, - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ - SliverAppBar( - backgroundColor: Colors.transparent, - elevation: 0, - expandedHeight: 140, - pinned: true, - flexibleSpace: FlexibleSpaceBar( - background: Stack( - children: [ - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - const Color(0xFF2E7D32), - const Color(0xFF1B5E20), - ], - ), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), + SliverAppBar( + backgroundColor: Colors.transparent, + elevation: 0, + expandedHeight: 140, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + background: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF2E7D32), + const Color(0xFF1B5E20), + ], + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), ), ), - ), - CustomPaint( - painter: DotPatternPainter( - color: Colors.white.withOpacity(0.1), + CustomPaint( + painter: DotPatternPainter( + color: Colors.white.withOpacity(0.1), + ), + size: Size(MediaQuery.of(context).size.width, 140), ), - size: Size(MediaQuery.of(context).size.width, 140), - ), - SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - border: Border.all( + SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + child: const Icon( + Icons.person, color: Colors.white, - width: 2, + size: 28, ), ), - child: const Icon( - Icons.person, - color: Colors.white, - size: 28, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - - const Text( - 'Welcome back,', - style: TextStyle( - color: Colors.white70, - fontSize: 12, + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Welcome back,', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), ), - ), - const SizedBox(height: 2), - Row( - children: [ - Flexible( - child: Text( - _userName ?? '—', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, + const SizedBox(height: 2), + Row( + children: [ + Flexible( + child: Text( + _userName ?? '—', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, ), - decoration: BoxDecoration( - color: Colors.white.withOpacity( - 0.2, + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, ), - borderRadius: BorderRadius.circular( - 12, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.trending_up, - color: - Colors.greenAccent.shade100, - size: 14, + decoration: BoxDecoration( + color: Colors.white.withOpacity( + 0.2, ), - const SizedBox(width: 4), - Text( - '+${_tasksCompleted > 0 && _tasksTotal > 0 ? ((_tasksCompleted / (_tasksTotal == 0 ? 1 : _tasksTotal)) * 100).round() : 0}%', - style: TextStyle( - color: Colors.greenAccent.shade100, - fontSize: 12, - fontWeight: FontWeight.bold, - ), + borderRadius: + BorderRadius.circular( + 12, ), - ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.trending_up, + color: Colors + .greenAccent.shade100, + size: 14, + ), + const SizedBox(width: 4), + Text( + '+${_tasksCompleted > 0 && _tasksTotal > 0 ? ((_tasksCompleted / (_tasksTotal == 0 ? 1 : _tasksTotal)) * 100).round() : 0}%', + style: TextStyle( + color: Colors + .greenAccent.shade100, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), ), - ), - ], - ), - if (_userTeams.length > 1) - GestureDetector( - onTap: _showTeamSwitcher, - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _currentTeamName ?? 'My Team', - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 14, + ], + ), + if (_userTeams.length > 1) + GestureDetector( + onTap: _showTeamSwitcher, + child: Padding( + padding: + const EdgeInsets.only(top: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _currentTeamName ?? 'My Team', + style: TextStyle( + color: Colors.white + .withOpacity(0.9), + fontSize: 14, + ), ), - ), - const SizedBox(width: 4), - Icon( - Icons.swap_horiz, - color: Colors.white.withOpacity(0.9), - size: 16, - ), - ], + const SizedBox(width: 4), + Icon( + Icons.swap_horiz, + color: Colors.white + .withOpacity(0.9), + size: 16, + ), + ], + ), ), ), - ), - ], + ], + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), - ), - ], + ], + ), ), ), - ), - SliverToBoxAdapter( - child: Transform.translate( - offset: const Offset(0, -10), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildOverviewCards(), - const SizedBox(height: 24), - _buildAnalyticsSection(), - const SizedBox(height: 24), - _buildUpcomingSection(), - const SizedBox(height: 24), - _buildRecentActivity(), - const SizedBox(height: 24), - ], + SliverToBoxAdapter( + child: Transform.translate( + offset: const Offset(0, -10), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildOverviewCards(), + const SizedBox(height: 24), + _buildAnalyticsSection(), + const SizedBox(height: 24), + _buildUpcomingSection(), + const SizedBox(height: 24), + _buildRecentActivity(), + const SizedBox(height: 24), + ], + ), ), ), ), - ), - ], + ], + ), ), - ), ); } @@ -554,11 +587,11 @@ class _DashboardScreenState extends State return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Theme.of(context).colorScheme.shadow.withOpacity(0.2), blurRadius: 10, offset: const Offset(0, 5), ), @@ -569,13 +602,12 @@ class _DashboardScreenState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Today\'s Overview', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), Container( padding: const EdgeInsets.symmetric( @@ -666,6 +698,7 @@ class _DashboardScreenState extends State IconData icon, Color color, ) { + final colorScheme = Theme.of(context).colorScheme; return Column( children: [ Container( @@ -679,26 +712,34 @@ class _DashboardScreenState extends State const SizedBox(height: 8), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: Colors.white, + color: colorScheme.onSurface, ), ), Text( label, - style: TextStyle(fontSize: 12, color: Colors.grey.shade400), + style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant), ), ], ); } Widget _buildAnalyticsSection() { + final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: colorScheme.shadow.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -706,27 +747,25 @@ class _DashboardScreenState extends State Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Task Completion by Day', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), if (_selectedTimeRange == 1) - Text( - DateFormat('MMMM yyyy').format(DateTime.now()), - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 13, - ), - ), - + Text( + DateFormat('MMMM yyyy').format(DateTime.now()), + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 13, + ), + ), const SizedBox(height: 10), Container( decoration: BoxDecoration( - color: Colors.grey.shade800, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), ), child: Row( @@ -749,155 +788,159 @@ class _DashboardScreenState extends State style: TextStyle(color: Colors.grey.shade500), ), ) - : _selectedTimeRange == 0 - // Bar Chart for Weekly view - ? BarChart( - BarChartData( - gridData: FlGridData( - show: true, - drawVerticalLine: false, - horizontalInterval: 1, - getDrawingHorizontalLine: (value) { - return FlLine( - color: Colors.grey.shade800, - strokeWidth: 1, - ); - }, - ), - titlesData: FlTitlesData( - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 30, - getTitlesWidget: (value, meta) { - return Text( - value.toInt().toString(), - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, - ), + : _selectedTimeRange == 0 + // Bar Chart for Weekly view + ? BarChart( + BarChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 1, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.shade800, + strokeWidth: 1, ); }, ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 30, - getTitlesWidget: (value, meta) { - final now = DateTime.now(); - final idx = value.toInt(); - if (idx < 0 || idx > 6) return const SizedBox(); - final day = now.subtract(Duration(days: 6 - idx)); - return Text( - DateFormat('E').format(day), - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: (value, meta) { + return Text( + value.toInt().toString(), + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 12, + ), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: (value, meta) { + final now = DateTime.now(); + final idx = value.toInt(); + if (idx < 0 || idx > 6) + return const SizedBox(); + final day = + now.subtract(Duration(days: 6 - idx)); + return Text( + DateFormat('E').format(day), + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 12, + ), + ); + }, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + barGroups: _taskCompletionSpots.map((spot) { + return BarChartGroupData( + x: spot.x.toInt(), + barRods: [ + BarChartRodData( + toY: spot.y, + color: Colors.green.shade400, + width: 16, + borderRadius: BorderRadius.circular(4), + backDrawRodData: BackgroundBarChartRodData( + show: true, + toY: + 5, // Maximum expected value or slightly higher + color: + Colors.green.shade400.withOpacity(0.1), + ), ), + ], + ); + }).toList(), + ), + ) + // Line Chart for Monthly view + : LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 1, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.shade800, + strokeWidth: 1, ); }, ), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - ), - borderData: FlBorderData(show: false), - barGroups: _taskCompletionSpots.map((spot) { - return BarChartGroupData( - x: spot.x.toInt(), - barRods: [ - BarChartRodData( - toY: spot.y, + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: (value, meta) { + return Text( + value.toInt().toString(), + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 12, + ), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 5, + reservedSize: 32, + getTitlesWidget: (value, meta) { + final day = value.toInt() + 1; + + return Text( + day.toString(), + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 11, + ), + ); + }, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + lineBarsData: [ + LineChartBarData( + spots: _taskCompletionSpots, + isCurved: true, color: Colors.green.shade400, - width: 16, - borderRadius: BorderRadius.circular(4), - backDrawRodData: BackgroundBarChartRodData( + barWidth: 3, + dotData: FlDotData(show: true), + belowBarData: BarAreaData( show: true, - toY: 5, // Maximum expected value or slightly higher color: Colors.green.shade400.withOpacity(0.1), ), ), ], - ); - }).toList(), - ), - ) - // Line Chart for Monthly view - : LineChart( - LineChartData( - gridData: FlGridData( - show: true, - drawVerticalLine: false, - horizontalInterval: 1, - getDrawingHorizontalLine: (value) { - return FlLine( - color: Colors.grey.shade800, - strokeWidth: 1, - ); - }, - ), - titlesData: FlTitlesData( - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: 30, - getTitlesWidget: (value, meta) { - return Text( - value.toInt().toString(), - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, - ), - ); - }, - ), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: 5, - reservedSize: 32, - getTitlesWidget: (value, meta) { - final day = value.toInt() + 1; - - return Text( - day.toString(), - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 11, - ), - ); - }, - ), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), ), ), - borderData: FlBorderData(show: false), - lineBarsData: [ - LineChartBarData( - spots: _taskCompletionSpots, - isCurved: true, - color: Colors.green.shade400, - barWidth: 3, - dotData: FlDotData(show: true), - belowBarData: BarAreaData( - show: true, - color: Colors.green.shade400.withOpacity(0.1), - ), - ), - ], - ), - ), ), ], ), @@ -926,7 +969,8 @@ class _DashboardScreenState extends State if (updated == null) continue; final diffDays = now - .difference(DateTime(updated.year, updated.month, updated.day)) + .difference( + DateTime(updated.year, updated.month, updated.day)) .inDays; if (diffDays >= 0 && diffDays < 7) { @@ -939,7 +983,8 @@ class _DashboardScreenState extends State _taskCompletionSpots = List.generate( 7, - (i) => FlSpot(i.toDouble(), (dayIndexToCompleted[i] ?? 0).toDouble()), + (i) => FlSpot( + i.toDouble(), (dayIndexToCompleted[i] ?? 0).toDouble()), ); } else { // Month → current calendar month @@ -956,7 +1001,9 @@ class _DashboardScreenState extends State child: Text( text, style: TextStyle( - color: isSelected ? Colors.white : Colors.grey.shade400, + color: isSelected + ? Colors.white + : Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.bold, ), @@ -966,55 +1013,60 @@ class _DashboardScreenState extends State } List _buildCurrentMonthSpots(List> tasks) { - final now = DateTime.now(); + final now = DateTime.now(); - final firstDayOfMonth = DateTime(now.year, now.month, 1); - final firstDayNextMonth = DateTime(now.year, now.month + 1, 1); - final daysInMonth = - firstDayNextMonth.difference(firstDayOfMonth).inDays; + final firstDayOfMonth = DateTime(now.year, now.month, 1); + final firstDayNextMonth = DateTime(now.year, now.month + 1, 1); + final daysInMonth = firstDayNextMonth.difference(firstDayOfMonth).inDays; - final Map dayCounts = { - for (int i = 0; i < daysInMonth; i++) i: 0, - }; + final Map dayCounts = { + for (int i = 0; i < daysInMonth; i++) i: 0, + }; - for (final t in tasks) { - if (t['status'] != 'completed') continue; + for (final t in tasks) { + if (t['status'] != 'completed') continue; - final ts = (t['updated_at'] ?? t['created_at'])?.toString(); - final date = ts != null ? DateTime.tryParse(ts) : null; - if (date == null) continue; + final ts = (t['updated_at'] ?? t['created_at'])?.toString(); + final date = ts != null ? DateTime.tryParse(ts) : null; + if (date == null) continue; - if (date.year == now.year && date.month == now.month) { - final index = date.day - 1; - dayCounts[index] = (dayCounts[index] ?? 0) + 1; + if (date.year == now.year && date.month == now.month) { + final index = date.day - 1; + dayCounts[index] = (dayCounts[index] ?? 0) + 1; + } } - } - - return List.generate( - daysInMonth, - (i) => FlSpot(i.toDouble(), (dayCounts[i] ?? 0).toDouble()), - ); -} + return List.generate( + daysInMonth, + (i) => FlSpot(i.toDouble(), (dayCounts[i] ?? 0).toDouble()), + ); + } Widget _buildUpcomingSection() { + final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Upcoming', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: colorScheme.shadow.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], ), child: _upcomingItems.isEmpty ? Center( @@ -1022,7 +1074,7 @@ class _DashboardScreenState extends State padding: const EdgeInsets.symmetric(vertical: 16), child: Text( 'Nothing due today or scheduled soon', - style: TextStyle(color: Colors.grey.shade400), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ), ) @@ -1030,7 +1082,9 @@ class _DashboardScreenState extends State children: List.generate(_upcomingItems.length, (index) { final item = _upcomingItems[index]; final dt = item['at'] as DateTime?; - final timeLabel = dt != null ? DateFormat('MMM d • h:mm a').format(dt) : ''; + final timeLabel = dt != null + ? DateFormat('MMM d • h:mm a').format(dt) + : ''; final IconData icon = item['icon'] as IconData; final Color color = item['color'] as Color; return InkWell( @@ -1043,7 +1097,8 @@ class _DashboardScreenState extends State icon, color, ), - if (index < _upcomingItems.length - 1) const Divider(color: Colors.grey), + if (index < _upcomingItems.length - 1) + Divider(color: Theme.of(context).dividerColor), ], ), ); @@ -1060,6 +1115,7 @@ class _DashboardScreenState extends State IconData icon, Color color, ) { + final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( @@ -1079,20 +1135,21 @@ class _DashboardScreenState extends State children: [ Text( title, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( time, - style: TextStyle(color: Colors.grey.shade400, fontSize: 14), + style: TextStyle( + color: colorScheme.onSurfaceVariant, fontSize: 14), ), ], ), ), - Icon(Icons.chevron_right, color: Colors.grey.shade400), + Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), ], ), ); @@ -1124,20 +1181,26 @@ class _DashboardScreenState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Recent Activity', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], ), child: _buildDynamicActivityList(), ), @@ -1168,17 +1231,22 @@ class _DashboardScreenState extends State }); } items.sort((a, b) { - final ta = DateTime.tryParse(a['time']?.toString() ?? '') ?? DateTime.now(); - final tb = DateTime.tryParse(b['time']?.toString() ?? '') ?? DateTime.now(); + final ta = + DateTime.tryParse(a['time']?.toString() ?? '') ?? DateTime.now(); + final tb = + DateTime.tryParse(b['time']?.toString() ?? '') ?? DateTime.now(); return tb.compareTo(ta); }); final limited = items.take(5).toList(); + final colorScheme = Theme.of(context).colorScheme; + if (limited.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: Text('No recent activity', style: TextStyle(color: Colors.grey.shade400)), + child: Text('No recent activity', + style: TextStyle(color: colorScheme.onSurfaceVariant)), ), ); } @@ -1187,7 +1255,8 @@ class _DashboardScreenState extends State children: List.generate(limited.length, (index) { final a = limited[index]; final date = DateTime.tryParse(a['time']?.toString() ?? ''); - final timeLabel = date != null ? DateFormat('MMM d, h:mm a').format(date) : ''; + final timeLabel = + date != null ? DateFormat('MMM d, h:mm a').format(date) : ''; return Column( children: [ Padding( @@ -1200,7 +1269,8 @@ class _DashboardScreenState extends State color: (a['color'] as Color).withOpacity(0.1), shape: BoxShape.circle, ), - child: Icon(a['icon'] as IconData, color: a['color'] as Color, size: 20), + child: Icon(a['icon'] as IconData, + color: a['color'] as Color, size: 20), ), const SizedBox(width: 16), Expanded( @@ -1209,27 +1279,31 @@ class _DashboardScreenState extends State children: [ Text( '${a['type']}: ${a['title']}', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( 'Status: ${a['status']}', - style: TextStyle(color: Colors.grey.shade400, fontSize: 14), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: 14), ), ], ), ), Text( timeLabel, - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, fontSize: 12), ), ], ), ), - if (index < limited.length - 1) const Divider(color: Colors.grey), + if (index < limited.length - 1) + Divider(color: Theme.of(context).dividerColor), ], ); }), @@ -1246,11 +1320,10 @@ class DotPatternPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final paint = - Paint() - ..color = color - ..strokeWidth = 2 - ..strokeCap = StrokeCap.round; + final paint = Paint() + ..color = color + ..strokeWidth = 2 + ..strokeCap = StrokeCap.round; const spacing = 30.0; const dotSize = 2.0; @@ -1265,4 +1338,3 @@ class DotPatternPainter extends CustomPainter { @override bool shouldRepaint(DotPatternPainter oldDelegate) => false; } - diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 46fc229..1ce2e74 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -10,7 +10,7 @@ import 'dashboard_screen.dart'; class HomeScreen extends StatefulWidget { final Map? arguments; - + const HomeScreen({super.key, this.arguments}); @override @@ -42,34 +42,35 @@ class _HomeScreenState extends State curve: Curves.easeOut, reverseCurve: Curves.easeIn, ); - + // Initialize screens _initializeScreens(); - + // Handle initial arguments if provided WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.arguments != null) { - if (widget.arguments!.containsKey('screen') && widget.arguments!['screen'] is int) { + if (widget.arguments!.containsKey('screen') && + widget.arguments!['screen'] is int) { setState(() { _selectedIndex = widget.arguments!['screen']; }); } - + // Handle initial message for chat screen - if (widget.arguments!.containsKey('initial_message') && - widget.arguments!['initial_message'] is String && + if (widget.arguments!.containsKey('initial_message') && + widget.arguments!['initial_message'] is String && _selectedIndex == 3) { // Update the chat screen with the initial message setState(() { - _screens[3] = ChatScreen( - arguments: {'initial_message': widget.arguments!['initial_message']} - ); + _screens[3] = ChatScreen(arguments: { + 'initial_message': widget.arguments!['initial_message'] + }); }); } } }); } - + void _initializeScreens() { _screens = [ const DashboardScreen(), @@ -138,16 +139,15 @@ class _HomeScreenState extends State @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), body: IndexedStack(index: _selectedIndex, children: _screens), bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedIndex, onTap: (index) => setState(() => _selectedIndex = index), type: BottomNavigationBarType.fixed, - backgroundColor: const Color(0xFF2D2D2D), - selectedItemColor: Colors.green, - unselectedItemColor: Colors.white70, + selectedItemColor: theme.colorScheme.primary, + unselectedItemColor: theme.colorScheme.onSurfaceVariant, items: const [ BottomNavigationBarItem( icon: Icon(Icons.dashboard), @@ -182,10 +182,19 @@ class _ChatBubble extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( - color: message.isUser ? Colors.green.shade400 : Colors.grey.shade800, + color: message.isUser + ? Colors.green.shade400 + : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), ), - child: Text(message.text, style: const TextStyle(color: Colors.white)), + child: Text( + message.text, + style: TextStyle( + color: message.isUser + ? Colors.white + : Theme.of(context).colorScheme.onSurface, + ), + ), ), ); } diff --git a/lib/screens/meetings/create_meeting_screen.dart b/lib/screens/meetings/create_meeting_screen.dart index f74a72e..f8df76a 100644 --- a/lib/screens/meetings/create_meeting_screen.dart +++ b/lib/screens/meetings/create_meeting_screen.dart @@ -14,14 +14,15 @@ class _CreateMeetingScreenState extends State { final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _urlController = TextEditingController(); - final _durationController = TextEditingController(text: '60'); // Default to 60 minutes + final _durationController = + TextEditingController(text: '60'); // Default to 60 minutes final _supabaseService = SupabaseService(); - + DateTime? _selectedDate; TimeOfDay? _selectedTime; bool _isLoading = false; bool _isGoogleMeetUrl = true; - + @override void dispose() { _titleController.dispose(); @@ -30,23 +31,23 @@ class _CreateMeetingScreenState extends State { _durationController.dispose(); super.dispose(); } - + // Validate if the URL is a Google Meet URL bool _validateGoogleMeetUrl(String url) { if (url.isEmpty) return true; // Empty URL is valid (not required) return url.contains('meet.google.com'); } - + // Check URL and update state void _checkUrl(String url) { setState(() { _isGoogleMeetUrl = _validateGoogleMeetUrl(url); }); } - + Future _createMeeting() async { if (!_formKey.currentState!.validate()) return; - + if (_selectedDate == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -56,7 +57,7 @@ class _CreateMeetingScreenState extends State { ); return; } - + if (_selectedTime == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -66,23 +67,24 @@ class _CreateMeetingScreenState extends State { ); return; } - + // Validate meeting URL if provided final meetingUrl = _urlController.text.trim(); if (meetingUrl.isNotEmpty && !_validateGoogleMeetUrl(meetingUrl)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Only Google Meet URLs are supported for transcription'), + content: + Text('Only Google Meet URLs are supported for transcription'), backgroundColor: Colors.red, ), ); return; } - + setState(() { _isLoading = true; }); - + try { // Combine date and time final meetingDateTime = DateTime( @@ -92,7 +94,7 @@ class _CreateMeetingScreenState extends State { _selectedTime!.hour, _selectedTime!.minute, ); - + // Parse duration int duration = 60; try { @@ -102,17 +104,17 @@ class _CreateMeetingScreenState extends State { // Default to 60 if parsing fails duration = 60; } - + final result = await _supabaseService.createMeeting( title: _titleController.text.trim(), - description: _descriptionController.text.trim().isNotEmpty - ? _descriptionController.text.trim() + description: _descriptionController.text.trim().isNotEmpty + ? _descriptionController.text.trim() : null, meetingDate: meetingDateTime, meetingUrl: meetingUrl.isNotEmpty ? meetingUrl : null, durationMinutes: duration, ); - + if (mounted) { if (result['success']) { Navigator.pop(context, true); @@ -120,7 +122,7 @@ class _CreateMeetingScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to create meeting: ${result['error']}'), @@ -135,7 +137,7 @@ class _CreateMeetingScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error creating meeting: $e'), @@ -145,56 +147,30 @@ class _CreateMeetingScreenState extends State { } } } - + Future _selectDate() async { final picked = await showDatePicker( context: context, initialDate: _selectedDate ?? DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: const ColorScheme.dark( - primary: Colors.green, - onPrimary: Colors.white, - surface: Color(0xFF2D2D2D), - onSurface: Colors.white, - ), - dialogBackgroundColor: const Color(0xFF1A1A1A), - ), - child: child!, - ); - }, + builder: (context, child) => child!, ); - + if (picked != null) { setState(() { _selectedDate = picked; }); } } - + Future _selectTime() async { final picked = await showTimePicker( context: context, initialTime: _selectedTime ?? TimeOfDay.now(), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: const ColorScheme.dark( - primary: Colors.green, - onPrimary: Colors.white, - surface: Color(0xFF2D2D2D), - onSurface: Colors.white, - ), - dialogBackgroundColor: const Color(0xFF1A1A1A), - ), - child: child!, - ); - }, + builder: (context, child) => child!, ); - + if (picked != null) { setState(() { _selectedTime = picked; @@ -205,9 +181,9 @@ class _CreateMeetingScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text('Create Meeting'), ), body: _isLoading @@ -241,7 +217,7 @@ class _CreateMeetingScreenState extends State { }, ), const SizedBox(height: 16), - + // Description TextFormField( controller: _descriptionController, @@ -259,7 +235,7 @@ class _CreateMeetingScreenState extends State { maxLines: 3, ), const SizedBox(height: 16), - + // Date and Time Row( children: [ @@ -274,14 +250,18 @@ class _CreateMeetingScreenState extends State { ), child: Row( children: [ - const Icon(Icons.calendar_today, color: Colors.grey), + const Icon(Icons.calendar_today, + color: Colors.grey), const SizedBox(width: 8), Text( _selectedDate == null ? 'Select Date *' - : DateFormat('MMM dd, yyyy').format(_selectedDate!), + : DateFormat('MMM dd, yyyy') + .format(_selectedDate!), style: TextStyle( - color: _selectedDate == null ? Colors.grey : Colors.white, + color: _selectedDate == null + ? Colors.grey + : Colors.white, ), ), ], @@ -301,14 +281,17 @@ class _CreateMeetingScreenState extends State { ), child: Row( children: [ - const Icon(Icons.access_time, color: Colors.grey), + const Icon(Icons.access_time, + color: Colors.grey), const SizedBox(width: 8), Text( _selectedTime == null ? 'Select Time *' : _selectedTime!.format(context), style: TextStyle( - color: _selectedTime == null ? Colors.grey : Colors.white, + color: _selectedTime == null + ? Colors.grey + : Colors.white, ), ), ], @@ -319,7 +302,7 @@ class _CreateMeetingScreenState extends State { ], ), const SizedBox(height: 16), - + // Duration TextFormField( controller: _durationController, @@ -350,7 +333,7 @@ class _CreateMeetingScreenState extends State { }, ), const SizedBox(height: 16), - + // Meeting URL TextFormField( controller: _urlController, @@ -360,22 +343,28 @@ class _CreateMeetingScreenState extends State { labelStyle: const TextStyle(color: Colors.grey), enabledBorder: OutlineInputBorder( borderSide: BorderSide( - color: _urlController.text.isNotEmpty && !_isGoogleMeetUrl + color: _urlController.text.isNotEmpty && + !_isGoogleMeetUrl ? Colors.red : Colors.grey, ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: _urlController.text.isNotEmpty && !_isGoogleMeetUrl + color: _urlController.text.isNotEmpty && + !_isGoogleMeetUrl ? Colors.red : Colors.green, ), ), suffixIcon: _urlController.text.isNotEmpty ? Icon( - _isGoogleMeetUrl ? Icons.check_circle : Icons.error, - color: _isGoogleMeetUrl ? Colors.green : Colors.red, + _isGoogleMeetUrl + ? Icons.check_circle + : Icons.error, + color: _isGoogleMeetUrl + ? Colors.green + : Colors.red, ) : null, ), @@ -383,19 +372,20 @@ class _CreateMeetingScreenState extends State { _checkUrl(value); }, ), - + // Warning message for non-Google Meet URLs if (_urlController.text.isNotEmpty && !_isGoogleMeetUrl) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( 'Ellena AI transcription only works with Google Meet URLs', - style: TextStyle(color: Colors.red.shade300, fontSize: 12), + style: TextStyle( + color: Colors.red.shade300, fontSize: 12), ), ), - + const SizedBox(height: 24), - + // Create button SizedBox( width: double.infinity, @@ -417,4 +407,4 @@ class _CreateMeetingScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/meetings/meeting_detail_screen.dart b/lib/screens/meetings/meeting_detail_screen.dart index 422a7cf..c5d6ee0 100644 --- a/lib/screens/meetings/meeting_detail_screen.dart +++ b/lib/screens/meetings/meeting_detail_screen.dart @@ -7,7 +7,7 @@ import '../../widgets/custom_widgets.dart'; class MeetingDetailScreen extends StatefulWidget { final String meetingId; - + const MeetingDetailScreen({ super.key, required this.meetingId, @@ -24,7 +24,7 @@ class _MeetingDetailScreenState extends State { bool _isEditing = false; bool _isAdmin = false; bool _isCreator = false; - + final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _urlController = TextEditingController(); @@ -33,13 +33,13 @@ class _MeetingDetailScreenState extends State { final _durationController = TextEditingController(text: '60'); DateTime? _meetingDate; TimeOfDay? _meetingTime; - + @override void initState() { super.initState(); _loadMeetingDetails(); } - + @override void dispose() { _titleController.dispose(); @@ -50,12 +50,12 @@ class _MeetingDetailScreenState extends State { _durationController.dispose(); super.dispose(); } - + Future _loadMeetingDetails() async { setState(() { _isLoading = true; }); - + try { // Check if user is admin final userProfile = await _supabaseService.getCurrentUserProfile(); @@ -64,31 +64,33 @@ class _MeetingDetailScreenState extends State { _isAdmin = userProfile?['role'] == 'admin'; }); } - + // Get meeting details - final meetingDetails = await _supabaseService.getMeetingDetails(widget.meetingId); - + final meetingDetails = + await _supabaseService.getMeetingDetails(widget.meetingId); + if (mounted && meetingDetails != null) { final userId = _supabaseService.client.auth.currentUser?.id; final isCreator = meetingDetails['created_by'] == userId; - + // Parse meeting date and time final meetingDateTime = DateTime.parse(meetingDetails['meeting_date']); - + setState(() { _meeting = meetingDetails; _isCreator = isCreator; _meetingDate = meetingDateTime; _meetingTime = TimeOfDay.fromDateTime(meetingDateTime); - + // Set initial values for editing _titleController.text = meetingDetails['title'] ?? ''; _descriptionController.text = meetingDetails['description'] ?? ''; _urlController.text = meetingDetails['meeting_url'] ?? ''; _transcriptionController.text = meetingDetails['transcription'] ?? ''; _aiSummaryController.text = meetingDetails['ai_summary'] ?? ''; - _durationController.text = meetingDetails['duration_minutes']?.toString() ?? '60'; - + _durationController.text = + meetingDetails['duration_minutes']?.toString() ?? '60'; + _isLoading = false; }); } else { @@ -108,7 +110,7 @@ class _MeetingDetailScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error loading meeting details: $e'), @@ -118,11 +120,11 @@ class _MeetingDetailScreenState extends State { } } } - + Future _deleteMeeting() async { try { final result = await _supabaseService.deleteMeeting(widget.meetingId); - + if (mounted) { if (result['success']) { Navigator.pop(context, true); @@ -147,7 +149,7 @@ class _MeetingDetailScreenState extends State { } } } - + Future _updateMeeting() async { if (_titleController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -158,7 +160,7 @@ class _MeetingDetailScreenState extends State { ); return; } - + if (_meetingDate == null || _meetingTime == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -168,11 +170,11 @@ class _MeetingDetailScreenState extends State { ); return; } - + setState(() { _isLoading = true; }); - + try { // Combine date and time final meetingDateTime = DateTime( @@ -182,7 +184,7 @@ class _MeetingDetailScreenState extends State { _meetingTime!.hour, _meetingTime!.minute, ); - + // Parse duration int? duration; if (_durationController.text.trim().isNotEmpty) { @@ -194,32 +196,38 @@ class _MeetingDetailScreenState extends State { duration = 60; } } - + final result = await _supabaseService.updateMeeting( meetingId: widget.meetingId, title: _titleController.text.trim(), description: _descriptionController.text.trim(), meetingDate: meetingDateTime, - meetingUrl: _urlController.text.trim().isNotEmpty ? _urlController.text.trim() : null, - transcription: _transcriptionController.text.trim().isNotEmpty ? _transcriptionController.text.trim() : null, - ai_summary: _aiSummaryController.text.trim().isNotEmpty ? _aiSummaryController.text.trim() : null, + meetingUrl: _urlController.text.trim().isNotEmpty + ? _urlController.text.trim() + : null, + transcription: _transcriptionController.text.trim().isNotEmpty + ? _transcriptionController.text.trim() + : null, + ai_summary: _aiSummaryController.text.trim().isNotEmpty + ? _aiSummaryController.text.trim() + : null, durationMinutes: duration, ); - + if (mounted) { if (result['success']) { setState(() { _isEditing = false; _isLoading = false; }); - + // Reload meeting details _loadMeetingDetails(); } else { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error updating meeting: ${result['error']}'), @@ -234,7 +242,7 @@ class _MeetingDetailScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error updating meeting: $e'), @@ -244,10 +252,10 @@ class _MeetingDetailScreenState extends State { } } } - + Future _launchMeetingUrl() async { if (_meeting == null || _meeting!['meeting_url'] == null) return; - + final url = Uri.parse(_meeting!['meeting_url']); if (await canLaunchUrl(url)) { await launchUrl(url); @@ -266,7 +274,8 @@ class _MeetingDetailScreenState extends State { Future _createTicketFromAction(Map action) async { try { final title = (action['item']?.toString() ?? 'Action Item'); - final description = 'Created from meeting action item. Owner: ${action['owner'] ?? 'N/A'} • Deadline: ${action['deadline'] ?? 'N/A'}'; + final description = + 'Created from meeting action item. Owner: ${action['owner'] ?? 'N/A'} • Deadline: ${action['deadline'] ?? 'N/A'}'; const category = 'Meeting Discussion'; const priority = 'medium'; final result = await _supabaseService.createTicket( @@ -278,11 +287,14 @@ class _MeetingDetailScreenState extends State { if (!mounted) return; if (result['success'] == true) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Ticket created'), backgroundColor: Colors.green), + const SnackBar( + content: Text('Ticket created'), backgroundColor: Colors.green), ); } else { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to create ticket: ${result['error']}'), backgroundColor: Colors.red), + SnackBar( + content: Text('Failed to create ticket: ${result['error']}'), + backgroundColor: Colors.red), ); } } catch (e) { @@ -316,11 +328,14 @@ class _MeetingDetailScreenState extends State { if (!mounted) return; if (result['success'] == true) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Task created'), backgroundColor: Colors.green), + const SnackBar( + content: Text('Task created'), backgroundColor: Colors.green), ); } else { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to create task: ${result['error']}'), backgroundColor: Colors.red), + SnackBar( + content: Text('Failed to create task: ${result['error']}'), + backgroundColor: Colors.red), ); } } catch (e) { @@ -330,12 +345,12 @@ class _MeetingDetailScreenState extends State { ); } } - + Future _copyMeetingUrl() async { if (_meeting == null || _meeting!['meeting_url'] == null) return; - + await Clipboard.setData(ClipboardData(text: _meeting!['meeting_url'])); - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -345,56 +360,30 @@ class _MeetingDetailScreenState extends State { ); } } - + Future _selectDate() async { final picked = await showDatePicker( context: context, initialDate: _meetingDate ?? DateTime.now(), firstDate: DateTime.now().subtract(const Duration(days: 365)), lastDate: DateTime.now().add(const Duration(days: 365)), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: const ColorScheme.dark( - primary: Colors.green, - onPrimary: Colors.white, - surface: Color(0xFF2D2D2D), - onSurface: Colors.white, - ), - dialogBackgroundColor: const Color(0xFF1A1A1A), - ), - child: child!, - ); - }, + builder: (context, child) => child!, ); - + if (picked != null) { setState(() { _meetingDate = picked; }); } } - + Future _selectTime() async { final picked = await showTimePicker( context: context, initialTime: _meetingTime ?? TimeOfDay.now(), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: const ColorScheme.dark( - primary: Colors.green, - onPrimary: Colors.white, - surface: Color(0xFF2D2D2D), - onSurface: Colors.white, - ), - dialogBackgroundColor: const Color(0xFF1A1A1A), - ), - child: child!, - ); - }, + builder: (context, child) => child!, ); - + if (picked != null) { setState(() { _meetingTime = picked; @@ -405,29 +394,29 @@ class _MeetingDetailScreenState extends State { @override Widget build(BuildContext context) { if (_isLoading) { - return const Scaffold( - backgroundColor: Color(0xFF1A1A1A), - body: Center(child: CustomLoading()), + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: const Center(child: CustomLoading()), ); } - + if (_meeting == null) { - return const Scaffold( - backgroundColor: Color(0xFF1A1A1A), - body: Center(child: Text('Meeting not found')), + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: const Center(child: Text('Meeting not found')), ); } - + final meetingDateTime = DateTime.parse(_meeting!['meeting_date']); final isUpcoming = meetingDateTime.isAfter(DateTime.now()); final dateFormat = DateFormat('EEEE, MMMM d, yyyy'); final timeFormat = DateFormat('h:mm a'); final canEdit = (_isAdmin || _isCreator) && isUpcoming; - + return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, title: Text(_isEditing ? 'Edit Meeting' : 'Meeting Details'), actions: [ if (_isEditing) @@ -450,18 +439,22 @@ class _MeetingDetailScreenState extends State { ), ], ), - body: _isEditing ? _buildEditForm() : _buildMeetingDetails(dateFormat, timeFormat, isUpcoming), + body: _isEditing + ? _buildEditForm() + : _buildMeetingDetails(dateFormat, timeFormat, isUpcoming), ); } - - Widget _buildMeetingDetails(DateFormat dateFormat, DateFormat timeFormat, bool isUpcoming) { + + Widget _buildMeetingDetails( + DateFormat dateFormat, DateFormat timeFormat, bool isUpcoming) { final meetingDateTime = DateTime.parse(_meeting!['meeting_date']); - + // Determine transcription status String transcriptionStatus = 'Not started'; Color transcriptionStatusColor = Colors.grey.shade600; - - if (_meeting!['transcription'] != null && _meeting!['transcription'].toString().isNotEmpty) { + + if (_meeting!['transcription'] != null && + _meeting!['transcription'].toString().isNotEmpty) { transcriptionStatus = 'Completed'; transcriptionStatusColor = Colors.green.shade400; } else if (_meeting!['transcription_attempted_at'] != null) { @@ -470,15 +463,18 @@ class _MeetingDetailScreenState extends State { } else if (_meeting!['bot_started_at'] != null) { transcriptionStatus = 'In progress'; transcriptionStatusColor = Colors.blue.shade400; - } else if (!isUpcoming && _meeting!['meeting_url'] != null && - _meeting!['meeting_url'].toString().contains('meet.google.com')) { + } else if (!isUpcoming && + _meeting!['meeting_url'] != null && + _meeting!['meeting_url'].toString().contains('meet.google.com')) { transcriptionStatus = 'Pending'; transcriptionStatusColor = Colors.yellow.shade700; - } else if (!_meeting!['meeting_url'].toString().contains('meet.google.com')) { + } else if (!_meeting!['meeting_url'] + .toString() + .contains('meet.google.com')) { transcriptionStatus = 'Not available'; transcriptionStatusColor = Colors.red.shade400; } - + return ListView( padding: const EdgeInsets.all(16), children: [ @@ -497,9 +493,9 @@ class _MeetingDetailScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: isUpcoming - ? Colors.green.shade400.withOpacity(0.2) - : Colors.grey.shade600.withOpacity(0.2), + color: isUpcoming + ? Colors.green.shade400.withOpacity(0.2) + : Colors.grey.shade600.withOpacity(0.2), borderRadius: BorderRadius.circular(16), ), child: Row( @@ -507,14 +503,18 @@ class _MeetingDetailScreenState extends State { children: [ Icon( isUpcoming ? Icons.event_available : Icons.event_busy, - color: isUpcoming ? Colors.green.shade400 : Colors.grey.shade600, + color: isUpcoming + ? Colors.green.shade400 + : Colors.grey.shade600, size: 16, ), const SizedBox(width: 8), Text( isUpcoming ? 'UPCOMING' : 'PAST', style: TextStyle( - color: isUpcoming ? Colors.green.shade400 : Colors.grey.shade600, + color: isUpcoming + ? Colors.green.shade400 + : Colors.grey.shade600, fontSize: 10, fontWeight: FontWeight.bold, ), @@ -525,139 +525,123 @@ class _MeetingDetailScreenState extends State { ], ), const SizedBox(height: 24), - + // Title Text( _meeting!['title'] ?? 'Untitled Meeting', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), - + // Date and time - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - Row( - children: [ - Icon( - Icons.calendar_month, - color: Colors.blue.shade400, - size: 24, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Date', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, - ), - ), - const SizedBox(height: 4), - Text( - dateFormat.format(meetingDateTime), - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), + Builder( + builder: (context) { + final cs = Theme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: cs.shadow.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), ), ], ), - const SizedBox(height: 16), - Row( + child: Column( children: [ - Icon( - Icons.access_time, - color: Colors.orange.shade400, - size: 24, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Time', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, - ), - ), - const SizedBox(height: 4), - Text( - timeFormat.format(meetingDateTime), - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), + Row( + children: [ + Icon(Icons.calendar_month, + color: Colors.blue.shade400, size: 24), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Date', + style: TextStyle( + color: cs.onSurfaceVariant, fontSize: 12)), + const SizedBox(height: 4), + Text(dateFormat.format(meetingDateTime), + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.bold)), + ], ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Icon( - Icons.timer, - color: Colors.purple.shade400, - size: 24, + ), + ], ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Duration', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, - ), + const SizedBox(height: 16), + Row( + children: [ + Icon(Icons.access_time, + color: Colors.orange.shade400, size: 24), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Time', + style: TextStyle( + color: cs.onSurfaceVariant, fontSize: 12)), + const SizedBox(height: 4), + Text(timeFormat.format(meetingDateTime), + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.bold)), + ], ), - const SizedBox(height: 4), - Text( - '${_meeting!['duration_minutes'] ?? 60} minutes', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Icon(Icons.timer, + color: Colors.purple.shade400, size: 24), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Duration', + style: TextStyle( + color: cs.onSurfaceVariant, fontSize: 12)), + const SizedBox(height: 4), + Text( + '${_meeting!['duration_minutes'] ?? 60} minutes', + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.bold)), + ], ), - ], - ), + ), + ], ), ], ), - ], - ), + ); + }, ), const SizedBox(height: 16), - + // Description - if (_meeting!['description'] != null && _meeting!['description'].toString().isNotEmpty) + if (_meeting!['description'] != null && + _meeting!['description'].toString().isNotEmpty) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), ), child: Column( @@ -666,30 +650,31 @@ class _MeetingDetailScreenState extends State { Text( 'Description', style: TextStyle( - color: Colors.grey.shade400, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, ), ), const SizedBox(height: 8), Text( _meeting!['description'], - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 14, ), ), ], ), ), - + const SizedBox(height: 16), - + // Meeting URL - if (_meeting!['meeting_url'] != null && _meeting!['meeting_url'].toString().isNotEmpty) + if (_meeting!['meeting_url'] != null && + _meeting!['meeting_url'].toString().isNotEmpty) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), ), child: Column( @@ -698,7 +683,7 @@ class _MeetingDetailScreenState extends State { Text( 'Meeting URL', style: TextStyle( - color: Colors.grey.shade400, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, ), ), @@ -718,19 +703,23 @@ class _MeetingDetailScreenState extends State { const SizedBox(width: 8), IconButton( onPressed: _copyMeetingUrl, - icon: const Icon(Icons.copy, color: Colors.white), + icon: Icon(Icons.copy, + color: Theme.of(context).colorScheme.onSurface), tooltip: 'Copy URL', ), IconButton( onPressed: _launchMeetingUrl, - icon: const Icon(Icons.open_in_new, color: Colors.white), + icon: Icon(Icons.open_in_new, + color: Theme.of(context).colorScheme.onSurface), tooltip: 'Open URL', ), ], ), - + // Show transcription status for Google Meet URLs - if (_meeting!['meeting_url'].toString().contains('meet.google.com')) + if (_meeting!['meeting_url'] + .toString() + .contains('meet.google.com')) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( @@ -755,16 +744,18 @@ class _MeetingDetailScreenState extends State { ], ), ), - + const SizedBox(height: 16), - + // Manage from AI summary (action items and follow-ups). Hide raw transcription/summary here. if (!isUpcoming) _buildManageSections(), - + const SizedBox(height: 24), - + // Join meeting button - if (isUpcoming && _meeting!['meeting_url'] != null && _meeting!['meeting_url'].toString().isNotEmpty) + if (isUpcoming && + _meeting!['meeting_url'] != null && + _meeting!['meeting_url'].toString().isNotEmpty) ElevatedButton( onPressed: _launchMeetingUrl, style: ElevatedButton.styleFrom( @@ -793,46 +784,47 @@ class _MeetingDetailScreenState extends State { ], ); } - + Widget _buildEditForm() { + final colorScheme = Theme.of(context).colorScheme; return ListView( padding: const EdgeInsets.all(16), children: [ // Title TextFormField( controller: _titleController, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( + style: TextStyle(color: colorScheme.onSurface), + decoration: InputDecoration( labelText: 'Title', - labelStyle: TextStyle(color: Colors.grey), + labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey), + borderSide: BorderSide(color: colorScheme.outline), ), - focusedBorder: OutlineInputBorder( + focusedBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.green), ), ), ), const SizedBox(height: 16), - + // Description TextFormField( controller: _descriptionController, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( + style: TextStyle(color: colorScheme.onSurface), + decoration: InputDecoration( labelText: 'Description', - labelStyle: TextStyle(color: Colors.grey), + labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey), + borderSide: BorderSide(color: colorScheme.outline), ), - focusedBorder: OutlineInputBorder( + focusedBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.green), ), ), maxLines: 3, ), const SizedBox(height: 16), - + // Date and Time Row( children: [ @@ -842,19 +834,22 @@ class _MeetingDetailScreenState extends State { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - border: Border.all(color: Colors.grey), + border: Border.all(color: colorScheme.outline), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ - const Icon(Icons.calendar_today, color: Colors.grey), + Icon(Icons.calendar_today, + color: colorScheme.onSurfaceVariant), const SizedBox(width: 8), Text( _meetingDate == null ? 'Select Date' : DateFormat('MMM dd, yyyy').format(_meetingDate!), style: TextStyle( - color: _meetingDate == null ? Colors.grey : Colors.white, + color: _meetingDate == null + ? colorScheme.onSurfaceVariant + : colorScheme.onSurface, ), ), ], @@ -869,19 +864,22 @@ class _MeetingDetailScreenState extends State { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - border: Border.all(color: Colors.grey), + border: Border.all(color: colorScheme.outline), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ - const Icon(Icons.access_time, color: Colors.grey), + Icon(Icons.access_time, + color: colorScheme.onSurfaceVariant), const SizedBox(width: 8), Text( _meetingTime == null ? 'Select Time' : _meetingTime!.format(context), style: TextStyle( - color: _meetingTime == null ? Colors.grey : Colors.white, + color: _meetingTime == null + ? colorScheme.onSurfaceVariant + : colorScheme.onSurface, ), ), ], @@ -892,46 +890,47 @@ class _MeetingDetailScreenState extends State { ], ), const SizedBox(height: 16), - + // Duration TextFormField( controller: _durationController, - style: const TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), keyboardType: TextInputType.number, - decoration: const InputDecoration( + decoration: InputDecoration( labelText: 'Duration (minutes)', - labelStyle: TextStyle(color: Colors.grey), + labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey), + borderSide: BorderSide(color: colorScheme.outline), ), - focusedBorder: OutlineInputBorder( + focusedBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.green), ), ), ), const SizedBox(height: 16), - + // Meeting URL TextFormField( controller: _urlController, - style: const TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), decoration: InputDecoration( labelText: 'Meeting URL', - labelStyle: const TextStyle(color: Colors.grey), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey), + labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.outline), ), focusedBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.green), ), - helperText: !_urlController.text.contains('meet.google.com') && _urlController.text.isNotEmpty + helperText: !_urlController.text.contains('meet.google.com') && + _urlController.text.isNotEmpty ? 'Ellena AI transcription only works with Google Meet URLs' : null, helperStyle: TextStyle(color: Colors.red.shade300), ), ), const SizedBox(height: 16), - + // Save button SizedBox( width: double.infinity, @@ -956,15 +955,23 @@ class _MeetingDetailScreenState extends State { if (summary == null || summary is! Map || summary.isEmpty) { return Container( padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: const Color(0xFF2D2D2D), borderRadius: BorderRadius.circular(16)), - child: Text('AI summary not available yet.', style: TextStyle(color: Colors.grey.shade400)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), + child: Text('AI summary not available yet.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant)), ); } final actionItems = (summary['action_items'] as List?) ?? []; final followUps = (summary['follow_up_tasks'] as List?) ?? []; - Widget pill({required Color color, required String title, required String subtitle, required VoidCallback onTap}) { + Widget pill( + {required Color color, + required String title, + required String subtitle, + required VoidCallback onTap}) { return InkWell( onTap: onTap, child: Container( @@ -983,13 +990,21 @@ class _MeetingDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + Text(title, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold)), const SizedBox(height: 2), - Text(subtitle, style: TextStyle(color: Colors.grey.shade400, fontSize: 12)), + Text(subtitle, + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12)), ], ), ), - const Icon(Icons.chevron_right, color: Colors.white54), + Icon(Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant), ], ), ), @@ -1001,7 +1016,9 @@ class _MeetingDetailScreenState extends State { children: [ Container( padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: const Color(0xFF2D2D2D), borderRadius: BorderRadius.circular(16)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1009,17 +1026,24 @@ class _MeetingDetailScreenState extends State { children: [ Icon(Icons.topic, color: Colors.amber.shade400, size: 18), const SizedBox(width: 8), - const Text('Manage Important Discussion', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + Text('Manage Important Discussion', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 12), if (actionItems.isEmpty) - Text('No action items detected', style: TextStyle(color: Colors.grey.shade400)) + Text('No action items detected', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant)) else ...actionItems.map((it) { final map = Map.from(it as Map); final title = map['item']?.toString() ?? 'Action Item'; - final subtitle = 'Owner: ${map['owner'] ?? '—'} • Deadline: ${map['deadline'] ?? 'N/A'}'; + final subtitle = + 'Owner: ${map['owner'] ?? '—'} • Deadline: ${map['deadline'] ?? 'N/A'}'; return pill( color: Colors.amber.shade400, title: title, @@ -1033,7 +1057,9 @@ class _MeetingDetailScreenState extends State { const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: const Color(0xFF2D2D2D), borderRadius: BorderRadius.circular(16)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1041,12 +1067,18 @@ class _MeetingDetailScreenState extends State { children: [ Icon(Icons.task_alt, color: Colors.green.shade400, size: 18), const SizedBox(width: 8), - const Text('Manage Tasks', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + Text('Manage Tasks', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 12), if (followUps.isEmpty) - Text('No follow-up tasks detected', style: TextStyle(color: Colors.grey.shade400)) + Text('No follow-up tasks detected', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant)) else ...followUps.map((it) { final map = Map.from(it as Map); @@ -1065,15 +1097,15 @@ class _MeetingDetailScreenState extends State { ], ); } - + void _showDeleteConfirmation(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), - title: const Text( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( 'Delete Meeting', - style: TextStyle(color: Colors.white), + style: Theme.of(context).textTheme.titleLarge, ), content: const Text( 'Are you sure you want to delete this meeting? This action cannot be undone.', @@ -1098,4 +1130,4 @@ class _MeetingDetailScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/meetings/meeting_insights_screen.dart b/lib/screens/meetings/meeting_insights_screen.dart index 8f22a9f..c28f170 100644 --- a/lib/screens/meetings/meeting_insights_screen.dart +++ b/lib/screens/meetings/meeting_insights_screen.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import '../../services/supabase_service.dart'; import 'package:pdf/widgets.dart' as pw; @@ -11,30 +10,36 @@ class MeetingInsightsScreen extends StatefulWidget { final String meetingId; final String initialTab; // 'transcript' or 'summary' - const MeetingInsightsScreen({super.key, required this.meetingId, this.initialTab = 'transcript'}); + const MeetingInsightsScreen( + {super.key, required this.meetingId, this.initialTab = 'transcript'}); @override State createState() => _MeetingInsightsScreenState(); } -class _MeetingInsightsScreenState extends State with SingleTickerProviderStateMixin { +class _MeetingInsightsScreenState extends State + with SingleTickerProviderStateMixin { final SupabaseService _supabase = SupabaseService(); bool _isLoading = true; - Map? _meeting; // includes final_transcription and meeting_summary_json + Map? + _meeting; // includes final_transcription and meeting_summary_json late TabController _tabController; - @override + @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this, initialIndex: widget.initialTab == 'summary' ? 1 : 0); + _tabController = TabController( + length: 2, + vsync: this, + initialIndex: widget.initialTab == 'summary' ? 1 : 0); _load(); } -@override -void dispose() { - _tabController.dispose(); - super.dispose(); -} + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } Future _load() async { setState(() => _isLoading = true); @@ -50,7 +55,9 @@ void dispose() { if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to load meeting: $e'), backgroundColor: Colors.red), + SnackBar( + content: Text('Failed to load meeting: $e'), + backgroundColor: Colors.red), ); } } @@ -59,9 +66,9 @@ void dispose() { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text('Meeting Insights'), bottom: TabBar( controller: _tabController, @@ -112,10 +119,15 @@ void dispose() { child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text(title, style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)), + pw.Text(title, + style: pw.TextStyle( + fontSize: 22, fontWeight: pw.FontWeight.bold)), pw.SizedBox(height: 4), - pw.Text(meetingTitle, style: const pw.TextStyle(fontSize: 14)), - if (meetingDate.isNotEmpty) pw.Text(meetingDate, style: const pw.TextStyle(fontSize: 12)), + pw.Text(meetingTitle, + style: const pw.TextStyle(fontSize: 14)), + if (meetingDate.isNotEmpty) + pw.Text(meetingDate, + style: const pw.TextStyle(fontSize: 12)), ], ), ), @@ -129,7 +141,10 @@ void dispose() { child: pw.RichText( text: pw.TextSpan( children: [ - pw.TextSpan(text: '$speaker: ', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.TextSpan( + text: '$speaker: ', + style: pw.TextStyle( + fontWeight: pw.FontWeight.bold)), pw.TextSpan(text: text), ], ), @@ -150,8 +165,11 @@ void dispose() { build: (ctx) { List bullets(dynamic list) { final l = (list as List?) ?? []; - return l.map((e) => pw.Bullet(text: e.toString())).toList(); + return l + .map((e) => pw.Bullet(text: e.toString())) + .toList(); } + pw.Widget actionItems(dynamic list) { final items = (list as List?) ?? []; return pw.Column( @@ -160,13 +178,19 @@ void dispose() { return pw.Container( padding: const pw.EdgeInsets.all(8), margin: const pw.EdgeInsets.only(bottom: 6), - decoration: pw.BoxDecoration(border: pw.Border.all(color: PdfColors.grey300), borderRadius: pw.BorderRadius.circular(4)), + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.grey300), + borderRadius: pw.BorderRadius.circular(4)), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text(map['item']?.toString() ?? '', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text(map['item']?.toString() ?? '', + style: + pw.TextStyle(fontWeight: pw.FontWeight.bold)), pw.SizedBox(height: 2), - pw.Text('Owner: ${map['owner'] ?? '—'} • Deadline: ${map['deadline'] ?? 'N/A'}', style: const pw.TextStyle(fontSize: 10)), + pw.Text( + 'Owner: ${map['owner'] ?? '—'} • Deadline: ${map['deadline'] ?? 'N/A'}', + style: const pw.TextStyle(fontSize: 10)), ], ), ); @@ -180,30 +204,49 @@ void dispose() { child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ - pw.Text(title, style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)), + pw.Text(title, + style: pw.TextStyle( + fontSize: 22, fontWeight: pw.FontWeight.bold)), pw.SizedBox(height: 4), - pw.Text(meetingTitle, style: const pw.TextStyle(fontSize: 14)), - if (meetingDate.isNotEmpty) pw.Text(meetingDate, style: const pw.TextStyle(fontSize: 12)), + pw.Text(meetingTitle, + style: const pw.TextStyle(fontSize: 14)), + if (meetingDate.isNotEmpty) + pw.Text(meetingDate, + style: const pw.TextStyle(fontSize: 12)), ], ), ), - if (summary == null || summary.isEmpty) pw.Text('No AI summary available') else ...[ - pw.Text('Key Discussion Points', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + if (summary == null || summary.isEmpty) + pw.Text('No AI summary available') + else ...[ + pw.Text('Key Discussion Points', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), ...bullets(summary['key_discussion_points']), pw.SizedBox(height: 8), - pw.Text('Important Decisions', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text('Important Decisions', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), ...bullets(summary['important_decisions']), pw.SizedBox(height: 8), - pw.Text('Action Items', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text('Action Items', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), actionItems(summary['action_items']), pw.SizedBox(height: 8), - pw.Text('Meeting Highlights', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text('Meeting Highlights', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), ...bullets(summary['meeting_highlights']), pw.SizedBox(height: 8), - pw.Text('Follow-up Tasks', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), - actionItems((summary['follow_up_tasks'] as List?)?.map((e) => {'item': e['task'], 'owner': '', 'deadline': e['deadline']}).toList()), + pw.Text('Follow-up Tasks', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + actionItems((summary['follow_up_tasks'] as List?) + ?.map((e) => { + 'item': e['task'], + 'owner': '', + 'deadline': e['deadline'] + }) + .toList()), pw.SizedBox(height: 8), - pw.Text('Overall Summary', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), + pw.Text('Overall Summary', + style: pw.TextStyle(fontWeight: pw.FontWeight.bold)), pw.Text(summary['overall_summary']?.toString() ?? ''), ], ]; @@ -213,7 +256,8 @@ void dispose() { } final bytes = await doc.save(); - String filename = '${title.replaceAll(' ', '_')}_${DateTime.now().millisecondsSinceEpoch}.pdf'; + String filename = + '${title.replaceAll(' ', '_')}_${DateTime.now().millisecondsSinceEpoch}.pdf'; if (Platform.isAndroid) { // Try Downloads directory; if it fails, fall back to temp. @@ -224,7 +268,9 @@ void dispose() { await file.writeAsBytes(bytes); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Saved to ${file.path}'), backgroundColor: Colors.green), + SnackBar( + content: Text('Saved to ${file.path}'), + backgroundColor: Colors.green), ); return; } @@ -237,7 +283,9 @@ void dispose() { await fallbackFile.writeAsBytes(bytes); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Saved to ${fallbackFile.path}'), backgroundColor: Colors.green), + SnackBar( + content: Text('Saved to ${fallbackFile.path}'), + backgroundColor: Colors.green), ); } catch (e) { if (!mounted) return; @@ -268,7 +316,7 @@ void dispose() { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), ), child: Row( @@ -279,7 +327,8 @@ void dispose() { backgroundColor: Colors.green.shade700, child: Text( speaker.isNotEmpty ? speaker[0].toUpperCase() : '?', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold), ), ), const SizedBox(width: 10), @@ -289,12 +338,14 @@ void dispose() { children: [ Text( speaker, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( text, - style: const TextStyle(color: Colors.white70, height: 1.3), + style: + const TextStyle(color: Colors.white70, height: 1.3), ), ], ), @@ -318,7 +369,11 @@ void dispose() { } List section(String title, List children) => [ - Text(title, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), + Text(title, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), ...children, const SizedBox(height: 16), @@ -330,13 +385,14 @@ void dispose() { crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(list.length, (i) { final text = list[i]?.toString() ?? ''; + final textColor = Theme.of(context).colorScheme.onSurfaceVariant; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('• ', style: TextStyle(color: Colors.white70)), - Expanded(child: Text(text, style: const TextStyle(color: Colors.white70))), + Text('• ', style: TextStyle(color: textColor)), + Expanded(child: Text(text, style: TextStyle(color: textColor))), ], ), ); @@ -353,7 +409,7 @@ void dispose() { margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), ), child: Row( @@ -364,11 +420,15 @@ void dispose() { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(map['item']?.toString() ?? '', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + Text(map['item']?.toString() ?? '', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold)), const SizedBox(height: 2), Text( 'Owner: ${map['owner'] ?? '—'} • Deadline: ${map['deadline'] ?? 'N/A'}', - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + style: TextStyle( + color: Colors.grey.shade400, fontSize: 12), ), ], ), @@ -394,17 +454,28 @@ void dispose() { ...section('Important Decisions', [bullets(decisions)]), ...section('Action Items', [actionItems(actions)]), ...section('Meeting Highlights', [bullets(highlights)]), - ...section('Follow-up Tasks', [actionItems(followUps.map((e) => {'item': e['task'], 'owner': '', 'deadline': e['deadline']}).toList())]), - Text('Overall Summary', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), + ...section('Follow-up Tasks', [ + actionItems(followUps + .map((e) => + {'item': e['task'], 'owner': '', 'deadline': e['deadline']}) + .toList()) + ]), + Text('Overall Summary', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: const Color(0xFF2D2D2D), borderRadius: BorderRadius.circular(8)), - child: Text(overall, style: const TextStyle(color: Colors.white70, height: 1.4)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8)), + child: Text(overall, + style: + Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.4)), ), ], ); } } - - diff --git a/lib/screens/meetings/meeting_screen.dart b/lib/screens/meetings/meeting_screen.dart index 3306d66..9a294b8 100644 --- a/lib/screens/meetings/meeting_screen.dart +++ b/lib/screens/meetings/meeting_screen.dart @@ -8,8 +8,9 @@ import 'meeting_detail_screen.dart'; import 'meeting_insights_screen.dart'; class MeetingScreen extends StatefulWidget { - static final GlobalKey<_MeetingScreenState> globalKey = GlobalKey<_MeetingScreenState>(); - + static final GlobalKey<_MeetingScreenState> globalKey = + GlobalKey<_MeetingScreenState>(); + const MeetingScreen({Key? key}) : super(key: key); static void refreshMeetings() { @@ -26,18 +27,18 @@ class _MeetingScreenState extends State { bool _isAdmin = false; String _selectedFilter = 'upcoming'; List> _meetings = []; - + @override void initState() { super.initState(); _loadInitialData(); } - + Future _loadInitialData() async { setState(() { _isLoading = true; }); - + try { // Check if user is admin final userProfile = await _supabaseService.getCurrentUserProfile(); @@ -46,17 +47,17 @@ class _MeetingScreenState extends State { _isAdmin = userProfile?['role'] == 'admin'; }); } - + // Initial load of meetings final meetings = await _supabaseService.getMeetings(); - + if (mounted) { setState(() { _meetings = meetings; _isLoading = false; }); } - + debugPrint('Meetings loaded: ${meetings.length}'); } catch (e) { debugPrint('Error loading initial data: $e'); @@ -67,11 +68,11 @@ class _MeetingScreenState extends State { } } } - + Future _deleteMeeting(String meetingId) async { try { final result = await _supabaseService.deleteMeeting(meetingId); - + if (mounted && !result['success']) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -94,10 +95,10 @@ class _MeetingScreenState extends State { } } } - + Future _launchMeetingUrl(String? url) async { if (url == null || url.isEmpty) return; - + try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { @@ -128,21 +129,21 @@ class _MeetingScreenState extends State { @override Widget build(BuildContext context) { if (_isLoading) { - return const Scaffold( - backgroundColor: Color(0xFF1A1A1A), - body: Center(child: CustomLoading()), + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: const Center(child: CustomLoading()), ); } - + // Make sure the key is properly associated with this instance if (MeetingScreen.globalKey.currentState != this) { WidgetsBinding.instance.addPostFrameCallback((_) { MeetingScreen.refreshMeetings(); }); } - + final now = DateTime.now(); - + // Filter meetings based on selected filter final filteredMeetings = _meetings.where((meeting) { final meetingDate = DateTime.parse(meeting['meeting_date']); @@ -152,14 +153,16 @@ class _MeetingScreenState extends State { return meetingDate.isBefore(now); } }).toList(); - - final upcomingCount = _meetings.where((m) => - DateTime.parse(m['meeting_date']).isAfter(now)).length; - final pastCount = _meetings.where((m) => - DateTime.parse(m['meeting_date']).isBefore(now)).length; - + + final upcomingCount = _meetings + .where((m) => DateTime.parse(m['meeting_date']).isAfter(now)) + .length; + final pastCount = _meetings + .where((m) => DateTime.parse(m['meeting_date']).isBefore(now)) + .length; + return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, floatingActionButton: FloatingActionButton( onPressed: () async { final result = await Navigator.push( @@ -168,7 +171,7 @@ class _MeetingScreenState extends State { builder: (context) => const CreateMeetingScreen(), ), ); - + if (result == true) { // Meeting was created, refresh meetings _loadInitialData(); @@ -199,9 +202,9 @@ class _MeetingScreenState extends State { }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: const BoxDecoration( - color: Color(0xFF2D2D2D), - borderRadius: BorderRadius.only( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), ), @@ -256,7 +259,9 @@ class _MeetingScreenState extends State { const SizedBox(height: 2), Text( label, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12), ), ], ); @@ -267,7 +272,7 @@ class _MeetingScreenState extends State { {'id': 'upcoming', 'label': 'Upcoming', 'color': Colors.blue}, {'id': 'past', 'label': 'Past', 'color': Colors.purple}, ]; - + return Container( height: 36, margin: const EdgeInsets.symmetric(horizontal: 16), @@ -275,14 +280,16 @@ class _MeetingScreenState extends State { children: filterOptions.map((filter) { final isSelected = filter['id'] == _selectedFilter; final color = filter['color'] as MaterialColor; - + return Expanded( child: GestureDetector( - onTap: () => setState(() => _selectedFilter = filter['id'] as String), + onTap: () => + setState(() => _selectedFilter = filter['id'] as String), child: Container( alignment: Alignment.center, decoration: BoxDecoration( - color: isSelected ? color.withOpacity(0.2) : Colors.transparent, + color: + isSelected ? color.withOpacity(0.2) : Colors.transparent, borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? color : Colors.transparent, @@ -292,8 +299,11 @@ class _MeetingScreenState extends State { child: Text( filter['label'] as String, style: TextStyle( - color: isSelected ? color : Colors.white70, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected + ? color + : Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, fontSize: 13, ), ), @@ -312,9 +322,11 @@ class _MeetingScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - _selectedFilter == 'upcoming' ? Icons.calendar_today : Icons.history, + _selectedFilter == 'upcoming' + ? Icons.calendar_today + : Icons.history, size: 70, - color: Colors.grey.shade600, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(height: 16), Text( @@ -322,17 +334,17 @@ class _MeetingScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: Colors.grey.shade400, + color: Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(height: 8), Text( - _selectedFilter == 'upcoming' - ? 'Create new meetings to get started' - : 'Past meetings will appear here', + _selectedFilter == 'upcoming' + ? 'Create new meetings to get started' + : 'Past meetings will appear here', style: TextStyle( fontSize: 13, - color: Colors.grey.shade600, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), @@ -340,7 +352,7 @@ class _MeetingScreenState extends State { ), ); } - + return ListView.builder( padding: const EdgeInsets.all(16), itemCount: filteredMeetings.length, @@ -348,7 +360,7 @@ class _MeetingScreenState extends State { final meeting = filteredMeetings[index]; final meetingDate = DateTime.parse(meeting['meeting_date']); final isUpcoming = meetingDate.isAfter(DateTime.now()); - + return _MeetingCard( meeting: meeting, isAdmin: _isAdmin, @@ -359,10 +371,11 @@ class _MeetingScreenState extends State { final result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => MeetingDetailScreen(meetingId: meeting['id']), + builder: (context) => + MeetingDetailScreen(meetingId: meeting['id']), ), ); - + if (result == true) { // Meeting was updated in detail screen, refresh meetings _loadInitialData(); @@ -396,25 +409,27 @@ class _MeetingCard extends StatelessWidget { final meetingDate = DateTime.parse(meeting['meeting_date']); final dateFormat = DateFormat('E, MMM d, yyyy'); final timeFormat = DateFormat('h:mm a'); - final hasUrl = meeting['meeting_url'] != null && meeting['meeting_url'].toString().isNotEmpty; - final isCreator = meeting['creator'] != null && - meeting['creator']['id'] == SupabaseService().client.auth.currentUser?.id; + final hasUrl = meeting['meeting_url'] != null && + meeting['meeting_url'].toString().isNotEmpty; + final isCreator = meeting['creator'] != null && + meeting['creator']['id'] == + SupabaseService().client.auth.currentUser?.id; final canCancel = isUpcoming && (isAdmin || isCreator); - + // Limit title to 25 characters - final title = (meeting['title'] ?? 'Untitled Meeting').length > 25 - ? '${(meeting['title'] ?? 'Untitled Meeting').substring(0, 25)}...' + final title = (meeting['title'] ?? 'Untitled Meeting').length > 25 + ? '${(meeting['title'] ?? 'Untitled Meeting').substring(0, 25)}...' : (meeting['title'] ?? 'Untitled Meeting'); - + // Get creator name String creatorName = 'Unknown'; if (meeting['creator'] != null && meeting['creator']['full_name'] != null) { creatorName = meeting['creator']['full_name']; } - + return Card( margin: const EdgeInsets.only(bottom: 16), - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), @@ -438,15 +453,17 @@ class _MeetingCard extends StatelessWidget { children: [ Icon( isUpcoming ? Icons.calendar_today : Icons.event_available, - color: isUpcoming ? Colors.green.shade400 : Colors.grey.shade400, + color: isUpcoming + ? Colors.green.shade400 + : Colors.grey.shade400, size: 18, ), const SizedBox(width: 8), Expanded( child: Text( title, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 15, fontWeight: FontWeight.bold, ), @@ -456,7 +473,8 @@ class _MeetingCard extends StatelessWidget { if (canCancel) IconButton( onPressed: () => onDelete(meeting['id']), - icon: const Icon(Icons.delete, color: Colors.red, size: 18), + icon: + const Icon(Icons.delete, color: Colors.red, size: 18), tooltip: 'Cancel Meeting', padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -464,7 +482,7 @@ class _MeetingCard extends StatelessWidget { ], ), ), - + // Meeting details Padding( padding: const EdgeInsets.all(16), @@ -476,16 +494,16 @@ class _MeetingCard extends StatelessWidget { children: [ Icon( Icons.access_time, - color: Colors.grey.shade400, + color: Theme.of(context).colorScheme.onSurfaceVariant, size: 14, ), const SizedBox(width: 8), Text( - isUpcoming + isUpcoming ? '${dateFormat.format(meetingDate)}, ${timeFormat.format(meetingDate)}' : 'Yesterday, ${timeFormat.format(meetingDate)}', style: TextStyle( - color: Colors.grey.shade300, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13, ), ), @@ -510,9 +528,9 @@ class _MeetingCard extends StatelessWidget { ), ], ), - + const SizedBox(height: 8), - + // Creator info Row( children: [ @@ -520,7 +538,9 @@ class _MeetingCard extends StatelessWidget { radius: 10, backgroundColor: Colors.green.shade700, child: Text( - creatorName.isNotEmpty ? creatorName[0].toUpperCase() : '?', + creatorName.isNotEmpty + ? creatorName[0].toUpperCase() + : '?', style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, @@ -532,7 +552,7 @@ class _MeetingCard extends StatelessWidget { Text( 'Created by $creatorName', style: TextStyle( - color: Colors.grey.shade400, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12, ), ), @@ -556,7 +576,7 @@ class _MeetingCard extends StatelessWidget { ), ], ), - + // Transcription and AI Summary buttons for past meetings with URL if (!isUpcoming && hasUrl) Padding( @@ -594,7 +614,8 @@ class _MeetingCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - padding: const EdgeInsets.symmetric(vertical: 6), + padding: + const EdgeInsets.symmetric(vertical: 6), ), ), ), @@ -629,7 +650,8 @@ class _MeetingCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), - padding: const EdgeInsets.symmetric(vertical: 6), + padding: + const EdgeInsets.symmetric(vertical: 6), ), ), ), @@ -644,4 +666,4 @@ class _MeetingCard extends StatelessWidget { ), ); } -} +} diff --git a/lib/screens/onboarding/onboarding_screen.dart b/lib/screens/onboarding/onboarding_screen.dart index 1277854..9352054 100644 --- a/lib/screens/onboarding/onboarding_screen.dart +++ b/lib/screens/onboarding/onboarding_screen.dart @@ -53,7 +53,7 @@ class _OnboardingScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( child: Column( children: [ @@ -102,10 +102,10 @@ class _OnboardingScreenState extends State { const SizedBox(height: 40), Text( slide.title, - style: const TextStyle( + style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, ), textAlign: TextAlign.center, ), @@ -114,7 +114,8 @@ class _OnboardingScreenState extends State { slide.description, style: TextStyle( fontSize: 16, - color: Colors.grey.shade400, + color: + Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), @@ -138,20 +139,21 @@ class _OnboardingScreenState extends State { margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( shape: BoxShape.circle, - color: - _currentPage == index - ? Colors.green.shade400 - : Colors.grey.shade700, + color: _currentPage == index + ? Colors.green.shade400 + : Theme.of(context) + .colorScheme + .onSurfaceVariant + .withOpacity(0.5), ), ), ), ), const SizedBox(height: 24), CustomButton( - text: - _currentPage == _slides.length - 1 - ? 'Get Started' - : 'Next', + text: _currentPage == _slides.length - 1 + ? 'Get Started' + : 'Next', onPressed: _handleNext, ), const SizedBox(height: 16), diff --git a/lib/screens/profile/edit_profile_screen.dart b/lib/screens/profile/edit_profile_screen.dart index 68bc980..acd4958 100644 --- a/lib/screens/profile/edit_profile_screen.dart +++ b/lib/screens/profile/edit_profile_screen.dart @@ -19,31 +19,32 @@ class _EditProfileScreenState extends State { final _formKey = GlobalKey(); final _supabaseService = SupabaseService(); bool _isLoading = false; - + late final TextEditingController _firstNameController; late final TextEditingController _lastNameController; late final TextEditingController _emailController; - + String? _fullName; String? _email; - + @override void initState() { super.initState(); - + // Split full name into first and last name final nameParts = widget.userProfile['full_name']?.split(' ') ?? ['', '']; final firstName = nameParts.isNotEmpty ? nameParts.first : ''; final lastName = nameParts.length > 1 ? nameParts.sublist(1).join(' ') : ''; - + _firstNameController = TextEditingController(text: firstName); _lastNameController = TextEditingController(text: lastName); - _emailController = TextEditingController(text: widget.userProfile['email'] ?? ''); - + _emailController = + TextEditingController(text: widget.userProfile['email'] ?? ''); + _fullName = widget.userProfile['full_name']; _email = widget.userProfile['email']; } - + @override void dispose() { _firstNameController.dispose(); @@ -51,25 +52,25 @@ class _EditProfileScreenState extends State { _emailController.dispose(); super.dispose(); } - + Future _saveProfile() async { if (!_formKey.currentState!.validate()) return; - + setState(() { _isLoading = true; }); - + try { final firstName = _firstNameController.text.trim(); final lastName = _lastNameController.text.trim(); final fullName = '$firstName $lastName'.trim(); - + // Only update if something has changed if (fullName != _fullName) { final success = await _supabaseService.updateUserProfile({ 'full_name': fullName, }); - + if (success) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -78,10 +79,10 @@ class _EditProfileScreenState extends State { backgroundColor: Colors.green, ), ); - + // Call the callback to refresh the profile screen widget.onProfileUpdated(); - + // Pop back to profile screen Navigator.pop(context); } @@ -120,21 +121,24 @@ class _EditProfileScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text('Edit Profile'), actions: [ TextButton.icon( onPressed: _isLoading ? null : _saveProfile, - icon: _isLoading + icon: _isLoading ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white) - ) - : const Icon(Icons.save, color: Colors.white), - label: const Text('Save', style: TextStyle(color: Colors.white)), + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, color: Colors.white)) + : Icon(Icons.save, + color: Theme.of(context).colorScheme.onSurface), + label: Text('Save', + style: + TextStyle(color: Theme.of(context).colorScheme.onSurface)), ), ], ), @@ -160,16 +164,19 @@ class _EditProfileScreenState extends State { border: Border.all(color: Colors.white, width: 3), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Theme.of(context) + .colorScheme + .shadow + .withOpacity(0.2), blurRadius: 10, offset: const Offset(0, 5), ), ], ), - child: const Icon( + child: Icon( Icons.person, size: 50, - color: Color(0xFF1A1A1A), + color: Theme.of(context).colorScheme.onSurface, ), ), Container( @@ -188,13 +195,12 @@ class _EditProfileScreenState extends State { ), ), const SizedBox(height: 32), - const Text( + Text( 'Personal Information', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 16), _buildTextField( @@ -218,43 +224,28 @@ class _EditProfileScreenState extends State { TextFormField( controller: _emailController, readOnly: true, - style: TextStyle(color: Colors.grey.shade500), - decoration: InputDecoration( + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurfaceVariant), + decoration: const InputDecoration( labelText: 'Email', - labelStyle: TextStyle(color: Colors.grey.shade600), helperText: 'Email cannot be changed', - helperStyle: TextStyle(color: Colors.grey.shade600), - prefixIcon: Icon( - Icons.email_outlined, - color: Colors.grey.shade600, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade900), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade900), - ), - filled: true, - fillColor: const Color(0xFF1F1F1F), + prefixIcon: Icon(Icons.email_outlined), ), ), - const SizedBox(height: 32), - const Text( + Text( 'Role', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), ), child: Row( @@ -281,9 +272,12 @@ class _EditProfileScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.userProfile['role'] == 'admin' ? 'Team Admin' : 'Team Member', - style: const TextStyle( - color: Colors.white, + widget.userProfile['role'] == 'admin' + ? 'Team Admin' + : 'Team Member', + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.bold, ), @@ -292,7 +286,11 @@ class _EditProfileScreenState extends State { widget.userProfile['role'] == 'admin' ? 'You have admin privileges' : 'Standard team member access', - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 12), ), ], ), @@ -305,7 +303,7 @@ class _EditProfileScreenState extends State { ), ); } - + Widget _buildTextField({ required TextEditingController controller, required String label, @@ -318,32 +316,12 @@ class _EditProfileScreenState extends State { controller: controller, enabled: enabled, validator: validator, - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( labelText: label, - labelStyle: TextStyle(color: Colors.grey.shade400), helperText: helperText, - helperStyle: TextStyle(color: Colors.grey.shade600), - prefixIcon: Icon(icon, color: Colors.grey.shade400), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade800), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade800), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.green.shade400), - ), - disabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Colors.grey.shade900), - ), - filled: true, - fillColor: const Color(0xFF2D2D2D), + prefixIcon: Icon(icon), ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index ec6330a..00ca35d 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../../services/supabase_service.dart'; import '../../services/navigation_service.dart'; +import '../../theme/app_theme_mode.dart'; +import '../../theme/theme_controller.dart'; import '../auth/login_screen.dart'; import 'team_members_screen.dart'; import 'edit_profile_screen.dart'; @@ -31,13 +34,16 @@ class _ProfileScreenState extends State { try { final profile = await _supabaseService.getCurrentUserProfile(); - + // Also load all teams associated with the user's email if (profile != null && profile['email'] != null) { try { - final teamsResponse = await _supabaseService.getUserTeams(profile['email']); - if (teamsResponse['success'] == true && teamsResponse['teams'] != null) { - _userTeams = List>.from(teamsResponse['teams']); + final teamsResponse = + await _supabaseService.getUserTeams(profile['email']); + if (teamsResponse['success'] == true && + teamsResponse['teams'] != null) { + _userTeams = + List>.from(teamsResponse['teams']); } } catch (e) { debugPrint('Error fetching user teams: $e'); @@ -64,36 +70,26 @@ class _ProfileScreenState extends State { } } } - + Future _handleLogout() async { //Confirmation dialog final shouldLogout = await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - title: const Text( - 'Log out?', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), + title: const Text('Log out?'), content: const Text( 'You will need to log in again to access your account.', - style: TextStyle(color: Colors.white70), ), actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text( - 'Cancel', - style: TextStyle(color: Colors.white70), - ), + child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.pop(context, true), @@ -105,10 +101,7 @@ class _ProfileScreenState extends State { ), child: const Text( 'Logout', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), + style: TextStyle(fontWeight: FontWeight.bold), ), ), ], @@ -150,7 +143,6 @@ class _ProfileScreenState extends State { Widget build(BuildContext context) { if (_isLoading) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), body: const Center( child: CircularProgressIndicator(), ), @@ -164,7 +156,6 @@ class _ProfileScreenState extends State { final String teamCode = _userProfile?['teams']?['team_code'] ?? ''; return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), body: SafeArea( child: CustomScrollView( slivers: [ @@ -212,30 +203,37 @@ class _ProfileScreenState extends State { decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, - border: Border.all(color: Colors.white, width: 3), + border: Border.all( + color: Colors.white, width: 3), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Theme.of(context) + .colorScheme + .shadow + .withOpacity(0.2), blurRadius: 10, offset: const Offset(0, 5), ), ], ), - child: const Icon( + child: Icon( Icons.person, size: 50, - color: Color(0xFF1A1A1A), + color: + Theme.of(context).colorScheme.onSurface, ), ), // Role badge Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: role == 'admin' - ? Colors.orange.shade400 + color: role == 'admin' + ? Colors.orange.shade400 : Colors.blue.shade400, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 2), + border: Border.all( + color: Colors.white, width: 2), ), child: Text( role == 'admin' ? 'ADMIN' : 'MEMBER', @@ -296,20 +294,16 @@ class _ProfileScreenState extends State { ), ); } - + void _showTeamSwitcher() { showDialog( context: context, builder: (context) { return AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text( 'Switch Team', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), content: SizedBox( width: double.maxFinite, @@ -319,35 +313,38 @@ class _ProfileScreenState extends State { itemBuilder: (context, index) { final team = _userTeams[index]; final isCurrentTeam = team['id'] == _userProfile?['team_id']; - + + final colorScheme = Theme.of(context).colorScheme; return ListTile( title: Text( team['name'] ?? 'Team', style: TextStyle( - color: Colors.white, - fontWeight: isCurrentTeam ? FontWeight.bold : FontWeight.normal, + color: colorScheme.onSurface, + fontWeight: + isCurrentTeam ? FontWeight.bold : FontWeight.normal, ), ), subtitle: Text( 'Team Code: ${team['team_code'] ?? 'N/A'}', style: TextStyle( - color: Colors.grey.shade400, + color: colorScheme.onSurfaceVariant, fontSize: 12, ), ), leading: CircleAvatar( - backgroundColor: isCurrentTeam - ? Colors.green.shade400 + backgroundColor: isCurrentTeam + ? Colors.green.shade400 : Colors.grey.shade700, - child: Text( + child: Text( (() { final n = (team['name'] as String?)?.trim(); - return (n != null && n.isNotEmpty ? n[0] : 'T').toUpperCase(); + return (n != null && n.isNotEmpty ? n[0] : 'T') + .toUpperCase(); })(), style: const TextStyle(color: Colors.white), ), ), - trailing: isCurrentTeam + trailing: isCurrentTeam ? Icon(Icons.check, color: Colors.green.shade400) : null, onTap: () { @@ -362,32 +359,27 @@ class _ProfileScreenState extends State { ), actions: [ TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text( - 'Cancel', - style: TextStyle(color: Colors.grey.shade400), - ), + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), ), ], ); }, ); } - + Future _switchTeam(String teamId) async { try { setState(() { _isLoading = true; }); - + final result = await _supabaseService.switchTeam(teamId); - + if (result['success'] == true) { // Reload profile with new team await _loadUserProfile(); - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -401,7 +393,7 @@ class _ProfileScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error switching team: ${result['error']}'), @@ -416,7 +408,7 @@ class _ProfileScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error switching team: $e'), @@ -453,14 +445,15 @@ class _ProfileScreenState extends State { } Widget _buildTeamInfoSection(String teamName, String teamCode) { + final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: colorScheme.shadow.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), @@ -469,39 +462,39 @@ class _ProfileScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Row( + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Team Information', style: TextStyle( - color: Colors.white, + color: colorScheme.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), ), - Icon(Icons.groups, color: Colors.white70), + Icon(Icons.groups, color: colorScheme.onSurfaceVariant), ], ), const SizedBox(height: 16), Row( children: [ - const Icon(Icons.business, color: Colors.grey), + Icon(Icons.business, color: colorScheme.onSurfaceVariant), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Team Name', style: TextStyle( - color: Colors.grey, + color: colorScheme.onSurfaceVariant, fontSize: 12, ), ), Text( teamName, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.bold, ), @@ -514,22 +507,22 @@ class _ProfileScreenState extends State { const SizedBox(height: 16), Row( children: [ - const Icon(Icons.key, color: Colors.grey), + Icon(Icons.key, color: colorScheme.onSurfaceVariant), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Team ID', style: TextStyle( - color: Colors.grey, + color: colorScheme.onSurfaceVariant, fontSize: 12, ), ), Text( teamCode, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1.5, @@ -546,14 +539,15 @@ class _ProfileScreenState extends State { } Widget _buildStatsSection() { + final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: colorScheme.shadow.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), @@ -561,18 +555,18 @@ class _ProfileScreenState extends State { ), child: Column( children: [ - const Row( + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Your Activity', style: TextStyle( - color: Colors.white, + color: colorScheme.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), ), - Icon(Icons.insights, color: Colors.white70), + Icon(Icons.insights, color: colorScheme.onSurfaceVariant), ], ), const SizedBox(height: 20), @@ -580,16 +574,20 @@ class _ProfileScreenState extends State { future: SupabaseService().getTasks(), builder: (context, snapshot) { final tasks = snapshot.data ?? const >[]; - final completed = tasks.where((t) => t['status'] == 'completed').length; + final completed = + tasks.where((t) => t['status'] == 'completed').length; // Placeholder dynamic numbers while no time tracking/projects table final hours = (tasks.length * 2).toString(); - final projects = (tasks.map((t) => t['team_id']).toSet().length).toString(); + final projects = + (tasks.map((t) => t['team_id']).toSet().length).toString(); return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _buildStatItem('Tasks\nCompleted', completed.toString(), Colors.green.shade400), + _buildStatItem('Tasks\nCompleted', completed.toString(), + Colors.green.shade400), _buildStatItem('Hours\nLogged', hours, Colors.blue.shade400), - _buildStatItem('Team\nProjects', projects, Colors.purple.shade400), + _buildStatItem( + 'Team\nProjects', projects, Colors.purple.shade400), ], ); }, @@ -621,7 +619,10 @@ class _ProfileScreenState extends State { Text( label, textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), ), ], ); @@ -631,14 +632,15 @@ class _ProfileScreenState extends State { final bool isAdmin = _userProfile?['role'] == 'admin'; final String teamId = _userProfile?['teams']?['team_code'] ?? ''; final String teamName = _userProfile?['teams']?['name'] ?? 'Your Team'; + final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Settings', style: TextStyle( - color: Colors.white, + color: colorScheme.onSurface, fontSize: 20, fontWeight: FontWeight.bold, ), @@ -646,7 +648,7 @@ class _ProfileScreenState extends State { const SizedBox(height: 16), Container( decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: colorScheme.surface, borderRadius: BorderRadius.circular(20), ), child: Column( @@ -723,50 +725,128 @@ class _ProfileScreenState extends State { ); } + String _themeModeLabel(AppThemeMode mode) { + switch (mode) { + case AppThemeMode.light: + return 'Light'; + case AppThemeMode.dark: + return 'Dark'; + case AppThemeMode.system: + return 'System'; + } + } + + void _showThemePicker() { + showModalBottomSheet( + context: context, + builder: (context) { + return Consumer( + builder: (context, controller, _) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: AppThemeMode.values.map((mode) { + final isSelected = controller.themeMode == mode; + return ListTile( + leading: Icon( + mode == AppThemeMode.light + ? Icons.light_mode + : mode == AppThemeMode.dark + ? Icons.dark_mode + : Icons.settings_brightness, + ), + title: Text(_themeModeLabel(mode)), + trailing: isSelected ? const Icon(Icons.check) : null, + onTap: () { + controller.setThemeMode(mode); + Navigator.pop(context); + }, + ); + }).toList(), + ), + ); + }, + ); + }, + ); + } + Widget _buildPreferencesSection() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Preferences', - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Container( - decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - _buildPreferenceItem( - icon: Icons.dark_mode_outlined, - title: 'Dark Mode', - isSwitch: true, - iconColor: Colors.purple.shade400, + return Consumer( + builder: (context, themeController, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Preferences', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, ), - const Divider(color: Colors.grey), - _buildPreferenceItem( - icon: Icons.notifications_active_outlined, - title: 'Push Notifications', - isSwitch: true, - iconColor: Colors.red.shade400, + ), + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(20), ), - const Divider(color: Colors.grey), - _buildPreferenceItem( - icon: Icons.language_outlined, - title: 'Language', - subtitle: 'English (US)', - iconColor: Colors.blue.shade400, + child: Column( + children: [ + _buildThemePreferenceItem( + themeController: themeController, + ), + Divider(color: Theme.of(context).colorScheme.outlineVariant), + _buildPreferenceItem( + icon: Icons.notifications_active_outlined, + title: 'Push Notifications', + isSwitch: true, + iconColor: Colors.red.shade400, + ), + Divider(color: Theme.of(context).colorScheme.outlineVariant), + _buildPreferenceItem( + icon: Icons.language_outlined, + title: 'Language', + subtitle: 'English (US)', + iconColor: Colors.blue.shade400, + ), + ], ), - ], - ), + ), + ], + ); + }, + ); + } + + Widget _buildThemePreferenceItem({ + required ThemeController themeController, + }) { + return ListTile( + leading: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.purple.shade400.withOpacity(0.1), + shape: BoxShape.circle, ), - ], + child: Icon(Icons.dark_mode_outlined, color: Colors.purple.shade400), + ), + title: Text( + 'Theme', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + _themeModeLabel(themeController.themeMode), + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onTap: _showThemePicker, ); } @@ -777,6 +857,7 @@ class _ProfileScreenState extends State { required Color iconColor, VoidCallback? onTap, }) { + final colorScheme = Theme.of(context).colorScheme; return ListTile( leading: Container( padding: const EdgeInsets.all(8), @@ -788,17 +869,19 @@ class _ProfileScreenState extends State { ), title: Text( title, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), - subtitle: Text(subtitle, style: TextStyle(color: Colors.grey.shade400)), - trailing: const Icon(Icons.chevron_right, color: Colors.white70), - onTap: onTap ?? () { - // Default implementation if no specific onTap is provided - // TODO: Implement settings navigation - }, + subtitle: + Text(subtitle, style: TextStyle(color: colorScheme.onSurfaceVariant)), + trailing: Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), + onTap: onTap ?? + () { + // Default implementation if no specific onTap is provided + // TODO: Implement settings navigation + }, ); } @@ -809,6 +892,7 @@ class _ProfileScreenState extends State { required Color iconColor, bool isSwitch = false, }) { + final colorScheme = Theme.of(context).colorScheme; return ListTile( leading: Container( padding: const EdgeInsets.all(8), @@ -820,31 +904,25 @@ class _ProfileScreenState extends State { ), title: Text( title, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), - subtitle: - subtitle != null - ? Text(subtitle, style: TextStyle(color: Colors.grey.shade400)) - : null, - trailing: - isSwitch - ? Switch( - value: true, - onChanged: (value) { - // TODO: Implement preference toggle - }, - activeColor: Colors.green.shade400, - ) - : const Icon(Icons.chevron_right, color: Colors.white70), - onTap: - isSwitch - ? null - : () { - // TODO: Implement preference navigation + subtitle: subtitle != null + ? Text(subtitle, + style: TextStyle(color: colorScheme.onSurfaceVariant)) + : null, + trailing: isSwitch + ? Switch( + value: true, + onChanged: (value) { + // TODO: Implement preference toggle }, + activeColor: Colors.green.shade400, + ) + : Icon(Icons.chevron_right, color: colorScheme.onSurfaceVariant), + onTap: isSwitch ? null : () {/* TODO: Implement preference navigation */}, ); } } @@ -856,11 +934,10 @@ class DotPatternPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final paint = - Paint() - ..color = color - ..strokeWidth = 2 - ..strokeCap = StrokeCap.round; + final paint = Paint() + ..color = color + ..strokeWidth = 2 + ..strokeCap = StrokeCap.round; const spacing = 30.0; const dotSize = 2.0; @@ -874,4 +951,4 @@ class DotPatternPainter extends CustomPainter { @override bool shouldRepaint(DotPatternPainter oldDelegate) => false; -} \ No newline at end of file +} diff --git a/lib/screens/profile/team_members_screen.dart b/lib/screens/profile/team_members_screen.dart index 1a1615b..401af07 100644 --- a/lib/screens/profile/team_members_screen.dart +++ b/lib/screens/profile/team_members_screen.dart @@ -33,7 +33,7 @@ class _TeamMembersScreenState extends State { try { final members = await _supabaseService.getTeamMembers(widget.teamId); - + if (mounted) { setState(() { _teamMembers = members; @@ -68,7 +68,7 @@ class _TeamMembersScreenState extends State { Colors.indigo, Colors.pink, ]; - + int hashCode = name.hashCode; return colors[hashCode.abs() % colors.length]; } @@ -76,7 +76,7 @@ class _TeamMembersScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: Text('${widget.teamName} Members'), backgroundColor: Colors.green.shade800, @@ -90,6 +90,7 @@ class _TeamMembersScreenState extends State { } Widget _buildEmptyState() { + final colorScheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -97,7 +98,7 @@ class _TeamMembersScreenState extends State { Icon( Icons.group_off, size: 80, - color: Colors.grey.shade600, + color: colorScheme.onSurfaceVariant, ), const SizedBox(height: 16), Text( @@ -105,7 +106,7 @@ class _TeamMembersScreenState extends State { style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.grey.shade400, + color: colorScheme.onSurface, ), ), const SizedBox(height: 8), @@ -113,7 +114,7 @@ class _TeamMembersScreenState extends State { 'Your team doesn\'t have any members yet', style: TextStyle( fontSize: 14, - color: Colors.grey.shade600, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -132,10 +133,10 @@ class _TeamMembersScreenState extends State { final role = member['role'] ?? 'member'; final firstLetter = name.isNotEmpty ? name[0].toUpperCase() : '?'; final avatarColor = _getAvatarColor(name); - + return Card( margin: const EdgeInsets.only(bottom: 12), - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -156,8 +157,8 @@ class _TeamMembersScreenState extends State { ), title: Text( name, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold, fontSize: 16, ), @@ -169,23 +170,26 @@ class _TeamMembersScreenState extends State { Text( email, style: TextStyle( - color: Colors.grey.shade400, + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14, ), ), const SizedBox(height: 4), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: role == 'admin' - ? Colors.orange.shade400.withOpacity(0.2) + color: role == 'admin' + ? Colors.orange.shade400.withOpacity(0.2) : Colors.blue.shade400.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Text( role == 'admin' ? 'Admin' : 'Member', style: TextStyle( - color: role == 'admin' ? Colors.orange.shade400 : Colors.blue.shade400, + color: role == 'admin' + ? Colors.orange.shade400 + : Colors.blue.shade400, fontWeight: FontWeight.bold, fontSize: 12, ), @@ -204,4 +208,4 @@ class _TeamMembersScreenState extends State { }, ); } -} \ No newline at end of file +} diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index b20bb00..e2e83a4 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -89,7 +89,7 @@ class _SplashScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Center( child: FadeTransition( opacity: _fadeAnimation, @@ -132,12 +132,12 @@ class _SplashScreenState extends State }, ), const SizedBox(height: 30), - const Text( + Text( 'Ell-ena', style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold, - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, letterSpacing: 2, ), ), diff --git a/lib/screens/tasks/create_task_screen.dart b/lib/screens/tasks/create_task_screen.dart index cffe552..69f1e08 100644 --- a/lib/screens/tasks/create_task_screen.dart +++ b/lib/screens/tasks/create_task_screen.dart @@ -17,31 +17,33 @@ class _CreateTaskScreenState extends State { DateTime? _selectedDueDate; String? _selectedAssigneeId; List> _teamMembers = []; - + @override void initState() { super.initState(); _loadTeamMembers(); } - + @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } - + Future _loadTeamMembers() async { setState(() { _isLoading = true; }); - + try { final userProfile = await _supabaseService.getCurrentUserProfile(); - if (userProfile != null && userProfile['teams'] != null && userProfile['teams']['team_code'] != null) { + if (userProfile != null && + userProfile['teams'] != null && + userProfile['teams']['team_code'] != null) { final teamId = userProfile['teams']['team_code']; final members = await _supabaseService.getTeamMembers(teamId); - + if (mounted) { setState(() { _teamMembers = members; @@ -62,43 +64,31 @@ class _CreateTaskScreenState extends State { } } } - + Future _selectDueDate() async { final DateTime? picked = await showDatePicker( context: context, - initialDate: _selectedDueDate ?? DateTime.now().add(const Duration(days: 1)), + initialDate: + _selectedDueDate ?? DateTime.now().add(const Duration(days: 1)), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)), - builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: ColorScheme.dark( - primary: Colors.green.shade400, - onPrimary: Colors.white, - surface: const Color(0xFF2D2D2D), - onSurface: Colors.white, - ), - dialogBackgroundColor: const Color(0xFF1A1A1A), - ), - child: child!, - ); - }, + builder: (context, child) => child!, ); - + if (picked != null && mounted) { setState(() { _selectedDueDate = picked; }); } } - + Future _createTask() async { if (!_formKey.currentState!.validate()) return; - + setState(() { _isLoading = true; }); - + try { final result = await _supabaseService.createTask( title: _titleController.text.trim(), @@ -106,14 +96,14 @@ class _CreateTaskScreenState extends State { dueDate: _selectedDueDate, assignedToUserId: _selectedAssigneeId, ); - + if (result['success'] && mounted) { Navigator.pop(context, true); } else if (mounted) { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error creating task: ${result['error']}'), @@ -127,7 +117,7 @@ class _CreateTaskScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error creating task: $e'), @@ -141,7 +131,7 @@ class _CreateTaskScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: const Text('Create New Task'), backgroundColor: Colors.green.shade800, @@ -156,13 +146,12 @@ class _CreateTaskScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Task title - const Text( + Text( 'Task Title', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), TextFormField( @@ -172,7 +161,7 @@ class _CreateTaskScreenState extends State { hintText: 'Enter task title', hintStyle: TextStyle(color: Colors.grey.shade400), filled: true, - fillColor: const Color(0xFF2D2D2D), + fillColor: Theme.of(context).colorScheme.surface, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -190,15 +179,14 @@ class _CreateTaskScreenState extends State { }, ), const SizedBox(height: 24), - + // Task description - const Text( + Text( 'Description', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), TextFormField( @@ -208,7 +196,7 @@ class _CreateTaskScreenState extends State { hintText: 'Enter task description', hintStyle: TextStyle(color: Colors.grey.shade400), filled: true, - fillColor: const Color(0xFF2D2D2D), + fillColor: Theme.of(context).colorScheme.surface, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -221,15 +209,14 @@ class _CreateTaskScreenState extends State { maxLines: 5, ), const SizedBox(height: 24), - + // Due date - const Text( + Text( 'Due Date', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), InkWell( @@ -240,14 +227,17 @@ class _CreateTaskScreenState extends State { vertical: 16, ), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: + Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon( Icons.calendar_today, - color: Colors.grey.shade400, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, size: 20, ), const SizedBox(width: 12), @@ -257,8 +247,10 @@ class _CreateTaskScreenState extends State { : '${_selectedDueDate!.day}/${_selectedDueDate!.month}/${_selectedDueDate!.year}', style: TextStyle( color: _selectedDueDate == null - ? Colors.grey.shade400 - : Colors.white, + ? Theme.of(context) + .colorScheme + .onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, ), ), ], @@ -266,15 +258,14 @@ class _CreateTaskScreenState extends State { ), ), const SizedBox(height: 24), - + // Assign to - const Text( + Text( 'Assign To', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Container( @@ -283,7 +274,8 @@ class _CreateTaskScreenState extends State { vertical: 4, ), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: + Theme.of(context).colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(12), ), child: DropdownButtonHideUnderline( @@ -291,15 +283,20 @@ class _CreateTaskScreenState extends State { value: _selectedAssigneeId, hint: Text( 'Select team member', - style: TextStyle(color: Colors.grey.shade400), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), ), - dropdownColor: const Color(0xFF2D2D2D), + dropdownColor: Theme.of(context).colorScheme.surface, isExpanded: true, icon: Icon( Icons.arrow_drop_down, - color: Colors.grey.shade400, + color: + Theme.of(context).colorScheme.onSurfaceVariant, ), - style: const TextStyle(color: Colors.white), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), items: [ const DropdownMenuItem( value: null, @@ -314,8 +311,13 @@ class _CreateTaskScreenState extends State { radius: 12, backgroundColor: Colors.green.shade700, child: Text( - (member['full_name'] != null && member['full_name'].toString().isNotEmpty) - ? member['full_name'].toString()[0].toUpperCase() + (member['full_name'] != null && + member['full_name'] + .toString() + .isNotEmpty) + ? member['full_name'] + .toString()[0] + .toUpperCase() : '?', style: const TextStyle( fontSize: 12, @@ -325,7 +327,8 @@ class _CreateTaskScreenState extends State { ), ), const SizedBox(width: 8), - Text(member['full_name']?.toString() ?? 'Unknown'), + Text(member['full_name']?.toString() ?? + 'Unknown'), if (member['role'] == 'admin') Container( margin: const EdgeInsets.only(left: 8), @@ -334,8 +337,10 @@ class _CreateTaskScreenState extends State { vertical: 2, ), decoration: BoxDecoration( - color: Colors.orange.shade400.withOpacity(0.2), - borderRadius: BorderRadius.circular(8), + color: Colors.orange.shade400 + .withOpacity(0.2), + borderRadius: + BorderRadius.circular(8), ), child: Text( 'Admin', @@ -360,7 +365,7 @@ class _CreateTaskScreenState extends State { ), ), const SizedBox(height: 32), - + // Submit button SizedBox( width: double.infinity, @@ -399,4 +404,4 @@ class _CreateTaskScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/tasks/task_detail_screen.dart b/lib/screens/tasks/task_detail_screen.dart index 82da6e7..3d50ab4 100644 --- a/lib/screens/tasks/task_detail_screen.dart +++ b/lib/screens/tasks/task_detail_screen.dart @@ -1,858 +1,861 @@ -import 'package:flutter/material.dart'; -import '../../services/supabase_service.dart'; -import '../../widgets/custom_widgets.dart'; - -class TaskDetailScreen extends StatefulWidget { - final String taskId; - - const TaskDetailScreen({super.key, required this.taskId}); - - @override - State createState() => _TaskDetailScreenState(); -} - -class _TaskDetailScreenState extends State { - final _supabaseService = SupabaseService(); - bool _isLoading = true; - Map? _taskDetails; - List> _comments = []; - bool _isAdmin = false; - final _commentController = TextEditingController(); - - @override - void initState() { - super.initState(); - _loadTaskDetails(); - _checkUserRole(); - } - - @override - void dispose() { - _commentController.dispose(); - super.dispose(); - } - - Future _checkUserRole() async { - final userProfile = await _supabaseService.getCurrentUserProfile(); - if (mounted) { - setState(() { - _isAdmin = userProfile?['role'] == 'admin'; - }); - } - } - - Future _loadTaskDetails() async { - setState(() { - _isLoading = true; - }); - - try { - final details = await _supabaseService.getTaskDetails(widget.taskId); - - if (mounted && details != null) { - setState(() { - _taskDetails = details['task']; - _comments = List>.from(details['comments']); - _isLoading = false; - }); - } else if (mounted) { - setState(() { - _isLoading = false; - }); - } - } catch (e) { - debugPrint('Error loading task details: $e'); - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - Future _updateTaskStatus(String status) async { - try { - final success = await _supabaseService.updateTaskStatus( - taskId: widget.taskId, - status: status, - ); - - if (mounted) { - if (success == true) { - // Reload task details - await _loadTaskDetails(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Task status updated to ${_getStatusLabel(status)}', - ), - backgroundColor: Colors.green, - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to update task status'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - debugPrint('Error updating task status: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error updating task status: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _updateTaskApproval(String approvalStatus) async { - try { - await _supabaseService.updateTaskApproval( - taskId: widget.taskId, - approvalStatus: approvalStatus, - ); - - // Reload task details - _loadTaskDetails(); - - // Show success message - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Task ${approvalStatus == 'approved' ? 'approved' : 'rejected'}', - ), - backgroundColor: - approvalStatus == 'approved' ? Colors.green : Colors.red, - ), - ); - } - } catch (e) { - debugPrint('Error updating task approval: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error updating task approval: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _deleteTask() async { - // Show confirmation dialog - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: const Color(0xFF2A2A2A), - title: const Text( - 'Delete Task', - style: TextStyle(color: Colors.white), - ), - content: const Text( - 'Are you sure you want to delete this task? This action cannot be undone.', - style: TextStyle(color: Colors.white70), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('Delete'), - ), - ], - ); - }, - ); - - if (confirm != true) return; - - try { - final result = await _supabaseService.deleteTask(widget.taskId); - - if (mounted) { - if (result['success']) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Task deleted successfully'), - backgroundColor: Colors.green, - ), - ); - Navigator.of(context).pop(true); // Return true to trigger refresh - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error deleting task: ${result['error']}'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - debugPrint('Error deleting task: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error deleting task: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _addComment() async { - if (_commentController.text.trim().isEmpty) return; - - try { - final result = await _supabaseService.addTaskComment( - taskId: widget.taskId, - content: _commentController.text.trim(), - ); - - if (result['success'] && mounted) { - _commentController.clear(); - _loadTaskDetails(); - } else if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error adding comment: ${result['error']}'), - backgroundColor: Colors.red, - ), - ); - } - } catch (e) { - debugPrint('Error adding comment: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error adding comment: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - String _getStatusLabel(String status) { - switch (status) { - case 'todo': - return 'To Do'; - case 'in_progress': - return 'In Progress'; - case 'completed': - return 'Completed'; - default: - return 'Unknown'; - } - } - - Color _getStatusColor(String status) { - switch (status) { - case 'todo': - return Colors.blue; - case 'in_progress': - return Colors.orange; - case 'completed': - return Colors.green; - default: - return Colors.grey; - } - } - - Color _getApprovalColor(String approvalStatus) { - switch (approvalStatus) { - case 'approved': - return Colors.green; - case 'rejected': - return Colors.red; - case 'pending': - default: - return Colors.grey; - } - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - appBar: AppBar( - title: const Text('Task Details'), - backgroundColor: Colors.green.shade800, - ), - body: const Center(child: CustomLoading()), - ); - } - - if (_taskDetails == null) { - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - appBar: AppBar( - title: const Text('Task Details'), - backgroundColor: Colors.green.shade800, - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 80, color: Colors.red.shade400), - const SizedBox(height: 16), - const Text( - 'Task not found', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - Text( - 'The task may have been deleted', - style: TextStyle(fontSize: 16, color: Colors.grey.shade400), - ), - ], - ), - ), - ); - } - - final String title = _taskDetails!['title'] ?? 'Untitled Task'; - final String description = _taskDetails!['description'] ?? 'No description'; - final String status = _taskDetails!['status'] ?? 'todo'; - final String approvalStatus = _taskDetails!['approval_status'] ?? 'pending'; - final String creatorName = - _taskDetails!['creator']?['full_name'] ?? 'Unknown'; - final String assigneeName = - _taskDetails!['assignee']?['full_name'] ?? 'Unassigned'; - - // Format due date if available - String dueDate = 'No due date'; - if (_taskDetails!['due_date'] != null) { - final DateTime date = DateTime.parse(_taskDetails!['due_date']); - dueDate = '${date.day}/${date.month}/${date.year}'; - } - - final Color statusColor = _getStatusColor(status); - final Color approvalColor = _getApprovalColor(approvalStatus); - - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - appBar: AppBar( - title: const Text('Task Details'), - backgroundColor: Colors.green.shade800, - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: _loadTaskDetails, - tooltip: 'Refresh', - ), - // Show delete button if user is creator or admin - if (_isAdmin || - _taskDetails!['created_by'] == _supabaseService.currentUser?.id) - IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: _deleteTask, - tooltip: 'Delete Task', - color: Colors.red.shade300, - ), - ], - ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Task header - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: statusColor), - ), - child: Row( - children: [ - Icon( - status == 'todo' - ? Icons.assignment_outlined - : status == 'in_progress' - ? Icons.pending_actions_outlined - : Icons.task_alt_outlined, - color: statusColor, - size: 16, - ), - const SizedBox(width: 8), - Text( - _getStatusLabel(status), - style: TextStyle( - color: statusColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: approvalColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: approvalColor), - ), - child: Text( - approvalStatus.toUpperCase(), - style: TextStyle( - color: approvalColor, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Text( - title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - Text( - 'Due: $dueDate', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 14, - ), - ), - const SizedBox(height: 16), - const Divider(color: Colors.grey), - const SizedBox(height: 16), - Text( - description, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - CircleAvatar( - radius: 16, - backgroundColor: Colors.blue.shade700, - child: Text( - creatorName.isNotEmpty - ? creatorName[0].toUpperCase() - : '?', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - Text( - 'Created by $creatorName', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - ], - ), - if (assigneeName != 'Unassigned') ...[ - const SizedBox(height: 8), - Row( - children: [ - CircleAvatar( - radius: 16, - backgroundColor: Colors.purple.shade700, - child: Text( - assigneeName.isNotEmpty - ? assigneeName[0].toUpperCase() - : '?', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - Text( - 'Assigned to $assigneeName', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - ], - ), - ], - ], - ), - ), - - // Action buttons - if (status != 'completed' || - (_isAdmin && approvalStatus == 'pending')) - Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Actions', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (status == 'todo') - ElevatedButton.icon( - onPressed: - () => _updateTaskStatus('in_progress'), - icon: const Icon( - Icons.play_arrow, - color: Colors.white, - ), - label: const Text( - 'Start', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange.shade600, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), - if (status == 'in_progress') - ElevatedButton.icon( - onPressed: - () => _updateTaskStatus('completed'), - icon: const Icon( - Icons.check_circle, - color: Colors.white, - ), - label: const Text( - 'Complete', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade600, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), - if (_isAdmin && approvalStatus == 'pending') ...[ - ElevatedButton.icon( - onPressed: - () => _updateTaskApproval('approved'), - icon: const Icon( - Icons.check, - color: Colors.white, - ), - label: const Text( - 'Approve', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade600, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), - ElevatedButton.icon( - onPressed: - () => _updateTaskApproval('rejected'), - icon: const Icon( - Icons.close, - color: Colors.white, - ), - label: const Text( - 'Reject', - style: TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade600, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), - ], - ], - ), - ], - ), - ), - - // Comments section - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Comments (${_comments.length})', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - if (_comments.isNotEmpty) - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.sort, size: 16), - label: const Text('Latest first'), - style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade400, - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Comment list - if (_comments.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Column( - children: [ - Icon( - Icons.chat_bubble_outline, - size: 48, - color: Colors.grey.shade600, - ), - const SizedBox(height: 16), - Text( - 'No comments yet', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Text( - 'Be the first to comment', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, - ), - ), - ], - ), - ), - ) - else - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _comments.length, - itemBuilder: (context, index) { - final comment = _comments[index]; - final userName = - comment['user']?['full_name'] ?? 'Unknown'; - final content = comment['content'] ?? ''; - final createdAt = DateTime.parse( - comment['created_at'], - ); - - // Format date - final now = DateTime.now(); - final difference = now.difference(createdAt); - String formattedDate; - - if (difference.inDays > 0) { - formattedDate = '${difference.inDays}d ago'; - } else if (difference.inHours > 0) { - formattedDate = '${difference.inHours}h ago'; - } else if (difference.inMinutes > 0) { - formattedDate = '${difference.inMinutes}m ago'; - } else { - formattedDate = 'Just now'; - } - - return Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CircleAvatar( - radius: 16, - backgroundColor: - Colors.green.shade700, - child: Text( - userName.isNotEmpty - ? userName[0].toUpperCase() - : '?', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - Text( - userName, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - Text( - formattedDate, - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, - ), - ), - ], - ), - const SizedBox(height: 12), - Text( - content, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - ], - ), - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - - // Comment input - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, -5), - ), - ], - ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _commentController, - decoration: InputDecoration( - hintText: 'Add a comment...', - hintStyle: TextStyle(color: Colors.grey.shade400), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: BorderSide.none, - ), - filled: true, - fillColor: const Color(0xFF1A1A1A), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - style: const TextStyle(color: Colors.white), - maxLines: 3, - minLines: 1, - ), - ), - const SizedBox(width: 8), - CircleAvatar( - radius: 24, - backgroundColor: Colors.green.shade400, - child: IconButton( - icon: const Icon(Icons.send, color: Colors.white), - onPressed: _addComment, - ), - ), - ], - ), - ), - ], - ), - ); - } -} +import 'package:flutter/material.dart'; +import '../../services/supabase_service.dart'; +import '../../widgets/custom_widgets.dart'; + +class TaskDetailScreen extends StatefulWidget { + final String taskId; + + const TaskDetailScreen({super.key, required this.taskId}); + + @override + State createState() => _TaskDetailScreenState(); +} + +class _TaskDetailScreenState extends State { + final _supabaseService = SupabaseService(); + bool _isLoading = true; + Map? _taskDetails; + List> _comments = []; + bool _isAdmin = false; + final _commentController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadTaskDetails(); + _checkUserRole(); + } + + @override + void dispose() { + _commentController.dispose(); + super.dispose(); + } + + Future _checkUserRole() async { + final userProfile = await _supabaseService.getCurrentUserProfile(); + if (mounted) { + setState(() { + _isAdmin = userProfile?['role'] == 'admin'; + }); + } + } + + Future _loadTaskDetails() async { + setState(() { + _isLoading = true; + }); + + try { + final details = await _supabaseService.getTaskDetails(widget.taskId); + + if (mounted && details != null) { + setState(() { + _taskDetails = details['task']; + _comments = List>.from(details['comments']); + _isLoading = false; + }); + } else if (mounted) { + setState(() { + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Error loading task details: $e'); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _updateTaskStatus(String status) async { + try { + final success = await _supabaseService.updateTaskStatus( + taskId: widget.taskId, + status: status, + ); + + if (mounted) { + if (success == true) { + // Reload task details + await _loadTaskDetails(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Task status updated to ${_getStatusLabel(status)}', + ), + backgroundColor: Colors.green, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to update task status'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + debugPrint('Error updating task status: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating task status: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _updateTaskApproval(String approvalStatus) async { + try { + await _supabaseService.updateTaskApproval( + taskId: widget.taskId, + approvalStatus: approvalStatus, + ); + + // Reload task details + _loadTaskDetails(); + + // Show success message + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Task ${approvalStatus == 'approved' ? 'approved' : 'rejected'}', + ), + backgroundColor: + approvalStatus == 'approved' ? Colors.green : Colors.red, + ), + ); + } + } catch (e) { + debugPrint('Error updating task approval: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating task approval: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _deleteTask() async { + // Show confirmation dialog + final bool? confirm = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( + 'Delete Task', + style: Theme.of(context).textTheme.titleLarge, + ), + content: Text( + 'Are you sure you want to delete this task? This action cannot be undone.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (confirm != true) return; + + try { + final result = await _supabaseService.deleteTask(widget.taskId); + + if (mounted) { + if (result['success']) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Task deleted successfully'), + backgroundColor: Colors.green, + ), + ); + Navigator.of(context).pop(true); // Return true to trigger refresh + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error deleting task: ${result['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + debugPrint('Error deleting task: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error deleting task: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _addComment() async { + if (_commentController.text.trim().isEmpty) return; + + try { + final result = await _supabaseService.addTaskComment( + taskId: widget.taskId, + content: _commentController.text.trim(), + ); + + if (result['success'] && mounted) { + _commentController.clear(); + _loadTaskDetails(); + } else if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error adding comment: ${result['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } catch (e) { + debugPrint('Error adding comment: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error adding comment: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + String _getStatusLabel(String status) { + switch (status) { + case 'todo': + return 'To Do'; + case 'in_progress': + return 'In Progress'; + case 'completed': + return 'Completed'; + default: + return 'Unknown'; + } + } + + Color _getStatusColor(String status) { + switch (status) { + case 'todo': + return Colors.blue; + case 'in_progress': + return Colors.orange; + case 'completed': + return Colors.green; + default: + return Colors.grey; + } + } + + Color _getApprovalColor(String approvalStatus) { + switch (approvalStatus) { + case 'approved': + return Colors.green; + case 'rejected': + return Colors.red; + case 'pending': + default: + return Colors.grey; + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Task Details'), + backgroundColor: Colors.green.shade800, + ), + body: const Center(child: CustomLoading()), + ); + } + + if (_taskDetails == null) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Task Details'), + backgroundColor: Colors.green.shade800, + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 80, color: Colors.red.shade400), + const SizedBox(height: 16), + const Text( + 'Task not found', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Text( + 'The task may have been deleted', + style: TextStyle(fontSize: 16, color: Colors.grey.shade400), + ), + ], + ), + ), + ); + } + + final String title = _taskDetails!['title'] ?? 'Untitled Task'; + final String description = _taskDetails!['description'] ?? 'No description'; + final String status = _taskDetails!['status'] ?? 'todo'; + final String approvalStatus = _taskDetails!['approval_status'] ?? 'pending'; + final String creatorName = + _taskDetails!['creator']?['full_name'] ?? 'Unknown'; + final String assigneeName = + _taskDetails!['assignee']?['full_name'] ?? 'Unassigned'; + + // Format due date if available + String dueDate = 'No due date'; + if (_taskDetails!['due_date'] != null) { + final DateTime date = DateTime.parse(_taskDetails!['due_date']); + dueDate = '${date.day}/${date.month}/${date.year}'; + } + + final Color statusColor = _getStatusColor(status); + final Color approvalColor = _getApprovalColor(approvalStatus); + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Task Details'), + backgroundColor: Colors.green.shade800, + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadTaskDetails, + tooltip: 'Refresh', + ), + // Show delete button if user is creator or admin + if (_isAdmin || + _taskDetails!['created_by'] == _supabaseService.currentUser?.id) + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: _deleteTask, + tooltip: 'Delete Task', + color: Colors.red.shade300, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Task header + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Theme.of(context) + .colorScheme + .shadow + .withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: statusColor), + ), + child: Row( + children: [ + Icon( + status == 'todo' + ? Icons.assignment_outlined + : status == 'in_progress' + ? Icons.pending_actions_outlined + : Icons.task_alt_outlined, + color: statusColor, + size: 16, + ), + const SizedBox(width: 8), + Text( + _getStatusLabel(status), + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: approvalColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: approvalColor), + ), + child: Text( + approvalStatus.toUpperCase(), + style: TextStyle( + color: approvalColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Text( + 'Due: $dueDate', + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 14, + ), + ), + const SizedBox(height: 16), + const Divider(color: Colors.grey), + const SizedBox(height: 16), + Text( + description, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade700, + child: Text( + creatorName.isNotEmpty + ? creatorName[0].toUpperCase() + : '?', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Text( + 'Created by $creatorName', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 14, + ), + ), + ], + ), + if (assigneeName != 'Unassigned') ...[ + const SizedBox(height: 8), + Row( + children: [ + CircleAvatar( + radius: 16, + backgroundColor: Colors.purple.shade700, + child: Text( + assigneeName.isNotEmpty + ? assigneeName[0].toUpperCase() + : '?', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Text( + 'Assigned to $assigneeName', + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurface, + fontSize: 14, + ), + ), + ], + ), + ], + ], + ), + ), + + // Action buttons + if (status != 'completed' || + (_isAdmin && approvalStatus == 'pending')) + Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Actions', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (status == 'todo') + ElevatedButton.icon( + onPressed: () => + _updateTaskStatus('in_progress'), + icon: const Icon( + Icons.play_arrow, + color: Colors.white, + ), + label: const Text( + 'Start', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange.shade600, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + if (status == 'in_progress') + ElevatedButton.icon( + onPressed: () => + _updateTaskStatus('completed'), + icon: const Icon( + Icons.check_circle, + color: Colors.white, + ), + label: const Text( + 'Complete', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + if (_isAdmin && approvalStatus == 'pending') ...[ + ElevatedButton.icon( + onPressed: () => + _updateTaskApproval('approved'), + icon: const Icon( + Icons.check, + color: Colors.white, + ), + label: const Text( + 'Approve', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ElevatedButton.icon( + onPressed: () => + _updateTaskApproval('rejected'), + icon: const Icon( + Icons.close, + color: Colors.white, + ), + label: const Text( + 'Reject', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ], + ], + ), + ], + ), + ), + + // Comments section + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Comments (${_comments.length})', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + if (_comments.isNotEmpty) + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.sort, size: 16), + label: const Text('Latest first'), + style: TextButton.styleFrom( + foregroundColor: Colors.grey.shade400, + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Comment list + if (_comments.isEmpty) + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Icon( + Icons.chat_bubble_outline, + size: 48, + color: Colors.grey.shade600, + ), + const SizedBox(height: 16), + Text( + 'No comments yet', + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Text( + 'Be the first to comment', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _comments.length, + itemBuilder: (context, index) { + final comment = _comments[index]; + final userName = + comment['user']?['full_name'] ?? 'Unknown'; + final content = comment['content'] ?? ''; + final createdAt = DateTime.parse( + comment['created_at'], + ); + + // Format date + final now = DateTime.now(); + final difference = now.difference(createdAt); + String formattedDate; + + if (difference.inDays > 0) { + formattedDate = '${difference.inDays}d ago'; + } else if (difference.inHours > 0) { + formattedDate = '${difference.inHours}h ago'; + } else if (difference.inMinutes > 0) { + formattedDate = '${difference.inMinutes}m ago'; + } else { + formattedDate = 'Just now'; + } + + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 16, + backgroundColor: + Colors.green.shade700, + child: Text( + userName.isNotEmpty + ? userName[0].toUpperCase() + : '?', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Text( + userName, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + Text( + formattedDate, + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + content, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + + // Comment input + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _commentController, + decoration: InputDecoration( + hintText: 'Add a comment...', + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), + maxLines: 3, + minLines: 1, + ), + ), + const SizedBox(width: 8), + CircleAvatar( + radius: 24, + backgroundColor: Colors.green.shade400, + child: IconButton( + icon: const Icon(Icons.send, color: Colors.white), + onPressed: _addComment, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/tasks/task_screen.dart b/lib/screens/tasks/task_screen.dart index f79189d..f0cdbdd 100644 --- a/lib/screens/tasks/task_screen.dart +++ b/lib/screens/tasks/task_screen.dart @@ -137,9 +137,9 @@ class _TaskScreenState extends State { @override Widget build(BuildContext context) { if (_isLoading) { - return const Scaffold( - backgroundColor: Color(0xFF1A1A1A), - body: Center(child: CustomLoading()), + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: const Center(child: CustomLoading()), ); } @@ -158,7 +158,7 @@ class _TaskScreenState extends State { final totalTasks = _tasks.length; return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, floatingActionButton: FloatingActionButton( onPressed: () async { final result = await Navigator.push( @@ -205,6 +205,7 @@ class _TaskScreenState extends State { _tasks.where((task) => task['status'] == _selectedStatus).toList(); if (filteredTasks.isEmpty) { + final colorScheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -216,7 +217,7 @@ class _TaskScreenState extends State { ? Icons.pending_actions_outlined : Icons.task_alt_outlined, size: 80, - color: Colors.grey.shade600, + color: colorScheme.onSurfaceVariant, ), const SizedBox(height: 16), Text( @@ -224,7 +225,7 @@ class _TaskScreenState extends State { style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.grey.shade400, + color: colorScheme.onSurface, ), ), const SizedBox(height: 8), @@ -236,7 +237,7 @@ class _TaskScreenState extends State { : 'Completed tasks will appear here', style: TextStyle( fontSize: 14, - color: Colors.grey.shade600, + color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), @@ -398,7 +399,9 @@ class _TaskScreenState extends State { child: Text( status['label'] as String, style: TextStyle( - color: isSelected ? color : Colors.white70, + color: isSelected + ? color + : Theme.of(context).colorScheme.onSurfaceVariant, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), @@ -429,9 +432,9 @@ class _TaskScreenState extends State { }) { return Container( padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Color(0xFF2D2D2D), - borderRadius: BorderRadius.only( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), ), @@ -466,7 +469,10 @@ class _TaskScreenState extends State { borderRadius: BorderRadius.circular(10), child: Stack( children: [ - Container(height: 8, color: Colors.grey.shade800), + Container( + height: 8, + color: + Theme.of(context).colorScheme.surfaceContainerHighest), Row( children: [ _buildProgressBar( @@ -511,7 +517,9 @@ class _TaskScreenState extends State { const SizedBox(height: 4), Text( label, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12), ), ], ); @@ -597,11 +605,11 @@ class _TaskCard extends StatelessWidget { child: Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Theme.of(context).colorScheme.shadow.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), @@ -688,12 +696,18 @@ class _TaskCard extends StatelessWidget { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), - title: const Text('Delete Task', - style: TextStyle(color: Colors.white)), - content: const Text( + backgroundColor: + Theme.of(context).colorScheme.surface, + title: Text('Delete Task', + style: Theme.of(context) + .textTheme + .titleLarge), + content: Text( 'Are you sure you want to delete this task? This action cannot be undone.', - style: TextStyle(color: Colors.white70), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), ), actions: [ TextButton( @@ -771,7 +785,10 @@ class _TaskCard extends StatelessWidget { Text( dueDate, style: TextStyle( - color: Colors.grey.shade400, fontSize: 12), + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 12), ), ], ), @@ -791,8 +808,8 @@ class _TaskCard extends StatelessWidget { Expanded( child: Text( title, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), @@ -819,7 +836,9 @@ class _TaskCard extends StatelessWidget { const SizedBox(height: 8), Text( description, - style: TextStyle(color: Colors.grey.shade400, fontSize: 14), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -847,7 +866,9 @@ class _TaskCard extends StatelessWidget { Text( 'Created by $creatorName', style: TextStyle( - color: Colors.grey.shade400, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, fontSize: 12, ), ), @@ -874,7 +895,9 @@ class _TaskCard extends StatelessWidget { Text( 'Assigned to $assigneeName', style: TextStyle( - color: Colors.grey.shade400, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, fontSize: 12, ), ), @@ -889,7 +912,7 @@ class _TaskCard extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - color: Colors.grey.shade800, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), @@ -900,7 +923,7 @@ class _TaskCard extends StatelessWidget { children: [ Icon( Icons.drag_indicator, - color: Colors.grey.shade500, + color: Theme.of(context).colorScheme.onSurfaceVariant, size: 20, ), const SizedBox(width: 8), diff --git a/lib/screens/tickets/create_ticket_screen.dart b/lib/screens/tickets/create_ticket_screen.dart index f44ccdc..2455b46 100644 --- a/lib/screens/tickets/create_ticket_screen.dart +++ b/lib/screens/tickets/create_ticket_screen.dart @@ -13,40 +13,40 @@ class _CreateTicketScreenState extends State { final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _supabaseService = SupabaseService(); - + String _selectedPriority = 'medium'; String _selectedCategory = 'Bug'; List> _teamMembers = []; String? _selectedAssignee; bool _isLoading = true; - + @override void initState() { super.initState(); _loadTeamMembers(); } - + @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } - + Future _loadTeamMembers() async { setState(() { _isLoading = true; }); - + try { final userProfile = await _supabaseService.getCurrentUserProfile(); if (userProfile != null && userProfile['team_id'] != null) { // Load team members cache first await _supabaseService.loadTeamMembers(userProfile['team_id']); - + // Get team members from cache final teamMembers = _supabaseService.teamMembersCache; - + if (mounted) { setState(() { _teamMembers = teamMembers; @@ -67,14 +67,14 @@ class _CreateTicketScreenState extends State { } } } - + Future _createTicket() async { if (!_formKey.currentState!.validate()) return; - + setState(() { _isLoading = true; }); - + try { final result = await _supabaseService.createTicket( title: _titleController.text.trim(), @@ -83,7 +83,7 @@ class _CreateTicketScreenState extends State { category: _selectedCategory, assignedToUserId: _selectedAssignee, ); - + if (result['success']) { if (mounted) { Navigator.pop(context, true); @@ -93,7 +93,7 @@ class _CreateTicketScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to create ticket: ${result['error']}'), @@ -108,7 +108,7 @@ class _CreateTicketScreenState extends State { setState(() { _isLoading = false; }); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error creating ticket: $e'), @@ -122,11 +122,11 @@ class _CreateTicketScreenState extends State { @override Widget build(BuildContext context) { final ticketCategories = _supabaseService.getTicketCategories(); - + return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), + backgroundColor: Theme.of(context).colorScheme.surface, title: const Text('Create Ticket'), actions: [ if (_isLoading) @@ -165,7 +165,7 @@ class _CreateTicketScreenState extends State { hintText: 'Enter ticket title', hintStyle: TextStyle(color: Colors.grey.shade600), filled: true, - fillColor: const Color(0xFF2D2D2D), + fillColor: Theme.of(context).colorScheme.surface, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, @@ -181,25 +181,28 @@ class _CreateTicketScreenState extends State { }, ), const SizedBox(height: 16), - + // Description TextFormField( controller: _descriptionController, decoration: InputDecoration( labelText: 'Description', labelStyle: TextStyle(color: Colors.grey.shade400), - hintText: 'Enter ticket description (max 75 words recommended)', + hintText: + 'Enter ticket description (max 75 words recommended)', hintStyle: TextStyle(color: Colors.grey.shade600), filled: true, - fillColor: const Color(0xFF2D2D2D), + fillColor: Theme.of(context).colorScheme.surface, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), - prefixIcon: const Icon(Icons.description, color: Colors.grey), + prefixIcon: + const Icon(Icons.description, color: Colors.grey), alignLabelWithHint: true, ), - style: const TextStyle(color: Colors.white), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), maxLines: 5, validator: (value) { if (value == null || value.trim().isEmpty) { @@ -209,7 +212,7 @@ class _CreateTicketScreenState extends State { }, ), const SizedBox(height: 24), - + // Priority Text( 'Priority', @@ -223,13 +226,14 @@ class _CreateTicketScreenState extends State { children: [ _buildPriorityOption('low', 'Low', Colors.green.shade400), const SizedBox(width: 8), - _buildPriorityOption('medium', 'Medium', Colors.orange.shade400), + _buildPriorityOption( + 'medium', 'Medium', Colors.orange.shade400), const SizedBox(width: 8), _buildPriorityOption('high', 'High', Colors.red.shade400), ], ), const SizedBox(height: 24), - + // Category Text( 'Category', @@ -242,7 +246,7 @@ class _CreateTicketScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), ), child: DropdownButtonHideUnderline( @@ -261,14 +265,15 @@ class _CreateTicketScreenState extends State { child: Text(category), ); }).toList(), - dropdownColor: const Color(0xFF2D2D2D), - style: const TextStyle(color: Colors.white), + dropdownColor: Theme.of(context).colorScheme.surface, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), isExpanded: true, ), ), ), const SizedBox(height: 24), - + // Assignee Text( 'Assign To (Optional)', @@ -281,7 +286,7 @@ class _CreateTicketScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), ), child: DropdownButtonHideUnderline( @@ -310,7 +315,8 @@ class _CreateTicketScreenState extends State { radius: 12, backgroundColor: Colors.green.shade700, child: Text( - member['full_name'] != null && member['full_name'].isNotEmpty + member['full_name'] != null && + member['full_name'].isNotEmpty ? member['full_name'][0].toUpperCase() : '?', style: const TextStyle( @@ -330,7 +336,8 @@ class _CreateTicketScreenState extends State { vertical: 2, ), decoration: BoxDecoration( - color: Colors.orange.shade400.withOpacity(0.2), + color: Colors.orange.shade400 + .withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -347,14 +354,15 @@ class _CreateTicketScreenState extends State { ); }).toList(), ], - dropdownColor: const Color(0xFF2D2D2D), - style: const TextStyle(color: Colors.white), + dropdownColor: Theme.of(context).colorScheme.surface, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), isExpanded: true, ), ), ), const SizedBox(height: 32), - + // Submit button ElevatedButton( onPressed: _isLoading ? null : _createTicket, @@ -364,7 +372,8 @@ class _CreateTicketScreenState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - disabledBackgroundColor: Colors.grey.shade800, + disabledBackgroundColor: + Theme.of(context).colorScheme.surfaceContainerHighest, ), child: _isLoading ? const SizedBox( @@ -389,10 +398,10 @@ class _CreateTicketScreenState extends State { ), ); } - + Widget _buildPriorityOption(String value, String label, Color color) { final isSelected = _selectedPriority == value; - + return Expanded( child: GestureDetector( onTap: () { @@ -403,7 +412,9 @@ class _CreateTicketScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( - color: isSelected ? color.withOpacity(0.2) : const Color(0xFF2D2D2D), + color: isSelected + ? color.withOpacity(0.2) + : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? color : Colors.transparent, @@ -434,4 +445,4 @@ class _CreateTicketScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/tickets/ticket_detail_screen.dart b/lib/screens/tickets/ticket_detail_screen.dart index 1abc834..e6c0754 100644 --- a/lib/screens/tickets/ticket_detail_screen.dart +++ b/lib/screens/tickets/ticket_detail_screen.dart @@ -1,1036 +1,1046 @@ -import 'package:flutter/material.dart'; -import '../../services/supabase_service.dart'; -import '../../widgets/custom_widgets.dart'; - -class TicketDetailScreen extends StatefulWidget { - final String ticketId; - - const TicketDetailScreen({super.key, required this.ticketId}); - - @override - State createState() => _TicketDetailScreenState(); -} - -class _TicketDetailScreenState extends State { - final _supabaseService = SupabaseService(); - final _commentController = TextEditingController(); - - bool _isLoading = true; - bool _isAdmin = false; - Map? _ticket; - List> _comments = []; - - @override - void initState() { - super.initState(); - _loadTicketDetails(); - } - - @override - void dispose() { - _commentController.dispose(); - super.dispose(); - } - - Future _loadTicketDetails() async { - setState(() { - _isLoading = true; - }); - - try { - // Check if user is admin - final userProfile = await _supabaseService.getCurrentUserProfile(); - if (mounted) { - setState(() { - _isAdmin = userProfile?['role'] == 'admin'; - }); - } - - // Load team members first - if (userProfile != null && userProfile['team_id'] != null) { - await _supabaseService.loadTeamMembers(userProfile['team_id']); - } - - // Get ticket details - final result = await _supabaseService.getTicketDetails(widget.ticketId); - - if (result != null && mounted) { - setState(() { - _ticket = result['ticket']; - _comments = List>.from(result['comments']); - _isLoading = false; - }); - } else if (mounted) { - setState(() { - _isLoading = false; - }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to load ticket details'), - backgroundColor: Colors.red, - ), - ); - } - } catch (e) { - debugPrint('Error loading ticket details: $e'); - if (mounted) { - setState(() { - _isLoading = false; - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error loading ticket details: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _deleteTicket() async { - // Show confirmation dialog - final bool? confirm = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: const Color(0xFF2A2A2A), - title: const Text( - 'Delete Ticket', - style: TextStyle(color: Colors.white), - ), - content: const Text( - 'Are you sure you want to delete this ticket? This action cannot be undone.', - style: TextStyle(color: Colors.white70), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), - child: const Text('Delete'), - ), - ], - ); - }, - ); - - if (confirm != true) return; - - try { - final result = await _supabaseService.deleteTicket(widget.ticketId); - - if (mounted) { - if (result['success']) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Ticket deleted successfully'), - backgroundColor: Colors.green, - ), - ); - Navigator.of(context).pop(true); // Return true to trigger refresh - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error deleting ticket: ${result['error']}'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - debugPrint('Error deleting ticket: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error deleting ticket: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _addComment() async { - if (_commentController.text.trim().isEmpty) return; - - try { - final result = await _supabaseService.addTicketComment( - ticketId: widget.ticketId, - content: _commentController.text.trim(), - ); - - if (result['success']) { - setState(() { - _comments.add(result['comment']); - _commentController.clear(); - }); - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to add comment: ${result['error']}'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - debugPrint('Error adding comment: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error adding comment: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _updateTicketStatus(String status) async { - try { - final result = await _supabaseService.updateTicketStatus( - ticketId: widget.ticketId, - status: status, - ); - - if (result['success']) { - setState(() { - if (_ticket != null) { - _ticket!['status'] = status; - } - }); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Ticket status updated successfully'), - backgroundColor: Colors.green, - ), - ); - } - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Failed to update ticket status: ${result['error']}'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - debugPrint('Error updating ticket status: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error updating ticket status: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _updateTicketPriority(String priority) async { - try { - final result = await _supabaseService.updateTicketPriority( - ticketId: widget.ticketId, - priority: priority, - ); - - if (result['success']) { - setState(() { - if (_ticket != null) { - _ticket!['priority'] = priority; - } - }); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Ticket priority updated successfully'), - backgroundColor: Colors.green, - ), - ); - } - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Failed to update ticket priority: ${result['error']}'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - debugPrint('Error updating ticket priority: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error updating ticket priority: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _assignTicket(String userId) async { - try { - final result = await _supabaseService.assignTicket( - ticketId: widget.ticketId, - userId: userId, - ); - - if (result['success']) { - // Reload ticket details to get updated assignee info - _loadTicketDetails(); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Ticket assigned successfully'), - backgroundColor: Colors.green, - ), - ); - } - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to assign ticket: ${result['error']}'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - debugPrint('Error assigning ticket: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error assigning ticket: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - // Show team members dialog for assignment - void _showAssignDialog() { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), - title: const Text( - 'Assign Ticket', - style: TextStyle(color: Colors.white), - ), - content: SizedBox( - width: double.maxFinite, - child: FutureBuilder>>( - future: _getTeamMembers(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (snapshot.hasError) { - return Text( - 'Error loading team members', - style: TextStyle(color: Colors.red.shade400), - ); - } - - final teamMembers = snapshot.data ?? []; - if (teamMembers.isEmpty) { - return const Text( - 'No team members found', - style: TextStyle(color: Colors.white), - ); - } - - return ListView.builder( - shrinkWrap: true, - itemCount: teamMembers.length, - itemBuilder: (context, index) { - final member = teamMembers[index]; - final fullName = member['full_name'] ?? 'Unknown'; - final isAdmin = member['role'] == 'admin'; - - return ListTile( - leading: CircleAvatar( - backgroundColor: isAdmin - ? Colors.orange.shade700 - : Colors.blue.shade700, - child: Text( - fullName.isNotEmpty ? fullName[0].toUpperCase() : '?', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - title: Text( - fullName, - style: const TextStyle(color: Colors.white), - ), - subtitle: Text( - isAdmin ? 'Admin' : 'Team Member', - style: TextStyle( - color: isAdmin - ? Colors.orange.shade400 - : Colors.grey.shade400, - ), - ), - onTap: () { - Navigator.pop(context); - _assignTicket(member['id']); - }, - ); - }, - ); - }, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text( - 'Cancel', - style: TextStyle(color: Colors.white), - ), - ), - ], - ); - }, - ); - } - - Future>> _getTeamMembers() async { - try { - final userProfile = await _supabaseService.getCurrentUserProfile(); - if (userProfile != null && userProfile['team_id'] != null) { - return _supabaseService.teamMembersCache; - } - return []; - } catch (e) { - debugPrint('Error getting team members: $e'); - return []; - } - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), - title: const Text('Ticket Details'), - ), - body: const Center(child: CustomLoading()), - ); - } - - if (_ticket == null) { - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), - title: const Text('Ticket Details'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - size: 80, - color: Colors.red.shade400, - ), - const SizedBox(height: 16), - const Text( - 'Ticket not found', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - Text( - 'The ticket may have been deleted or you do not have access to it.', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade400, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - final priority = _ticket!['priority'] as String; - final status = _ticket!['status'] as String; - final approvalStatus = _ticket!['approval_status'] as String; - - Color priorityColor; - switch (priority.toLowerCase()) { - case 'high': - priorityColor = Colors.red.shade400; - break; - case 'medium': - priorityColor = Colors.orange.shade400; - break; - case 'low': - priorityColor = Colors.green.shade400; - break; - default: - priorityColor = Colors.grey; - } - - Color statusColor; - switch (status) { - case 'open': - statusColor = Colors.blue.shade400; - break; - case 'in_progress': - statusColor = Colors.orange.shade400; - break; - case 'resolved': - statusColor = Colors.green.shade400; - break; - default: - statusColor = Colors.grey; - } - - Color approvalColor; - IconData approvalIcon; - switch (approvalStatus) { - case 'approved': - approvalColor = Colors.green.shade400; - approvalIcon = Icons.check_circle; - break; - case 'rejected': - approvalColor = Colors.red.shade400; - approvalIcon = Icons.cancel; - break; - case 'pending': - default: - approvalColor = Colors.grey; - approvalIcon = Icons.pending; - } - - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - appBar: AppBar( - backgroundColor: const Color(0xFF2D2D2D), - title: Text(_ticket!['ticket_number'] ?? 'Ticket Details'), - actions: [ - if (_isAdmin || - _ticket!['created_by'] == _supabaseService.currentUser?.id) - PopupMenuButton( - icon: const Icon(Icons.more_vert), - onSelected: (value) { - if (value == 'delete') { - _deleteTicket(); - } else if (value.startsWith('status:')) { - _updateTicketStatus(value.split(':')[1]); - } else if (value.startsWith('priority:')) { - _updateTicketPriority(value.split(':')[1]); - } - }, - itemBuilder: (context) => [ - if (_isAdmin) ...[ - const PopupMenuItem( - value: 'header_status', - enabled: false, - child: Text( - 'Change Status', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - const PopupMenuItem( - value: 'status:open', - child: Text('Open'), - ), - const PopupMenuItem( - value: 'status:in_progress', - child: Text('In Progress'), - ), - const PopupMenuItem( - value: 'status:resolved', - child: Text('Resolved'), - ), - const PopupMenuItem( - value: 'divider1', - enabled: false, - child: Divider(), - ), - const PopupMenuItem( - value: 'header_priority', - enabled: false, - child: Text( - 'Change Priority', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - const PopupMenuItem( - value: 'priority:high', - child: Text('High'), - ), - const PopupMenuItem( - value: 'priority:medium', - child: Text('Medium'), - ), - const PopupMenuItem( - value: 'priority:low', - child: Text('Low'), - ), - const PopupMenuItem( - value: 'divider2', - enabled: false, - child: Divider(), - ), - ], - const PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete_outline, color: Colors.red, size: 20), - SizedBox(width: 8), - Text('Delete Ticket', - style: TextStyle(color: Colors.red)), - ], - ), - ), - ], - ), - ], - ), - body: Column( - children: [ - Expanded( - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - // Ticket header - Card( - color: const Color(0xFF2D2D2D), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: priorityColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.flag, - color: priorityColor, - size: 14, - ), - const SizedBox(width: 4), - Text( - priority.toUpperCase(), - style: TextStyle( - color: priorityColor, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - status.toUpperCase().replaceAll('_', ' '), - style: TextStyle( - color: statusColor, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: approvalColor.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - approvalIcon, - color: approvalColor, - size: 14, - ), - const SizedBox(width: 4), - Text( - approvalStatus.toUpperCase(), - style: TextStyle( - color: approvalColor, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - const Spacer(), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.purple.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _ticket!['category'] ?? 'Other', - style: TextStyle( - color: Colors.purple.shade300, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Text( - _ticket!['title'] ?? 'Untitled Ticket', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Text( - _ticket!['description'] ?? 'No description provided.', - style: TextStyle( - color: Colors.grey.shade300, - fontSize: 16, - ), - ), - const SizedBox(height: 16), - const Divider(color: Colors.grey), - const SizedBox(height: 16), - Row( - children: [ - if (_ticket!['creator'] != null) ...[ - CircleAvatar( - radius: 16, - backgroundColor: Colors.green.shade700, - child: Text( - _ticket!['creator']['full_name'] != null && - _ticket!['creator']['full_name'] - .isNotEmpty - ? _ticket!['creator']['full_name'][0] - .toUpperCase() - : '?', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Created by', - style: TextStyle( - color: Colors.grey.shade500, - fontSize: 12, - ), - ), - Text( - _ticket!['creator']['full_name'] ?? - 'Unknown', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ], - const Spacer(), - if (_ticket!['assignee'] != null) ...[ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - 'Assigned to', - style: TextStyle( - color: Colors.grey.shade500, - fontSize: 12, - ), - ), - Text( - _ticket!['assignee']['full_name'] ?? - 'Unknown', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(width: 8), - CircleAvatar( - radius: 16, - backgroundColor: Colors.blue.shade700, - child: Text( - _ticket!['assignee']['full_name'] != null && - _ticket!['assignee']['full_name'] - .isNotEmpty - ? _ticket!['assignee']['full_name'][0] - .toUpperCase() - : '?', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ] else ...[ - ElevatedButton.icon( - onPressed: _showAssignDialog, - icon: const Icon(Icons.person_add, size: 16), - label: const Text('Assign'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 8), - ), - ), - ], - ], - ), - ], - ), - ), - ), - - const SizedBox(height: 24), - - // Comments section - Text( - 'Comments (${_comments.length})', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - if (_comments.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Icon( - Icons.chat_bubble_outline, - size: 48, - color: Colors.grey.shade600, - ), - const SizedBox(height: 16), - Text( - 'No comments yet', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Text( - 'Be the first to add a comment', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 14, - ), - ), - ], - ), - ), - ), - - ...List.generate(_comments.length, (index) { - final comment = _comments[index]; - return Card( - margin: const EdgeInsets.only(bottom: 16), - color: const Color(0xFF2D2D2D), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (comment['user'] != null) ...[ - CircleAvatar( - radius: 14, - backgroundColor: Colors.green.shade700, - child: Text( - comment['user']['full_name'] != null && - comment['user']['full_name'] - .isNotEmpty - ? comment['user']['full_name'][0] - .toUpperCase() - : '?', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - Text( - comment['user']['full_name'] ?? 'Unknown', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ], - const Spacer(), - Text( - _formatDate(comment['created_at']), - style: TextStyle( - color: Colors.grey.shade500, - fontSize: 12, - ), - ), - ], - ), - const SizedBox(height: 12), - Text( - comment['content'] ?? '', - style: TextStyle( - color: Colors.grey.shade300, - fontSize: 14, - ), - ), - ], - ), - ), - ); - }), - - // Add extra space at the bottom for the comment input - const SizedBox(height: 80), - ], - ), - ), - - // Comment input - Container( - padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Color(0xFF2D2D2D), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _commentController, - decoration: InputDecoration( - hintText: 'Add a comment...', - hintStyle: TextStyle(color: Colors.grey.shade500), - filled: true, - fillColor: const Color(0xFF1A1A1A), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - style: const TextStyle(color: Colors.white), - maxLines: 3, - minLines: 1, - ), - ), - const SizedBox(width: 8), - IconButton( - onPressed: _addComment, - icon: const Icon(Icons.send_rounded), - color: Colors.green.shade400, - iconSize: 28, - ), - ], - ), - ), - ], - ), - ); - } - - String _formatDate(String? dateString) { - if (dateString == null) return ''; - - try { - final date = DateTime.parse(dateString); - return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; - } catch (e) { - return ''; - } - } -} +import 'package:flutter/material.dart'; +import '../../services/supabase_service.dart'; +import '../../widgets/custom_widgets.dart'; + +class TicketDetailScreen extends StatefulWidget { + final String ticketId; + + const TicketDetailScreen({super.key, required this.ticketId}); + + @override + State createState() => _TicketDetailScreenState(); +} + +class _TicketDetailScreenState extends State { + final _supabaseService = SupabaseService(); + final _commentController = TextEditingController(); + + bool _isLoading = true; + bool _isAdmin = false; + Map? _ticket; + List> _comments = []; + + @override + void initState() { + super.initState(); + _loadTicketDetails(); + } + + @override + void dispose() { + _commentController.dispose(); + super.dispose(); + } + + Future _loadTicketDetails() async { + setState(() { + _isLoading = true; + }); + + try { + // Check if user is admin + final userProfile = await _supabaseService.getCurrentUserProfile(); + if (mounted) { + setState(() { + _isAdmin = userProfile?['role'] == 'admin'; + }); + } + + // Load team members first + if (userProfile != null && userProfile['team_id'] != null) { + await _supabaseService.loadTeamMembers(userProfile['team_id']); + } + + // Get ticket details + final result = await _supabaseService.getTicketDetails(widget.ticketId); + + if (result != null && mounted) { + setState(() { + _ticket = result['ticket']; + _comments = List>.from(result['comments']); + _isLoading = false; + }); + } else if (mounted) { + setState(() { + _isLoading = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to load ticket details'), + backgroundColor: Colors.red, + ), + ); + } + } catch (e) { + debugPrint('Error loading ticket details: $e'); + if (mounted) { + setState(() { + _isLoading = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading ticket details: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _deleteTicket() async { + // Show confirmation dialog + final bool? confirm = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( + 'Delete Ticket', + style: Theme.of(context).textTheme.titleLarge, + ), + content: Text( + 'Are you sure you want to delete this ticket? This action cannot be undone.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (confirm != true) return; + + try { + final result = await _supabaseService.deleteTicket(widget.ticketId); + + if (mounted) { + if (result['success']) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ticket deleted successfully'), + backgroundColor: Colors.green, + ), + ); + Navigator.of(context).pop(true); // Return true to trigger refresh + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error deleting ticket: ${result['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + debugPrint('Error deleting ticket: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error deleting ticket: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _addComment() async { + if (_commentController.text.trim().isEmpty) return; + + try { + final result = await _supabaseService.addTicketComment( + ticketId: widget.ticketId, + content: _commentController.text.trim(), + ); + + if (result['success']) { + setState(() { + _comments.add(result['comment']); + _commentController.clear(); + }); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to add comment: ${result['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + debugPrint('Error adding comment: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error adding comment: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _updateTicketStatus(String status) async { + try { + final result = await _supabaseService.updateTicketStatus( + ticketId: widget.ticketId, + status: status, + ); + + if (result['success']) { + setState(() { + if (_ticket != null) { + _ticket!['status'] = status; + } + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Ticket status updated successfully'), + backgroundColor: Colors.green, + ), + ); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to update ticket status: ${result['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + debugPrint('Error updating ticket status: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating ticket status: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _updateTicketPriority(String priority) async { + try { + final result = await _supabaseService.updateTicketPriority( + ticketId: widget.ticketId, + priority: priority, + ); + + if (result['success']) { + setState(() { + if (_ticket != null) { + _ticket!['priority'] = priority; + } + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Ticket priority updated successfully'), + backgroundColor: Colors.green, + ), + ); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to update ticket priority: ${result['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + debugPrint('Error updating ticket priority: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating ticket priority: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _assignTicket(String userId) async { + try { + final result = await _supabaseService.assignTicket( + ticketId: widget.ticketId, + userId: userId, + ); + + if (result['success']) { + // Reload ticket details to get updated assignee info + _loadTicketDetails(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Ticket assigned successfully'), + backgroundColor: Colors.green, + ), + ); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to assign ticket: ${result['error']}'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + debugPrint('Error assigning ticket: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error assigning ticket: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + // Show team members dialog for assignment + void _showAssignDialog() { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text( + 'Assign Ticket', + style: Theme.of(context).textTheme.titleLarge, + ), + content: SizedBox( + width: double.maxFinite, + child: FutureBuilder>>( + future: _getTeamMembers(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Text( + 'Error loading team members', + style: TextStyle(color: Colors.red.shade400), + ); + } + + final teamMembers = snapshot.data ?? []; + if (teamMembers.isEmpty) { + return Text( + 'No team members found', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), + ); + } + + return ListView.builder( + shrinkWrap: true, + itemCount: teamMembers.length, + itemBuilder: (context, index) { + final member = teamMembers[index]; + final fullName = member['full_name'] ?? 'Unknown'; + final isAdmin = member['role'] == 'admin'; + + return ListTile( + leading: CircleAvatar( + backgroundColor: isAdmin + ? Colors.orange.shade700 + : Colors.blue.shade700, + child: Text( + fullName.isNotEmpty ? fullName[0].toUpperCase() : '?', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + title: Text( + fullName, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), + ), + subtitle: Text( + isAdmin ? 'Admin' : 'Team Member', + style: TextStyle( + color: isAdmin + ? Colors.orange.shade400 + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + onTap: () { + Navigator.pop(context); + _assignTicket(member['id']); + }, + ); + }, + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ), + ], + ); + }, + ); + } + + Future>> _getTeamMembers() async { + try { + final userProfile = await _supabaseService.getCurrentUserProfile(); + if (userProfile != null && userProfile['team_id'] != null) { + return _supabaseService.teamMembersCache; + } + return []; + } catch (e) { + debugPrint('Error getting team members: $e'); + return []; + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + title: const Text('Ticket Details'), + ), + body: const Center(child: CustomLoading()), + ); + } + + if (_ticket == null) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + title: const Text('Ticket Details'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 80, + color: Colors.red.shade400, + ), + const SizedBox(height: 16), + Text( + 'Ticket not found', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + 'The ticket may have been deleted or you do not have access to it.', + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + final priority = _ticket!['priority'] as String; + final status = _ticket!['status'] as String; + final approvalStatus = _ticket!['approval_status'] as String; + + Color priorityColor; + switch (priority.toLowerCase()) { + case 'high': + priorityColor = Colors.red.shade400; + break; + case 'medium': + priorityColor = Colors.orange.shade400; + break; + case 'low': + priorityColor = Colors.green.shade400; + break; + default: + priorityColor = Colors.grey; + } + + Color statusColor; + switch (status) { + case 'open': + statusColor = Colors.blue.shade400; + break; + case 'in_progress': + statusColor = Colors.orange.shade400; + break; + case 'resolved': + statusColor = Colors.green.shade400; + break; + default: + statusColor = Colors.grey; + } + + Color approvalColor; + IconData approvalIcon; + switch (approvalStatus) { + case 'approved': + approvalColor = Colors.green.shade400; + approvalIcon = Icons.check_circle; + break; + case 'rejected': + approvalColor = Colors.red.shade400; + approvalIcon = Icons.cancel; + break; + case 'pending': + default: + approvalColor = Colors.grey; + approvalIcon = Icons.pending; + } + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + title: Text(_ticket!['ticket_number'] ?? 'Ticket Details'), + actions: [ + if (_isAdmin || + _ticket!['created_by'] == _supabaseService.currentUser?.id) + PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (value) { + if (value == 'delete') { + _deleteTicket(); + } else if (value.startsWith('status:')) { + _updateTicketStatus(value.split(':')[1]); + } else if (value.startsWith('priority:')) { + _updateTicketPriority(value.split(':')[1]); + } + }, + itemBuilder: (context) => [ + if (_isAdmin) ...[ + const PopupMenuItem( + value: 'header_status', + enabled: false, + child: Text( + 'Change Status', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + const PopupMenuItem( + value: 'status:open', + child: Text('Open'), + ), + const PopupMenuItem( + value: 'status:in_progress', + child: Text('In Progress'), + ), + const PopupMenuItem( + value: 'status:resolved', + child: Text('Resolved'), + ), + const PopupMenuItem( + value: 'divider1', + enabled: false, + child: Divider(), + ), + const PopupMenuItem( + value: 'header_priority', + enabled: false, + child: Text( + 'Change Priority', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + const PopupMenuItem( + value: 'priority:high', + child: Text('High'), + ), + const PopupMenuItem( + value: 'priority:medium', + child: Text('Medium'), + ), + const PopupMenuItem( + value: 'priority:low', + child: Text('Low'), + ), + const PopupMenuItem( + value: 'divider2', + enabled: false, + child: Divider(), + ), + ], + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete_outline, color: Colors.red, size: 20), + SizedBox(width: 8), + Text('Delete Ticket', + style: TextStyle(color: Colors.red)), + ], + ), + ), + ], + ), + ], + ), + body: Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Ticket header + Card( + color: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: priorityColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.flag, + color: priorityColor, + size: 14, + ), + const SizedBox(width: 4), + Text( + priority.toUpperCase(), + style: TextStyle( + color: priorityColor, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + status.toUpperCase().replaceAll('_', ' '), + style: TextStyle( + color: statusColor, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: approvalColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + approvalIcon, + color: approvalColor, + size: 14, + ), + const SizedBox(width: 4), + Text( + approvalStatus.toUpperCase(), + style: TextStyle( + color: approvalColor, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _ticket!['category'] ?? 'Other', + style: TextStyle( + color: Colors.purple.shade300, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + _ticket!['title'] ?? 'Untitled Ticket', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + _ticket!['description'] ?? 'No description provided.', + style: TextStyle( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16, + ), + ), + const SizedBox(height: 16), + Divider(color: Theme.of(context).dividerColor), + const SizedBox(height: 16), + Row( + children: [ + if (_ticket!['creator'] != null) ...[ + CircleAvatar( + radius: 16, + backgroundColor: Colors.green.shade700, + child: Text( + _ticket!['creator']['full_name'] != null && + _ticket!['creator']['full_name'] + .isNotEmpty + ? _ticket!['creator']['full_name'][0] + .toUpperCase() + : '?', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Created by', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 12, + ), + ), + Text( + _ticket!['creator']['full_name'] ?? + 'Unknown', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurface, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + const Spacer(), + if (_ticket!['assignee'] != null) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Assigned to', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 12, + ), + ), + Text( + _ticket!['assignee']['full_name'] ?? + 'Unknown', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurface, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(width: 8), + CircleAvatar( + radius: 16, + backgroundColor: Colors.blue.shade700, + child: Text( + _ticket!['assignee']['full_name'] != null && + _ticket!['assignee']['full_name'] + .isNotEmpty + ? _ticket!['assignee']['full_name'][0] + .toUpperCase() + : '?', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ] else ...[ + ElevatedButton.icon( + onPressed: _showAssignDialog, + icon: const Icon(Icons.person_add, size: 16), + label: const Text('Assign'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + ), + ), + ], + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Comments section + Text( + 'Comments (${_comments.length})', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + + if (_comments.isEmpty) + Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Icon( + Icons.chat_bubble_outline, + size: 48, + color: Colors.grey.shade600, + ), + const SizedBox(height: 16), + Text( + 'No comments yet', + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Text( + 'Be the first to add a comment', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + ], + ), + ), + ), + + ...List.generate(_comments.length, (index) { + final comment = _comments[index]; + return Card( + margin: const EdgeInsets.only(bottom: 16), + color: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (comment['user'] != null) ...[ + CircleAvatar( + radius: 14, + backgroundColor: Colors.green.shade700, + child: Text( + comment['user']['full_name'] != null && + comment['user']['full_name'] + .isNotEmpty + ? comment['user']['full_name'][0] + .toUpperCase() + : '?', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Text( + comment['user']['full_name'] ?? 'Unknown', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + const Spacer(), + Text( + _formatDate(comment['created_at']), + style: TextStyle( + color: Colors.grey.shade500, + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + comment['content'] ?? '', + style: TextStyle( + color: Colors.grey.shade300, + fontSize: 14, + ), + ), + ], + ), + ), + ); + }), + + // Add extra space at the bottom for the comment input + const SizedBox(height: 80), + ], + ), + ), + + // Comment input + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _commentController, + decoration: InputDecoration( + hintText: 'Add a comment...', + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface), + maxLines: 3, + minLines: 1, + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: _addComment, + icon: const Icon(Icons.send_rounded), + color: Colors.green.shade400, + iconSize: 28, + ), + ], + ), + ), + ], + ), + ); + } + + String _formatDate(String? dateString) { + if (dateString == null) return ''; + + try { + final date = DateTime.parse(dateString); + return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; + } catch (e) { + return ''; + } + } +} diff --git a/lib/screens/tickets/ticket_screen.dart b/lib/screens/tickets/ticket_screen.dart index 491105f..33cb38b 100644 --- a/lib/screens/tickets/ticket_screen.dart +++ b/lib/screens/tickets/ticket_screen.dart @@ -1,965 +1,986 @@ -import 'package:flutter/material.dart'; -import '../../services/supabase_service.dart'; -import '../../widgets/custom_widgets.dart'; -import 'ticket_detail_screen.dart'; -import 'create_ticket_screen.dart'; - -class TicketScreen extends StatefulWidget { - static final GlobalKey<_TicketScreenState> globalKey = - GlobalKey<_TicketScreenState>(); - - const TicketScreen({super.key}); - - // Static method to refresh tickets from anywhere - static void refreshTickets() { - final state = globalKey.currentState; - if (state != null) { - state.refreshTickets(); - } - } - - @override - State createState() => _TicketScreenState(); -} - -class _TicketScreenState extends State { - final _supabaseService = SupabaseService(); - bool _isLoading = true; - String _selectedStatus = 'open'; - bool _isAdmin = false; - List> _tickets = []; - - @override - void initState() { - super.initState(); - _loadInitialData(); - } - - // Method to refresh tickets - void refreshTickets() { - _loadInitialData(); - } - - Future _loadInitialData() async { - setState(() { - _isLoading = true; - }); - - try { - // Check if user is admin - final userProfile = await _supabaseService.getCurrentUserProfile(); - if (mounted) { - setState(() { - _isAdmin = userProfile?['role'] == 'admin'; - }); - } - - // Load team members first - if (userProfile != null && userProfile['team_id'] != null) { - await _supabaseService.loadTeamMembers(userProfile['team_id']); - } - - // Initial load of tickets with filtering - final tickets = - await _supabaseService.getTickets(filterByAssignment: true); - - if (mounted) { - setState(() { - _tickets = tickets; - _isLoading = false; - }); - } - } catch (e) { - debugPrint('Error loading initial data: $e'); - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - Future _updateTicketStatus(String ticketId, String status) async { - try { - await _supabaseService.updateTicketStatus( - ticketId: ticketId, - status: status, - ); - - // Reload tickets after update - _loadInitialData(); - } catch (e) { - debugPrint('Error updating ticket status: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error updating ticket status: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - Future _updateTicketApproval( - String ticketId, String approvalStatus) async { - try { - await _supabaseService.updateTicketApproval( - ticketId: ticketId, - approvalStatus: approvalStatus, - ); - - // Reload tickets after update - _loadInitialData(); - } catch (e) { - debugPrint('Error updating ticket approval: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error updating ticket approval: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return const Scaffold( - backgroundColor: Color(0xFF1A1A1A), - body: Center(child: CustomLoading()), - ); - } - - // Make sure the key is properly associated with this instance - if (TicketScreen.globalKey.currentState != this) { - WidgetsBinding.instance.addPostFrameCallback((_) { - TicketScreen.refreshTickets(); - }); - } - - final openTickets = - _tickets.where((ticket) => ticket['status'] == 'open').toList(); - final inProgressTickets = - _tickets.where((ticket) => ticket['status'] == 'in_progress').toList(); - final resolvedTickets = - _tickets.where((ticket) => ticket['status'] == 'resolved').toList(); - final totalTickets = _tickets.length; - - return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), - floatingActionButton: FloatingActionButton( - onPressed: () async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const CreateTicketScreen(), - fullscreenDialog: true, - ), - ); - - if (result == true) { - refreshTickets(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Ticket created successfully'), - backgroundColor: Colors.green, - ), - ); - } - }, - backgroundColor: Colors.green.shade400, - child: const Icon(Icons.add, color: Colors.white), - ), - body: Column( - children: [ - _buildProgressHeader( - openTickets: openTickets.length, - inProgressTickets: inProgressTickets.length, - resolvedTickets: resolvedTickets.length, - totalTickets: totalTickets, - ), - const SizedBox(height: 16), - _buildStatusTabs(), - Expanded( - child: _buildTicketList(_tickets - .where((ticket) => ticket['status'] == _selectedStatus) - .toList()), - ), - ], - ), - ); - } - - Widget _buildProgressHeader({ - required int openTickets, - required int inProgressTickets, - required int resolvedTickets, - required int totalTickets, - }) { - return Container( - padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Color(0xFF2D2D2D), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20), - ), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildProgressStat( - label: 'Open', - value: openTickets, - total: totalTickets > 0 ? totalTickets : 1, - color: Colors.blue.shade400, - ), - _buildProgressStat( - label: 'In Progress', - value: inProgressTickets, - total: totalTickets > 0 ? totalTickets : 1, - color: Colors.orange.shade400, - ), - _buildProgressStat( - label: 'Resolved', - value: resolvedTickets, - total: totalTickets > 0 ? totalTickets : 1, - color: Colors.green.shade400, - ), - ], - ), - const SizedBox(height: 16), - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Stack( - children: [ - Container(height: 8, color: Colors.grey.shade800), - Row( - children: [ - _buildProgressBar( - width: totalTickets > 0 ? openTickets / totalTickets : 0, - color: Colors.blue.shade400, - ), - _buildProgressBar( - width: totalTickets > 0 - ? inProgressTickets / totalTickets - : 0, - color: Colors.orange.shade400, - ), - _buildProgressBar( - width: - totalTickets > 0 ? resolvedTickets / totalTickets : 0, - color: Colors.green.shade400, - ), - ], - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildProgressStat({ - required String label, - required int value, - required int total, - required Color color, - }) { - final percentage = (value / total * 100).round(); - return Column( - children: [ - Text( - '$percentage%', - style: TextStyle( - color: color, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - label, - style: const TextStyle(color: Colors.white70, fontSize: 12), - ), - ], - ); - } - - Widget _buildProgressBar({required double width, required Color color}) { - final screenWidth = - MediaQuery.of(context).size.width - 32; // Total available width - return Container( - height: 8, - width: screenWidth * width, - color: color, - ); - } - - Widget _buildStatusTabs() { - final statusOptions = [ - {'id': 'open', 'label': 'Open', 'color': Colors.blue}, - {'id': 'in_progress', 'label': 'In Progress', 'color': Colors.orange}, - {'id': 'resolved', 'label': 'Resolved', 'color': Colors.green}, - ]; - - return Container( - height: 40, - margin: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: statusOptions.map((status) { - final isSelected = status['id'] == _selectedStatus; - final color = status['color'] as MaterialColor; - - return Expanded( - child: DragTarget>( - builder: (context, candidateData, rejectedData) { - return Container( - decoration: BoxDecoration( - color: isSelected - ? color.withOpacity(0.2) - : candidateData.isNotEmpty - ? color.withOpacity(0.1) - : Colors.transparent, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: isSelected ? color : Colors.transparent, - width: 2, - ), - ), - child: InkWell( - onTap: () => setState( - () => _selectedStatus = status['id'] as String), - borderRadius: BorderRadius.circular(20), - child: Center( - child: Text( - status['label'] as String, - style: TextStyle( - color: isSelected ? color : Colors.white70, - fontWeight: - isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - ), - ); - }, - onAccept: (ticket) { - final newStatus = status['id'] as String; - if (ticket['status'] != newStatus) { - _updateTicketStatus(ticket['id'], newStatus); - } - }, - onWillAccept: (data) => data != null, - ), - ); - }).toList(), - ), - ); - } - - Widget _buildTicketList(List> filteredTickets) { - if (filteredTickets.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - _selectedStatus == 'open' - ? Icons.report_problem_outlined - : _selectedStatus == 'in_progress' - ? Icons.pending_actions_outlined - : Icons.task_alt_outlined, - size: 80, - color: Colors.grey.shade600, - ), - const SizedBox(height: 16), - Text( - 'No tickets found', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey.shade400, - ), - ), - const SizedBox(height: 8), - Text( - _selectedStatus == 'open' - ? 'Create new tickets to get started' - : _selectedStatus == 'in_progress' - ? 'Move tickets here when you start working on them' - : 'Resolved tickets will appear here', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade600, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - _buildStatusChangeHint(), - ], - ), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: ReorderableListView.builder( - itemCount: filteredTickets.length, - onReorder: (oldIndex, newIndex) { - // Just for visual reordering, no status change - setState(() { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final item = filteredTickets.removeAt(oldIndex); - filteredTickets.insert(newIndex, item); - }); - }, - itemBuilder: (context, index) { - final ticket = filteredTickets[index]; - return Draggable>( - key: ValueKey(ticket['id']), - data: ticket, - feedback: Material( - color: Colors.transparent, - child: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: _TicketCard( - ticket: ticket, - isAdmin: _isAdmin, - onStatusChange: _updateTicketStatus, - onApprovalChange: _updateTicketApproval, - onTap: () {}, - ), - ), - ), - childWhenDragging: Opacity( - opacity: 0.5, - child: _TicketCard( - ticket: ticket, - isAdmin: _isAdmin, - onStatusChange: _updateTicketStatus, - onApprovalChange: _updateTicketApproval, - onTap: () {}, - ), - ), - child: _TicketCard( - ticket: ticket, - isAdmin: _isAdmin, - onStatusChange: _updateTicketStatus, - onApprovalChange: _updateTicketApproval, - onTap: () async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - TicketDetailScreen(ticketId: ticket['id']), - ), - ); - - if (result == true) { - // Ticket was updated in detail screen, refresh tickets - _loadInitialData(); - } - }, - ), - ); - }, - ), - ); - } - - Widget _buildStatusChangeHint() { - final nextStatus = _selectedStatus == 'open' - ? 'In Progress' - : _selectedStatus == 'in_progress' - ? 'Resolved' - : 'Open'; - - final nextStatusId = _selectedStatus == 'open' - ? 'in_progress' - : _selectedStatus == 'in_progress' - ? 'resolved' - : 'open'; - - final color = _selectedStatus == 'open' - ? Colors.orange - : _selectedStatus == 'in_progress' - ? Colors.green - : Colors.blue; - - return ElevatedButton.icon( - onPressed: () => setState(() => _selectedStatus = nextStatusId), - icon: Icon( - _selectedStatus == 'open' - ? Icons.arrow_forward - : _selectedStatus == 'in_progress' - ? Icons.check - : Icons.refresh, - color: Colors.white, - size: 16, - ), - label: Text( - 'View $nextStatus Tickets', - style: const TextStyle(color: Colors.white), - ), - style: ElevatedButton.styleFrom( - backgroundColor: color, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ); - } -} - -class _TicketCard extends StatelessWidget { - final Map ticket; - final bool isAdmin; - final Function(String, String) onStatusChange; - final Function(String, String) onApprovalChange; - final VoidCallback onTap; - - const _TicketCard({ - required this.ticket, - required this.isAdmin, - required this.onStatusChange, - required this.onApprovalChange, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final String title = ticket['title'] ?? 'Untitled Ticket'; - final String description = ticket['description'] ?? 'No description'; - // Limit description to 75 words - final String limitedDescription = _limitWords(description, 75); - final String status = ticket['status'] ?? 'open'; - final String priority = ticket['priority'] ?? 'medium'; - final String category = ticket['category'] ?? 'Bug'; - final String approvalStatus = ticket['approval_status'] ?? 'pending'; - final String ticketNumber = ticket['ticket_number'] ?? 'TKT-???'; - final String createdAt = _formatDate(ticket['created_at']); - - // Get names from the team members cache - final supabaseService = SupabaseService(); - String creatorName; - if (ticket['created_by'] != null) { - if (supabaseService.isCurrentUser(ticket['created_by'])) { - creatorName = 'You'; - } else if (ticket['creator'] != null && - ticket['creator']['full_name'] != null) { - creatorName = ticket['creator']['full_name']; - } else { - creatorName = supabaseService.getUserNameById(ticket['created_by']); - } - } else { - creatorName = 'Unknown'; - } - - // Determine colors and icons based on priority - Color priorityColor; - IconData priorityIcon; - switch (priority.toLowerCase()) { - case 'high': - priorityColor = Colors.red.shade400; - priorityIcon = Icons.priority_high; - break; - case 'medium': - priorityColor = Colors.orange.shade400; - priorityIcon = Icons.remove_circle_outline; - break; - case 'low': - priorityColor = Colors.green.shade400; - priorityIcon = Icons.arrow_downward; - break; - default: - priorityColor = Colors.grey; - priorityIcon = Icons.help_outline; - } - - // Determine category icon - IconData categoryIcon; - switch (category.toLowerCase()) { - case 'bug': - categoryIcon = Icons.bug_report; - break; - case 'feature request': - categoryIcon = Icons.lightbulb_outline; - break; - case 'ui/ux': - categoryIcon = Icons.design_services; - break; - case 'performance': - categoryIcon = Icons.speed; - break; - case 'documentation': - categoryIcon = Icons.description; - break; - case 'security': - categoryIcon = Icons.security; - break; - default: - categoryIcon = Icons.category; - } - - // Determine colors based on approval status - final Color approvalColor = approvalStatus == 'pending' - ? Colors.grey - : approvalStatus == 'approved' - ? Colors.green - : Colors.red; - - return GestureDetector( - onTap: onTap, - child: Container( - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: const Color(0xFF2D2D2D), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: priorityColor.withOpacity(0.1), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - priorityIcon, - color: priorityColor, - size: 20, - ), - const SizedBox(width: 8), - Text( - priority.toUpperCase(), - style: TextStyle( - color: priorityColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - Row( - children: [ - if (isAdmin && approvalStatus == 'pending') - Row( - children: [ - IconButton( - onPressed: () => - onApprovalChange(ticket['id'], 'approved'), - icon: Icon(Icons.check_circle, - color: Colors.green.shade400, size: 20), - tooltip: 'Approve', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: 8), - IconButton( - onPressed: () => - onApprovalChange(ticket['id'], 'rejected'), - icon: Icon(Icons.cancel, - color: Colors.red.shade400, size: 20), - tooltip: 'Reject', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - const SizedBox(width: 12), - ], - ), - Row( - children: [ - if (ticket['created_by'] == - SupabaseService() - .client - .auth - .currentUser - ?.id || - isAdmin) - IconButton( - onPressed: () async { - final confirm = await showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF2D2D2D), - title: const Text('Delete Ticket', - style: TextStyle(color: Colors.white)), - content: const Text( - 'Are you sure you want to delete this ticket? This action cannot be undone.', - style: TextStyle(color: Colors.white70), - ), - actions: [ - TextButton( - onPressed: () => - Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => - Navigator.pop(context, true), - style: TextButton.styleFrom( - foregroundColor: Colors.red), - child: const Text('Delete'), - ), - ], - ), - ); - - if (confirm == true) { - try { - final result = await SupabaseService() - .deleteTicket(ticket['id']); - if (result['success'] == true) { - if (context.mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text( - 'Ticket deleted successfully'), - backgroundColor: Colors.green, - ), - ); - // Refresh the ticket list - TicketScreen.refreshTickets(); - } - } else { - if (context.mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text(result['error'] ?? - 'Failed to delete ticket'), - backgroundColor: Colors.red, - ), - ); - } - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text('Error: $e'), - backgroundColor: Colors.red, - ), - ); - } - } - } - }, - icon: Icon(Icons.delete_outline, - color: Colors.red.shade400, size: 20), - tooltip: 'Delete', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - if (ticket['created_by'] == - SupabaseService() - .client - .auth - .currentUser - ?.id || - isAdmin) - const SizedBox(width: 8), - Text( - createdAt, - style: TextStyle( - color: Colors.grey.shade400, fontSize: 12), - ), - ], - ), - ], - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ticketNumber, - style: TextStyle( - color: Colors.grey.shade500, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - title, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: approvalColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - approvalStatus.toUpperCase(), - style: TextStyle( - color: approvalColor, - fontWeight: FontWeight.bold, - fontSize: 10, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - limitedDescription, - style: TextStyle(color: Colors.grey.shade400, fontSize: 14), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.purple.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Icon( - categoryIcon, - color: Colors.purple.shade300, - size: 14, - ), - const SizedBox(width: 4), - Text( - category, - style: TextStyle( - color: Colors.purple.shade300, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - Row( - children: [ - CircleAvatar( - radius: 12, - backgroundColor: Colors.blue.shade700, - child: Text( - creatorName.isNotEmpty - ? creatorName[0].toUpperCase() - : '?', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 8), - Text( - 'Created by $creatorName', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 12, - ), - ), - ], - ), - ], - ), - ], - ), - ), - // Drag handle indicator - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.drag_indicator, - color: Colors.grey.shade500, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Drag to change status', - style: TextStyle( - color: Colors.grey.shade500, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - String _formatDate(String? dateString) { - if (dateString == null) return ''; - - try { - final date = DateTime.parse(dateString); - return '${date.day}/${date.month}/${date.year}'; - } catch (e) { - return ''; - } - } - - String _limitWords(String text, int wordLimit) { - if (text.isEmpty) return text; - - final words = text.split(' '); - if (words.length <= wordLimit) return text; - - return '${words.take(wordLimit).join(' ')}...'; - } -} +import 'package:flutter/material.dart'; +import '../../services/supabase_service.dart'; +import '../../widgets/custom_widgets.dart'; +import 'ticket_detail_screen.dart'; +import 'create_ticket_screen.dart'; + +class TicketScreen extends StatefulWidget { + static final GlobalKey<_TicketScreenState> globalKey = + GlobalKey<_TicketScreenState>(); + + const TicketScreen({super.key}); + + // Static method to refresh tickets from anywhere + static void refreshTickets() { + final state = globalKey.currentState; + if (state != null) { + state.refreshTickets(); + } + } + + @override + State createState() => _TicketScreenState(); +} + +class _TicketScreenState extends State { + final _supabaseService = SupabaseService(); + bool _isLoading = true; + String _selectedStatus = 'open'; + bool _isAdmin = false; + List> _tickets = []; + + @override + void initState() { + super.initState(); + _loadInitialData(); + } + + // Method to refresh tickets + void refreshTickets() { + _loadInitialData(); + } + + Future _loadInitialData() async { + setState(() { + _isLoading = true; + }); + + try { + // Check if user is admin + final userProfile = await _supabaseService.getCurrentUserProfile(); + if (mounted) { + setState(() { + _isAdmin = userProfile?['role'] == 'admin'; + }); + } + + // Load team members first + if (userProfile != null && userProfile['team_id'] != null) { + await _supabaseService.loadTeamMembers(userProfile['team_id']); + } + + // Initial load of tickets with filtering + final tickets = + await _supabaseService.getTickets(filterByAssignment: true); + + if (mounted) { + setState(() { + _tickets = tickets; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Error loading initial data: $e'); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _updateTicketStatus(String ticketId, String status) async { + try { + await _supabaseService.updateTicketStatus( + ticketId: ticketId, + status: status, + ); + + // Reload tickets after update + _loadInitialData(); + } catch (e) { + debugPrint('Error updating ticket status: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating ticket status: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _updateTicketApproval( + String ticketId, String approvalStatus) async { + try { + await _supabaseService.updateTicketApproval( + ticketId: ticketId, + approvalStatus: approvalStatus, + ); + + // Reload tickets after update + _loadInitialData(); + } catch (e) { + debugPrint('Error updating ticket approval: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating ticket approval: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: const Center(child: CustomLoading()), + ); + } + + // Make sure the key is properly associated with this instance + if (TicketScreen.globalKey.currentState != this) { + WidgetsBinding.instance.addPostFrameCallback((_) { + TicketScreen.refreshTickets(); + }); + } + + final openTickets = + _tickets.where((ticket) => ticket['status'] == 'open').toList(); + final inProgressTickets = + _tickets.where((ticket) => ticket['status'] == 'in_progress').toList(); + final resolvedTickets = + _tickets.where((ticket) => ticket['status'] == 'resolved').toList(); + final totalTickets = _tickets.length; + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + floatingActionButton: FloatingActionButton( + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CreateTicketScreen(), + fullscreenDialog: true, + ), + ); + + if (result == true) { + refreshTickets(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Ticket created successfully'), + backgroundColor: Colors.green, + ), + ); + } + }, + backgroundColor: Colors.green.shade400, + child: const Icon(Icons.add, color: Colors.white), + ), + body: Column( + children: [ + _buildProgressHeader( + openTickets: openTickets.length, + inProgressTickets: inProgressTickets.length, + resolvedTickets: resolvedTickets.length, + totalTickets: totalTickets, + ), + const SizedBox(height: 16), + _buildStatusTabs(), + Expanded( + child: _buildTicketList(_tickets + .where((ticket) => ticket['status'] == _selectedStatus) + .toList()), + ), + ], + ), + ); + } + + Widget _buildProgressHeader({ + required int openTickets, + required int inProgressTickets, + required int resolvedTickets, + required int totalTickets, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildProgressStat( + label: 'Open', + value: openTickets, + total: totalTickets > 0 ? totalTickets : 1, + color: Colors.blue.shade400, + ), + _buildProgressStat( + label: 'In Progress', + value: inProgressTickets, + total: totalTickets > 0 ? totalTickets : 1, + color: Colors.orange.shade400, + ), + _buildProgressStat( + label: 'Resolved', + value: resolvedTickets, + total: totalTickets > 0 ? totalTickets : 1, + color: Colors.green.shade400, + ), + ], + ), + const SizedBox(height: 16), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Stack( + children: [ + Container( + height: 8, + color: + Theme.of(context).colorScheme.surfaceContainerHighest), + Row( + children: [ + _buildProgressBar( + width: totalTickets > 0 ? openTickets / totalTickets : 0, + color: Colors.blue.shade400, + ), + _buildProgressBar( + width: totalTickets > 0 + ? inProgressTickets / totalTickets + : 0, + color: Colors.orange.shade400, + ), + _buildProgressBar( + width: + totalTickets > 0 ? resolvedTickets / totalTickets : 0, + color: Colors.green.shade400, + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildProgressStat({ + required String label, + required int value, + required int total, + required Color color, + }) { + final percentage = (value / total * 100).round(); + return Column( + children: [ + Text( + '$percentage%', + style: TextStyle( + color: color, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12), + ), + ], + ); + } + + Widget _buildProgressBar({required double width, required Color color}) { + final screenWidth = + MediaQuery.of(context).size.width - 32; // Total available width + return Container( + height: 8, + width: screenWidth * width, + color: color, + ); + } + + Widget _buildStatusTabs() { + final statusOptions = [ + {'id': 'open', 'label': 'Open', 'color': Colors.blue}, + {'id': 'in_progress', 'label': 'In Progress', 'color': Colors.orange}, + {'id': 'resolved', 'label': 'Resolved', 'color': Colors.green}, + ]; + + return Container( + height: 40, + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: statusOptions.map((status) { + final isSelected = status['id'] == _selectedStatus; + final color = status['color'] as MaterialColor; + + return Expanded( + child: DragTarget>( + builder: (context, candidateData, rejectedData) { + return Container( + decoration: BoxDecoration( + color: isSelected + ? color.withOpacity(0.2) + : candidateData.isNotEmpty + ? color.withOpacity(0.1) + : Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? color : Colors.transparent, + width: 2, + ), + ), + child: InkWell( + onTap: () => setState( + () => _selectedStatus = status['id'] as String), + borderRadius: BorderRadius.circular(20), + child: Center( + child: Text( + status['label'] as String, + style: TextStyle( + color: isSelected + ? color + : Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ), + ); + }, + onAccept: (ticket) { + final newStatus = status['id'] as String; + if (ticket['status'] != newStatus) { + _updateTicketStatus(ticket['id'], newStatus); + } + }, + onWillAccept: (data) => data != null, + ), + ); + }).toList(), + ), + ); + } + + Widget _buildTicketList(List> filteredTickets) { + if (filteredTickets.isEmpty) { + final colorScheme = Theme.of(context).colorScheme; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _selectedStatus == 'open' + ? Icons.report_problem_outlined + : _selectedStatus == 'in_progress' + ? Icons.pending_actions_outlined + : Icons.task_alt_outlined, + size: 80, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + 'No tickets found', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + _selectedStatus == 'open' + ? 'Create new tickets to get started' + : _selectedStatus == 'in_progress' + ? 'Move tickets here when you start working on them' + : 'Resolved tickets will appear here', + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + _buildStatusChangeHint(), + ], + ), + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ReorderableListView.builder( + itemCount: filteredTickets.length, + onReorder: (oldIndex, newIndex) { + // Just for visual reordering, no status change + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final item = filteredTickets.removeAt(oldIndex); + filteredTickets.insert(newIndex, item); + }); + }, + itemBuilder: (context, index) { + final ticket = filteredTickets[index]; + return Draggable>( + key: ValueKey(ticket['id']), + data: ticket, + feedback: Material( + color: Colors.transparent, + child: SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: _TicketCard( + ticket: ticket, + isAdmin: _isAdmin, + onStatusChange: _updateTicketStatus, + onApprovalChange: _updateTicketApproval, + onTap: () {}, + ), + ), + ), + childWhenDragging: Opacity( + opacity: 0.5, + child: _TicketCard( + ticket: ticket, + isAdmin: _isAdmin, + onStatusChange: _updateTicketStatus, + onApprovalChange: _updateTicketApproval, + onTap: () {}, + ), + ), + child: _TicketCard( + ticket: ticket, + isAdmin: _isAdmin, + onStatusChange: _updateTicketStatus, + onApprovalChange: _updateTicketApproval, + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TicketDetailScreen(ticketId: ticket['id']), + ), + ); + + if (result == true) { + // Ticket was updated in detail screen, refresh tickets + _loadInitialData(); + } + }, + ), + ); + }, + ), + ); + } + + Widget _buildStatusChangeHint() { + final nextStatus = _selectedStatus == 'open' + ? 'In Progress' + : _selectedStatus == 'in_progress' + ? 'Resolved' + : 'Open'; + + final nextStatusId = _selectedStatus == 'open' + ? 'in_progress' + : _selectedStatus == 'in_progress' + ? 'resolved' + : 'open'; + + final color = _selectedStatus == 'open' + ? Colors.orange + : _selectedStatus == 'in_progress' + ? Colors.green + : Colors.blue; + + return ElevatedButton.icon( + onPressed: () => setState(() => _selectedStatus = nextStatusId), + icon: Icon( + _selectedStatus == 'open' + ? Icons.arrow_forward + : _selectedStatus == 'in_progress' + ? Icons.check + : Icons.refresh, + color: Colors.white, + size: 16, + ), + label: Text( + 'View $nextStatus Tickets', + style: const TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: color, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + ); + } +} + +class _TicketCard extends StatelessWidget { + final Map ticket; + final bool isAdmin; + final Function(String, String) onStatusChange; + final Function(String, String) onApprovalChange; + final VoidCallback onTap; + + const _TicketCard({ + required this.ticket, + required this.isAdmin, + required this.onStatusChange, + required this.onApprovalChange, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final String title = ticket['title'] ?? 'Untitled Ticket'; + final String description = ticket['description'] ?? 'No description'; + // Limit description to 75 words + final String limitedDescription = _limitWords(description, 75); + final String status = ticket['status'] ?? 'open'; + final String priority = ticket['priority'] ?? 'medium'; + final String category = ticket['category'] ?? 'Bug'; + final String approvalStatus = ticket['approval_status'] ?? 'pending'; + final String ticketNumber = ticket['ticket_number'] ?? 'TKT-???'; + final String createdAt = _formatDate(ticket['created_at']); + + // Get names from the team members cache + final supabaseService = SupabaseService(); + String creatorName; + if (ticket['created_by'] != null) { + if (supabaseService.isCurrentUser(ticket['created_by'])) { + creatorName = 'You'; + } else if (ticket['creator'] != null && + ticket['creator']['full_name'] != null) { + creatorName = ticket['creator']['full_name']; + } else { + creatorName = supabaseService.getUserNameById(ticket['created_by']); + } + } else { + creatorName = 'Unknown'; + } + + // Determine colors and icons based on priority + Color priorityColor; + IconData priorityIcon; + switch (priority.toLowerCase()) { + case 'high': + priorityColor = Colors.red.shade400; + priorityIcon = Icons.priority_high; + break; + case 'medium': + priorityColor = Colors.orange.shade400; + priorityIcon = Icons.remove_circle_outline; + break; + case 'low': + priorityColor = Colors.green.shade400; + priorityIcon = Icons.arrow_downward; + break; + default: + priorityColor = Colors.grey; + priorityIcon = Icons.help_outline; + } + + // Determine category icon + IconData categoryIcon; + switch (category.toLowerCase()) { + case 'bug': + categoryIcon = Icons.bug_report; + break; + case 'feature request': + categoryIcon = Icons.lightbulb_outline; + break; + case 'ui/ux': + categoryIcon = Icons.design_services; + break; + case 'performance': + categoryIcon = Icons.speed; + break; + case 'documentation': + categoryIcon = Icons.description; + break; + case 'security': + categoryIcon = Icons.security; + break; + default: + categoryIcon = Icons.category; + } + + // Determine colors based on approval status + final Color approvalColor = approvalStatus == 'pending' + ? Colors.grey + : approvalStatus == 'approved' + ? Colors.green + : Colors.red; + + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: priorityColor.withOpacity(0.1), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + priorityIcon, + color: priorityColor, + size: 20, + ), + const SizedBox(width: 8), + Text( + priority.toUpperCase(), + style: TextStyle( + color: priorityColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Row( + children: [ + if (isAdmin && approvalStatus == 'pending') + Row( + children: [ + IconButton( + onPressed: () => + onApprovalChange(ticket['id'], 'approved'), + icon: Icon(Icons.check_circle, + color: Colors.green.shade400, size: 20), + tooltip: 'Approve', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: 8), + IconButton( + onPressed: () => + onApprovalChange(ticket['id'], 'rejected'), + icon: Icon(Icons.cancel, + color: Colors.red.shade400, size: 20), + tooltip: 'Reject', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: 12), + ], + ), + Row( + children: [ + if (ticket['created_by'] == + SupabaseService() + .client + .auth + .currentUser + ?.id || + isAdmin) + IconButton( + onPressed: () async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: + Theme.of(context).colorScheme.surface, + title: Text('Delete Ticket', + style: Theme.of(context) + .textTheme + .titleLarge), + content: Text( + 'Are you sure you want to delete this ticket? This action cannot be undone.', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), + ), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => + Navigator.pop(context, true), + style: TextButton.styleFrom( + foregroundColor: Colors.red), + child: const Text('Delete'), + ), + ], + ), + ); + + if (confirm == true) { + try { + final result = await SupabaseService() + .deleteTicket(ticket['id']); + if (result['success'] == true) { + if (context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Ticket deleted successfully'), + backgroundColor: Colors.green, + ), + ); + // Refresh the ticket list + TicketScreen.refreshTickets(); + } + } else { + if (context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text(result['error'] ?? + 'Failed to delete ticket'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + }, + icon: Icon(Icons.delete_outline, + color: Colors.red.shade400, size: 20), + tooltip: 'Delete', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + if (ticket['created_by'] == + SupabaseService() + .client + .auth + .currentUser + ?.id || + isAdmin) + const SizedBox(width: 8), + Text( + createdAt, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 12), + ), + ], + ), + ], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ticketNumber, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + title, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: approvalColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + approvalStatus.toUpperCase(), + style: TextStyle( + color: approvalColor, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + limitedDescription, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + categoryIcon, + color: Colors.purple.shade300, + size: 14, + ), + const SizedBox(width: 4), + Text( + category, + style: TextStyle( + color: Colors.purple.shade300, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Row( + children: [ + CircleAvatar( + radius: 12, + backgroundColor: Colors.blue.shade700, + child: Text( + creatorName.isNotEmpty + ? creatorName[0].toUpperCase() + : '?', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Text( + 'Created by $creatorName', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ], + ), + ), + // Drag handle indicator + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.drag_indicator, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Drag to change status', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + String _formatDate(String? dateString) { + if (dateString == null) return ''; + + try { + final date = DateTime.parse(dateString); + return '${date.day}/${date.month}/${date.year}'; + } catch (e) { + return ''; + } + } + + String _limitWords(String text, int wordLimit) { + if (text.isEmpty) return text; + + final words = text.split(' '); + if (words.length <= wordLimit) return text; + + return '${words.take(wordLimit).join(' ')}...'; + } +} diff --git a/lib/screens/workspace/workspace_screen.dart b/lib/screens/workspace/workspace_screen.dart index af2e0c6..adfd44b 100644 --- a/lib/screens/workspace/workspace_screen.dart +++ b/lib/screens/workspace/workspace_screen.dart @@ -145,20 +145,20 @@ class _WorkspaceScreenState extends State @override Widget build(BuildContext context) { if (_isLoading) { - return const Scaffold( - backgroundColor: Color(0xFF1A1A1A), - body: WorkspaceLoadingSkeleton(), + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: const WorkspaceLoadingSkeleton(), ); } final statusBarHeight = MediaQuery.of(context).padding.top; return Scaffold( - backgroundColor: const Color(0xFF1A1A1A), + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight + statusBarHeight), child: Container( - color: const Color(0xFF2D2D2D), + color: Theme.of(context).colorScheme.surface, child: SafeArea( top: true, bottom: false, @@ -168,7 +168,8 @@ class _WorkspaceScreenState extends State controller: _tabController, indicatorColor: Colors.green, labelColor: Colors.green, - unselectedLabelColor: Colors.white70, + unselectedLabelColor: + Theme.of(context).colorScheme.onSurfaceVariant, tabs: const [ Tab(icon: Icon(Icons.task), text: 'Tasks'), Tab(icon: Icon(Icons.confirmation_number), text: 'Tickets'), diff --git a/lib/theme/app_theme_mode.dart b/lib/theme/app_theme_mode.dart new file mode 100644 index 0000000..2573eb9 --- /dev/null +++ b/lib/theme/app_theme_mode.dart @@ -0,0 +1,6 @@ +/// Supported app theme modes. +enum AppThemeMode { + light, + dark, + system, +} diff --git a/lib/theme/app_themes.dart b/lib/theme/app_themes.dart new file mode 100644 index 0000000..959dd5d --- /dev/null +++ b/lib/theme/app_themes.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; + +// Light theme: strong contrast, white surface, dark text, visible cards & inputs. +ThemeData get lightTheme { + const surface = Color(0xFFFFFFFF); + const onSurface = Color(0xFF1C1C1C); + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + primaryColor: Colors.green.shade400, + scaffoldBackgroundColor: const Color(0xFFF0F0F0), + appBarTheme: const AppBarTheme( + backgroundColor: surface, + foregroundColor: onSurface, + iconTheme: IconThemeData(color: onSurface), + titleTextStyle: TextStyle( + color: onSurface, fontSize: 20, fontWeight: FontWeight.w600), + ), + iconTheme: const IconThemeData(color: onSurface), + colorScheme: ColorScheme.light( + primary: Colors.green.shade400, + secondary: Colors.green.shade700, + surface: const Color(0xFFFFFFFF), + onSurface: const Color(0xFF1C1C1C), + onSurfaceVariant: const Color(0xFF5C5C5C), + outline: const Color(0xFFE0E0E0), + outlineVariant: const Color(0xFFEEEEEE), + surfaceContainerLowest: const Color(0xFFFFFFFF), + surfaceContainerLow: const Color(0xFFF8F8F8), + surfaceContainer: const Color(0xFFF2F2F2), + surfaceContainerHigh: const Color(0xFFECECEC), + surfaceContainerHighest: const Color(0xFFE6E6E6), + onInverseSurface: const Color(0xFFF0F0F0), + shadow: const Color(0xFF000000), + scrim: const Color(0xFF000000), + inverseSurface: const Color(0xFF303030), + onPrimary: Colors.white, + onSecondary: Colors.white, + ), + dividerColor: const Color(0xFFBDBDBD), + dividerTheme: const DividerThemeData( + color: Color(0xFFBDBDBD), + thickness: 1, + space: 1, + ), + cardTheme: CardThemeData( + elevation: 2, + shadowColor: const Color(0xFF000000), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + color: const Color(0xFFFFFFFF), + surfaceTintColor: Colors.transparent, + margin: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFFF5F5F5), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFE0E0E0)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.green.shade400, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFB00020)), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + hintStyle: const TextStyle(color: Color(0xFF9E9E9E)), + labelStyle: const TextStyle(color: Color(0xFF5C5C5C)), + ), + textTheme: const TextTheme( + displayLarge: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + color: Color(0xFF1C1C1C), + ), + displayMedium: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Color(0xFF1C1C1C), + ), + displaySmall: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFF1C1C1C), + ), + headlineLarge: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Color(0xFF1C1C1C), + ), + headlineMedium: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF1C1C1C), + ), + headlineSmall: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF1C1C1C), + ), + titleLarge: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF1C1C1C), + ), + titleMedium: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF1C1C1C), + ), + titleSmall: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF1C1C1C), + ), + bodyLarge: + TextStyle(fontSize: 16, letterSpacing: 0.5, color: Color(0xFF1C1C1C)), + bodyMedium: TextStyle( + fontSize: 14, letterSpacing: 0.25, color: Color(0xFF1C1C1C)), + bodySmall: + TextStyle(fontSize: 12, letterSpacing: 0.4, color: Color(0xFF5C5C5C)), + labelLarge: TextStyle( + fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1C1C1C)), + labelMedium: TextStyle( + fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF5C5C5C)), + labelSmall: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + color: Color(0xFF5C5C5C)), + ), + ); +} + +/// Dark theme: unchanged behavior, add CardTheme and InputDecorationTheme for consistency. +ThemeData get darkTheme { + const onSurfaceDark = Color(0xFFE8E8E8); + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + primaryColor: Colors.green.shade400, + scaffoldBackgroundColor: const Color(0xFF1A1A1A), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF2A2A2A), + foregroundColor: onSurfaceDark, + iconTheme: IconThemeData(color: onSurfaceDark), + titleTextStyle: TextStyle( + color: onSurfaceDark, fontSize: 20, fontWeight: FontWeight.w600), + ), + iconTheme: const IconThemeData(color: onSurfaceDark), + colorScheme: ColorScheme.dark( + primary: Colors.green.shade400, + secondary: Colors.green.shade700, + surface: const Color(0xFF2A2A2A), + onSurface: const Color(0xFFE8E8E8), + onSurfaceVariant: const Color(0xFFB0B0B0), + outline: const Color(0xFF404040), + outlineVariant: const Color(0xFF383838), + surfaceContainerLowest: const Color(0xFF1E1E1E), + surfaceContainerLow: const Color(0xFF242424), + surfaceContainer: const Color(0xFF2A2A2A), + surfaceContainerHigh: const Color(0xFF303030), + surfaceContainerHighest: const Color(0xFF363636), + onPrimary: Colors.white, + onSecondary: Colors.white, + ), + dividerColor: const Color(0xFF404040), + dividerTheme: const DividerThemeData( + color: Color(0xFF404040), + thickness: 1, + space: 1, + ), + cardTheme: CardThemeData( + elevation: 2, + shadowColor: const Color(0xFF000000), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + color: const Color(0xFF2A2A2A), + surfaceTintColor: Colors.transparent, + margin: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFF242424), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFF404040)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.green.shade400, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFCF6679)), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + hintStyle: const TextStyle(color: Color(0xFF9E9E9E)), + labelStyle: const TextStyle(color: Color(0xFFB0B0B0)), + ), + textTheme: const TextTheme( + displayLarge: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + color: Color(0xFFE8E8E8), + ), + displayMedium: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Color(0xFFE8E8E8), + ), + displaySmall: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFFE8E8E8), + ), + headlineLarge: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Color(0xFFE8E8E8), + ), + headlineMedium: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFFE8E8E8), + ), + headlineSmall: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFFE8E8E8), + ), + titleLarge: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFFE8E8E8), + ), + titleMedium: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFFE8E8E8), + ), + titleSmall: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFFE8E8E8), + ), + bodyLarge: + TextStyle(fontSize: 16, letterSpacing: 0.5, color: Color(0xFFE8E8E8)), + bodyMedium: TextStyle( + fontSize: 14, letterSpacing: 0.25, color: Color(0xFFE8E8E8)), + bodySmall: + TextStyle(fontSize: 12, letterSpacing: 0.4, color: Color(0xFFB0B0B0)), + labelLarge: TextStyle( + fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFFE8E8E8)), + labelMedium: TextStyle( + fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFFB0B0B0)), + labelSmall: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + color: Color(0xFFB0B0B0)), + ), + ); +} diff --git a/lib/theme/theme_controller.dart b/lib/theme/theme_controller.dart new file mode 100644 index 0000000..d4e3df5 --- /dev/null +++ b/lib/theme/theme_controller.dart @@ -0,0 +1,65 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'app_theme_mode.dart'; + +const String _themeModeKey = 'app_theme_mode'; + +/// Manages app theme state with persistence via SharedPreferences. +class ThemeController extends ChangeNotifier { + ThemeController(this._prefs) { + _themeMode = _loadThemeMode(); + } + + final SharedPreferences _prefs; + late AppThemeMode _themeMode; + + AppThemeMode get themeMode => _themeMode; + + ThemeMode get flutterThemeMode { + switch (_themeMode) { + case AppThemeMode.light: + return ThemeMode.light; + case AppThemeMode.dark: + return ThemeMode.dark; + case AppThemeMode.system: + return ThemeMode.system; + } + } + + bool get isDarkMode { + switch (_themeMode) { + case AppThemeMode.light: + return false; + case AppThemeMode.dark: + return true; + case AppThemeMode.system: + return WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark; + } + } + + AppThemeMode _loadThemeMode() { + final stored = _prefs.getString(_themeModeKey); + if (stored == null) return AppThemeMode.system; + return AppThemeMode.values.firstWhere( + (m) => m.name == stored, + orElse: () => AppThemeMode.system, + ); + } + + Future setThemeMode(AppThemeMode mode) async { + if (_themeMode == mode) return; + _themeMode = mode; + await _prefs.setString(_themeModeKey, mode.name); + notifyListeners(); + } + + /// Static factory to create and initialize the controller. + static Future create() async { + final prefs = await SharedPreferences.getInstance(); + return ThemeController(prefs); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index bba9397..55b6448 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,9 @@ dependencies: supabase_flutter: ^2.3.4 flutter_dotenv: ^5.2.1 + # State management + provider: ^6.1.2 + # Local storage shared_preferences: ^2.5.3