Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions client/components/home/HomePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,20 @@ Template.home.onCreated(function () {

template.gridOptions = createGridOptions(template.shouldShowClockTimes());

// Helper to get teams where user is leader or admin
template.getAdminOrLeaderTeams = () => {
return Teams.find({
$or: [
{ leader: Meteor.userId() },
{ admins: Meteor.userId() }
]
}).fetch();
};

// Subscribe to data
this.autorun(() => {
const leaderTeams = Teams.find({ leader: Meteor.userId() }).fetch();
const teamIds = leaderTeams.map(t => t._id);
const adminOrLeaderTeams = template.getAdminOrLeaderTeams();
const teamIds = adminOrLeaderTeams.map(t => t._id);

if (teamIds.length) {
this.subscribe('clockEventsForTeams', teamIds);
Expand Down Expand Up @@ -153,16 +163,16 @@ Template.home.onCreated(function () {
template.computeTeamMemberSummary = () => {
const startDateStr = template.startDate.get();
const endDateStr = template.endDate.get();
const leaderTeams = Teams.find({ leader: Meteor.userId() }).fetch();
const adminOrLeaderTeams = template.getAdminOrLeaderTeams();

if (!leaderTeams.length) return [];
if (!adminOrLeaderTeams.length) return [];

const startDate = new Date(startDateStr + 'T00:00:00');
const endDate = new Date(endDateStr + 'T23:59:59');
const teamIds = leaderTeams.map(t => t._id);
const teamIds = adminOrLeaderTeams.map(t => t._id);

const allMembers = Array.from(new Set(
leaderTeams.flatMap(t => [...(t.members || []), ...(t.admins || []), t.leader].filter(id => id))
adminOrLeaderTeams.flatMap(t => [...(t.members || []), ...(t.admins || []), t.leader].filter(id => id))
));

const rows = [];
Expand Down Expand Up @@ -267,8 +277,13 @@ Template.home.onRendered(function () {
// Initialize grid when ready
instance.autorun(() => {
const teamsReady = !isTeamsLoading.get();
const hasLeaderTeam = !!Teams.findOne({ leader: Meteor.userId() });
if (!teamsReady || !hasLeaderTeam) return;
const hasAdminOrLeaderTeam = !!Teams.findOne({
$or: [
{ leader: Meteor.userId() },
{ admins: Meteor.userId() }
]
});
if (!teamsReady || !hasAdminOrLeaderTeam) return;

Tracker.afterFlush(() => {
const gridEl = instance.find('#teamDashboardGrid');
Expand Down Expand Up @@ -296,7 +311,12 @@ Template.home.onRendered(function () {
instance.startDate.get();
instance.endDate.get();
instance.selectedPreset.get();
Teams.find({ leader: Meteor.userId() }).fetch();
Teams.find({
$or: [
{ leader: Meteor.userId() },
{ admins: Meteor.userId() }
]
}).fetch();
ClockEvents.find().fetch();
Tickets.find().fetch();

Expand All @@ -316,9 +336,14 @@ Template.home.onDestroyed(function () {
});

Template.home.helpers({
// User role helpers
// User role helpers - check if user is leader or admin
isTeamLeader() {
return Teams.findOne({ leader: Meteor.userId() });
return Teams.findOne({
$or: [
{ leader: Meteor.userId() },
{ admins: Meteor.userId() }
]
});
},

isFirstTimeUser() {
Expand Down Expand Up @@ -379,8 +404,13 @@ Template.home.helpers({

// Legacy helpers for team dashboard
allClockEvents() {
const leaderTeams = Teams.find({ leader: Meteor.userId() }).fetch();
const teamIds = leaderTeams.map(t => t._id);
const adminOrLeaderTeams = Teams.find({
$or: [
{ leader: Meteor.userId() },
{ admins: Meteor.userId() }
]
}).fetch();
const teamIds = adminOrLeaderTeams.map(t => t._id);
return ClockEvents.find({ teamId: { $in: teamIds } }, { sort: { startTimestamp: -1 } }).fetch();
},

Expand Down
53 changes: 51 additions & 2 deletions client/components/teams/TeamsPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,58 @@ <h4 class="text-lg font-bold mb-2">{{selectedTeam.name}}</h4>
<button id="copyTeamCode" class="btn btn-sm btn-outline">Copy Code</button>
</div>
<h5 class="font-semibold mb-2">Collaborators:</h5>
<div class="flex flex-wrap gap-2">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{{#each selectedTeam.members}}
<div class="badge badge-neutral">{{#if this.name}}{{this.name}}{{else}}{{this.email}}{{/if}}</div>
<div class="card bg-base-200 shadow-sm p-3 flex items-center justify-between gap-2">
<div class="flex items-center gap-2 flex-1 min-w-0">
<span class="font-medium truncate">{{#if this.name}}{{this.name}}{{else}}{{this.email}}{{/if}}</span>
{{#if isMemberAdmin id}}
<span class="text-sm text-blue-500 flex-shrink-0" title="Admin">👑</span>
{{/if}}
</div>
<div class="flex items-center gap-1 flex-shrink-0">
{{#if canViewMemberDashboard id}}
<button
type="button"
class="view-dashboard-btn btn btn-xs btn-ghost text-green-600 hover:text-green-700 hover:bg-green-50"
data-member-id="{{id}}"
title="View Dashboard"
>
📊
</button>
{{/if}}
{{#if canPromoteToAdmin id}}
<button
type="button"
class="promote-admin-btn btn btn-xs btn-ghost text-blue-600 hover:text-blue-700 hover:bg-blue-50"
data-member-id="{{id}}"
title="Make Admin"
>
</button>
{{/if}}
{{#if canDemoteFromAdmin id}}
<button
type="button"
class="demote-admin-btn btn btn-xs btn-ghost text-orange-600 hover:text-orange-700 hover:bg-orange-50"
data-member-id="{{id}}"
title="Remove Admin"
>
</button>
{{/if}}
{{#if canRemoveMember id}}
<button
type="button"
class="remove-member-btn btn btn-xs btn-ghost text-red-600 hover:text-red-700 hover:bg-red-50"
data-member-id="{{id}}"
title="Remove from team"
>
</button>
{{/if}}
</div>
</div>
{{/each}}
</div>
</div>
Expand Down
144 changes: 142 additions & 2 deletions client/components/teams/TeamsPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { Teams } from '../../../collections.js';
import { getUserTeams } from '../../utils/UserTeamUtils.js';

Expand Down Expand Up @@ -57,16 +58,88 @@ Template.teams.helpers({
const userId = Meteor.userId();
return !!(team && Array.isArray(team.admins) && userId && team.admins.includes(userId));
},
canRemoveMember(memberId) {
const instance = Template.instance();
const teamId = instance.selectedTeamId.get();
if (!teamId || !memberId) return false;

const team = Teams.findOne(teamId);
const userId = Meteor.userId();
if (!team || !userId) return false;

const isAdmin = Array.isArray(team.admins) && team.admins.includes(userId);
const isLeader = team.leader === memberId;

// Admins can remove any member except the team leader
return isAdmin && !isLeader;
},
isMemberAdmin(memberId) {
const instance = Template.instance();
const teamId = instance.selectedTeamId.get();
if (!teamId || !memberId) return false;

const team = Teams.findOne(teamId);
if (!team) return false;

return Array.isArray(team.admins) && team.admins.includes(memberId);
},
canPromoteToAdmin(memberId) {
const instance = Template.instance();
const teamId = instance.selectedTeamId.get();
if (!teamId || !memberId) return false;

const team = Teams.findOne(teamId);
const userId = Meteor.userId();
if (!team || !userId) return false;

const isAdmin = Array.isArray(team.admins) && team.admins.includes(userId);
const isAlreadyAdmin = Array.isArray(team.admins) && team.admins.includes(memberId);
const isLeader = team.leader === memberId;

// Admins can promote any member who is not already an admin (leader is effectively always admin)
return isAdmin && !isAlreadyAdmin && !isLeader;
},
canDemoteFromAdmin(memberId) {
const instance = Template.instance();
const teamId = instance.selectedTeamId.get();
if (!teamId || !memberId) return false;

const team = Teams.findOne(teamId);
const userId = Meteor.userId();
if (!team || !userId) return false;

const isAdmin = Array.isArray(team.admins) && team.admins.includes(userId);
const isMemberAdmin = Array.isArray(team.admins) && team.admins.includes(memberId);
const isLeader = team.leader === memberId;

// Admins can demote other admins, but not the leader
return isAdmin && isMemberAdmin && !isLeader;
},
canViewMemberDashboard(memberId) {
const instance = Template.instance();
const teamId = instance.selectedTeamId.get();
if (!teamId || !memberId) return false;

const team = Teams.findOne(teamId);
const userId = Meteor.userId();
if (!team || !userId) return false;

const isAdmin = Array.isArray(team.admins) && team.admins.includes(userId);
const isLeader = team.leader === userId;

// Admins and leaders can view any team member's dashboard
return isAdmin || isLeader;
},
});

Template.teams.events({
'click #showCreateTeamForm'(e, t) {
t.showCreateTeam.set(true);
t.showJoinTeam && t.showJoinTeam.set(false);
t.showJoinTeam.set(false);
},
'click #showJoinTeamForm'(e, t) {
t.showJoinTeam.set(true);
t.showCreateTeam && t.showCreateTeam.set(false);
t.showCreateTeam.set(false);
},
'click #cancelCreateTeam'(e, t) {
t.showCreateTeam.set(false);
Expand Down Expand Up @@ -130,6 +203,66 @@ Template.teams.events({
});
}
},
'click .remove-member-btn'(e, t) {
e.preventDefault();
const memberId = e.currentTarget.dataset.memberId;
const teamId = t.selectedTeamId.get();
if (!teamId || !memberId) return;

const users = t.selectedTeamUsers.get() || [];
const user = users.find(u => u.id === memberId);
const displayName = user?.name || user?.email || 'this member';

if (!confirm(`Remove ${displayName} from this team?`)) {
return;
}

Meteor.call('removeTeamMember', teamId, memberId, (err) => {
if (err) {
alert('Error removing member: ' + (err.reason || err.message));
}
});
},
'click .promote-admin-btn'(e, t) {
e.preventDefault();
const memberId = e.currentTarget.dataset.memberId;
const teamId = t.selectedTeamId.get();
if (!teamId || !memberId) return;

const users = t.selectedTeamUsers.get() || [];
const user = users.find(u => u.id === memberId);
const displayName = user?.name || user?.email || 'this member';

if (!confirm(`Make ${displayName} a co-admin? They will have all admin rights.`)) {
return;
}

Meteor.call('promoteToAdmin', teamId, memberId, (err) => {
if (err) {
alert('Error promoting to admin: ' + (err.reason || err.message));
}
});
},
'click .demote-admin-btn'(e, t) {
e.preventDefault();
const memberId = e.currentTarget.dataset.memberId;
const teamId = t.selectedTeamId.get();
if (!teamId || !memberId) return;

const users = t.selectedTeamUsers.get() || [];
const user = users.find(u => u.id === memberId);
const displayName = user?.name || user?.email || 'this member';

if (!confirm(`Remove admin rights from ${displayName}? They will remain a team member.`)) {
return;
}

Meteor.call('demoteFromAdmin', teamId, memberId, (err) => {
if (err) {
alert('Error removing admin rights: ' + (err.reason || err.message));
}
});
},
'click .edit-team-btn'(e, t) {
e.preventDefault();
const teamId = e.currentTarget.dataset.id;
Expand Down Expand Up @@ -157,4 +290,11 @@ Template.teams.events({
});
}
},
'click .view-dashboard-btn'(e, t) {
e.preventDefault();
const memberId = e.currentTarget.dataset.memberId;
if (memberId) {
FlowRouter.go(`/timesheet/${memberId}`);
}
},
});
Loading