diff --git a/frontend/lib/api/team_service.dart b/frontend/lib/api/team_service.dart new file mode 100644 index 0000000..6788c62 --- /dev/null +++ b/frontend/lib/api/team_service.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:dev_track_app/models/team_model.dart'; + +class TeamService { + // Mock delay to simulate network calls + final Duration _mockDelay = const Duration(milliseconds: 800); + + // Sample data + final List _mockTeams = [ + Team( + id: 't1', + name: 'Team Alpha', + domain: 'Frontend', + members: [ + TeamMember(id: 'm1', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm2', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm3', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm4', name: 'Nameeeee', year: '2025'), + ], + ), + Team( + id: 't2', + name: 'Team Beta', + domain: 'Backend', + members: [ + TeamMember(id: 'm5', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm6', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm7', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm8', name: 'Nameeeee', year: '2025'), + ], + ), + Team( + id: 't3', + name: 'Team Gamma', + domain: 'Design', + members: [ + TeamMember(id: 'm9', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm10', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm11', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm12', name: 'Nameeeee', year: '2025'), + ], + ), + Team( + id: 't4', + name: 'Team Delta', + domain: 'Mobile', + members: [ + TeamMember(id: 'm13', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm14', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm15', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm16', name: 'Nameeeee', year: '2025'), + ], + ), + Team( + id: 't5', + name: 'Team Epsilon', + domain: 'DevOps', + members: [ + TeamMember(id: 'm17', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm18', name: 'Nameeeee', year: '2025'), + TeamMember(id: 'm19', name: 'Nameeeee', year: '2024'), + TeamMember(id: 'm20', name: 'Nameeeee', year: '2024'), + ], + ), + ]; + + // Methods that would normally make API calls + Future> getTeams() async { + // Simulate API call delay + await Future.delayed(_mockDelay); + + // Return a deep copy of the mock data to avoid reference issues + return _mockTeams + .map((team) => Team( + id: team.id, + name: team.name, + domain: team.domain, + members: team.members + .map((member) => TeamMember( + id: member.id, + name: member.name, + year: member.year, + )) + .toList(), + )) + .toList(); + } + + Future updateTeams(List teams) async { + // Simulate API call delay + await Future.delayed(_mockDelay); + + // In a real app, this would send the updated teams to an API + print('Teams updated successfully!'); + return; + } +} diff --git a/frontend/lib/models/team_model.dart b/frontend/lib/models/team_model.dart new file mode 100644 index 0000000..6909caf --- /dev/null +++ b/frontend/lib/models/team_model.dart @@ -0,0 +1,61 @@ +class Team { + final String id; + final String name; + final String domain; + final List members; + + Team({ + required this.id, + required this.name, + required this.domain, + required this.members, + }); + + factory Team.fromJson(Map json) { + return Team( + id: json['id'], + name: json['name'], + domain: json['domain'], + members: (json['members'] as List) + .map((memberJson) => TeamMember.fromJson(memberJson)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'domain': domain, + 'members': members.map((member) => member.toJson()).toList(), + }; + } +} + +class TeamMember { + final String id; + final String name; + final String year; + + TeamMember({ + required this.id, + required this.name, + required this.year, + }); + + factory TeamMember.fromJson(Map json) { + return TeamMember( + id: json['id'], + name: json['name'], + year: json['year'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'year': year, + }; + } +} diff --git a/frontend/lib/view_models/team_viewmodel.dart b/frontend/lib/view_models/team_viewmodel.dart new file mode 100644 index 0000000..5917b60 --- /dev/null +++ b/frontend/lib/view_models/team_viewmodel.dart @@ -0,0 +1,226 @@ +import 'package:dev_track_app/api/team_service.dart'; +import 'package:dev_track_app/models/team_model.dart'; +import 'package:flutter/material.dart'; + +class MemberDragData { + final TeamMember member; + final String currentTeamId; + + MemberDragData(this.member, this.currentTeamId); +} + +class TeamDashboardViewModel extends ChangeNotifier { + final TeamService _teamService = TeamService(); + + List _teams = []; + List _originalTeams = []; // For tracking changes + bool _isEditMode = false; + bool _isLoading = true; + bool _hasChanges = false; + + // Configuration + final bool saveChangesManually = + true; // Set to false for auto-save after each drag + + // Filter options + String? _selectedDomain; + String? _selectedYear; + List _domains = []; + List _years = []; + + TeamDashboardViewModel() { + _loadTeams(); + } + + // Getters + List get teams => _teams; + List get filteredTeams { + return _teams.where((team) { + // If no filters are selected, show all teams + if (_selectedDomain == null && _selectedYear == null) { + return true; + } + + // Apply domain filter if selected + bool matchesDomain = + _selectedDomain == null || team.domain == _selectedDomain; + + // Apply year filter if selected (this would normally filter team members, but for simplicity we're checking if any member matches) + bool matchesYear = _selectedYear == null || + team.members.any((m) => m.year == _selectedYear); + + return matchesDomain && matchesYear; + }).toList(); + } + + bool get isEditMode => _isEditMode; + bool get isLoading => _isLoading; + bool get hasChanges => _hasChanges; + String? get selectedDomain => _selectedDomain; + String? get selectedYear => _selectedYear; + List get domains => _domains; + List get years => _years; + + // Methods + void toggleEditMode() { + _isEditMode = !_isEditMode; + notifyListeners(); + } + + void setDomain(String domain) { + _selectedDomain = domain; + notifyListeners(); + } + + void setYear(String year) { + _selectedYear = year; + notifyListeners(); + } + + Future _loadTeams() async { + _isLoading = true; + notifyListeners(); + + try { + final result = await _teamService.getTeams(); + _teams = result; + _originalTeams = _deepCopyTeams(result); + + // Extract unique domains and years for filters + _extractFilterOptions(); + + _isLoading = false; + notifyListeners(); + } catch (e) { + _isLoading = false; + notifyListeners(); + // Handle error (in a real app, you might want to show an error message) + debugPrint('Error loading teams: $e'); + } + } + + void _extractFilterOptions() { + final domainSet = {}; + final yearSet = {}; + + for (final team in _teams) { + domainSet.add(team.domain); + + for (final member in team.members) { + yearSet.add(member.year); + } + } + + _domains = domainSet.toList()..sort(); + _years = yearSet.toList()..sort(); + } + + void moveMember(TeamMember member, String fromTeamId, String toTeamId) { + // Find source and destination teams + final fromTeamIndex = _teams.indexWhere((t) => t.id == fromTeamId); + final toTeamIndex = _teams.indexWhere((t) => t.id == toTeamId); + + if (fromTeamIndex == -1 || toTeamIndex == -1) return; + + // Find the member in the source team + final memberIndex = + _teams[fromTeamIndex].members.indexWhere((m) => m.id == member.id); + + if (memberIndex == -1) return; + + // Create a copy of the member (to avoid reference issues) + final memberToMove = TeamMember( + id: member.id, + name: member.name, + year: member.year, + ); + + // Remove from source team + _teams[fromTeamIndex].members.removeAt(memberIndex); + + // Add to destination team + _teams[toTeamIndex].members.add(memberToMove); + + // Mark that we have changes + _hasChanges = !_areTeamsEqual(_teams, _originalTeams); + + notifyListeners(); + + // If we're auto-saving, call the API + if (!saveChangesManually) { + saveChanges(); + } + } + + Future saveChanges() async { + if (!_hasChanges) return; + + _isLoading = true; + notifyListeners(); + + try { + // Call the API to update teams + await _teamService.updateTeams(_teams); + + // Update original teams reference + _originalTeams = _deepCopyTeams(_teams); + _hasChanges = false; + + _isLoading = false; + notifyListeners(); + } catch (e) { + _isLoading = false; + notifyListeners(); + // Handle error + debugPrint('Error saving changes: $e'); + } + } + + // Helper methods + List _deepCopyTeams(List teams) { + return teams.map((team) { + return Team( + id: team.id, + name: team.name, + domain: team.domain, + members: team.members.map((member) { + return TeamMember( + id: member.id, + name: member.name, + year: member.year, + ); + }).toList(), + ); + }).toList(); + } + + bool _areTeamsEqual(List teams1, List teams2) { + if (teams1.length != teams2.length) return false; + + for (int i = 0; i < teams1.length; i++) { + final team1 = teams1[i]; + final team2 = teams2[i]; + + if (team1.id != team2.id || + team1.name != team2.name || + team1.domain != team2.domain || + team1.members.length != team2.members.length) { + return false; + } + + // Check if all members are the same + for (int j = 0; j < team1.members.length; j++) { + final member1 = team1.members[j]; + final member2 = team2.members[j]; + + if (member1.id != member2.id || + member1.name != member2.name || + member1.year != member2.year) { + return false; + } + } + } + + return true; + } +} diff --git a/frontend/lib/views/common_pages/Theme-Demo-Page/sample.dart b/frontend/lib/views/common_pages/Theme-Demo-Page/sample.dart index 1f85d9b..66c4035 100644 --- a/frontend/lib/views/common_pages/Theme-Demo-Page/sample.dart +++ b/frontend/lib/views/common_pages/Theme-Demo-Page/sample.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:dev_track_app/theme/theme.dart'; // Import your theme file class ThemedPage extends StatelessWidget { const ThemedPage({super.key}); diff --git a/frontend/lib/views/user_pages/shuffle/team_shuffle.dart b/frontend/lib/views/user_pages/shuffle/team_shuffle.dart new file mode 100644 index 0000000..f7c3edd --- /dev/null +++ b/frontend/lib/views/user_pages/shuffle/team_shuffle.dart @@ -0,0 +1,310 @@ +import 'package:dev_track_app/models/team_model.dart'; +import 'package:dev_track_app/view_models/team_viewmodel.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class TeamShufflePage extends StatelessWidget { + const TeamShufflePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => TeamDashboardViewModel(), + child: const TeamDashboardView(), + ); + } +} + +class TeamDashboardView extends StatelessWidget { + const TeamDashboardView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final viewModel = Provider.of(context); + + return Scaffold( + backgroundColor: Colors.grey[800], + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Card( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with title and toggle + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Team Dashboard', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + Switch( + value: viewModel.isEditMode, + onChanged: (value) => viewModel.toggleEditMode(), + activeColor: Colors.purple, + ), + ], + ), + const SizedBox(height: 20), + + // Filter section + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + // Domain Dropdown + Expanded( + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: viewModel.selectedDomain, + hint: const Text('Domain'), + icon: const Icon(Icons.arrow_drop_down, + color: Colors.purple), + isExpanded: true, + onChanged: (String? newValue) { + if (newValue != null) { + viewModel.setDomain(newValue); + } + }, + items: viewModel.domains + .map>( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + ), + const SizedBox(width: 16), + + // Year Dropdown + Expanded( + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: viewModel.selectedYear, + hint: const Text('Year'), + icon: const Icon(Icons.arrow_drop_down, + color: Colors.purple), + isExpanded: true, + onChanged: (String? newValue) { + if (newValue != null) { + viewModel.setYear(newValue); + } + }, + items: viewModel.years + .map>( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + // Team Lists + Expanded( + child: viewModel.isLoading + ? const Center( + child: + CircularProgressIndicator(color: Colors.purple)) + : ListView.builder( + itemCount: viewModel.filteredTeams.length, + itemBuilder: (context, index) { + final team = viewModel.filteredTeams[index]; + return _buildTeamCard(context, team, viewModel); + }, + ), + ), + + // Save Button (only shown in edit mode) + if (viewModel.isEditMode && viewModel.saveChangesManually) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: ElevatedButton( + onPressed: viewModel.hasChanges + ? () => viewModel.saveChanges() + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + ), + child: Text( + 'Save Changes', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: viewModel.hasChanges + ? Colors.white + : Colors.grey[300], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildTeamCard( + BuildContext context, Team team, TeamDashboardViewModel viewModel) { + return Padding( + padding: const EdgeInsets.only(bottom: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + team.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.purple, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + child: DragTarget( + onWillAccept: (data) => + viewModel.isEditMode && data?.currentTeamId != team.id, + onAccept: (data) { + viewModel.moveMember(data.member, data.currentTeamId, team.id); + HapticFeedback.mediumImpact(); // Vibration feedback on drop + }, + builder: (context, candidateData, rejectedData) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: candidateData.isNotEmpty + ? Border.all(color: Colors.purple, width: 2) + : null, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: team.members.map((member) { + return viewModel.isEditMode + ? Draggable( + // Add onDragStarted callback to trigger haptic feedback + onDragStarted: () { + // Quick selection vibration when item is picked up + HapticFeedback.selectionClick(); + }, + data: MemberDragData(member, team.id), + feedback: Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.9), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + member.name, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Text( + member.year, + style: const TextStyle( + color: Colors.white, + ), + ), + ], + ), + ), + ), + childWhenDragging: Opacity( + opacity: 0.3, + child: _buildMemberRow(member), + ), + child: _buildMemberRow(member), + ) + : _buildMemberRow(member); + }).toList(), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildMemberRow(TeamMember member) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + member.name, + style: const TextStyle(fontSize: 16), + ), + Text( + member.year, + style: const TextStyle(fontSize: 16), + ), + ], + ), + ); + } +} + +//vibrator config + +class HapticUtils { + // Method for selection click haptic feedback + static Future selectionClick() async { + await SystemChannels.platform.invokeMethod( + 'HapticFeedback.vibrate', + 'HapticFeedbackType.selectionClick', + ); + } +}