import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import {
getTeams,
createTeam,
updateTeam,
deleteTeam,
getTeamMembers,
addTeamMember,
removeTeamMember,
getUsers,
getRoles,
getTeamRoles,
assignTeamRole,
removeTeamRole,
} from '../../../api';
import type {
Team,
TeamCreate,
TeamUpdate,
TeamMember,
User,
Role,
} from '../../../types';
import '@shoelace-style/shoelace/dist/components/alert/alert.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js';
import '@shoelace-style/shoelace/dist/components/input/input.js';
import '@shoelace-style/shoelace/dist/components/textarea/textarea.js';
import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/spinner/spinner.js';
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
import '@shoelace-style/shoelace/dist/components/badge/badge.js';
import '@shoelace-style/shoelace/dist/components/select/select.js';
import '@shoelace-style/shoelace/dist/components/option/option.js';
import '@shoelace-style/shoelace/dist/components/checkbox/checkbox.js';
@customElement('team-management-view')
export class TeamManagementView extends LitElement {
@state()
private teams: Team[] = [];
@state()
private users: User[] = [];
@state()
private isLoading = true;
@state()
private error: string | null = null;
@state()
private isCreateModalOpen = false;
@state()
private isEditModalOpen = false;
@state()
private isMembersModalOpen = false;
@state()
private selectedTeam: Team | null = null;
@state()
private teamMembers: TeamMember[] = [];
@state()
private newTeam: Partial<TeamCreate> = {};
@state()
private editTeam: Partial<TeamUpdate> = {};
@state()
private selectedUserId = '';
@state()
private roles: Role[] = [];
@state()
private isRoleModalOpen = false;
@state()
private teamRoles: Role[] = [];
static styles = css`
:host {
display: block;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.teams-grid {
display: grid;
gap: 1rem;
}
sl-card {
width: 100%;
}
.team-card-content {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 1rem;
align-items: center;
}
.team-icon {
font-size: 2rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--sl-color-primary-50);
border-radius: 50%;
color: var(--sl-color-primary-600);
}
.team-details {
flex: 1;
}
.team-name {
font-weight: 600;
font-size: 1rem;
margin: 0 0 0.25rem 0;
}
.team-description {
color: var(--sl-color-neutral-600);
font-size: 0.875rem;
margin: 0 0 0.5rem 0;
}
.team-roles {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
margin-top: 0.5rem;
}
.team-roles strong {
font-size: 0.875rem;
color: var(--sl-color-neutral-600);
}
.team-actions {
display: flex;
gap: 0.5rem;
}
.form-grid {
display: grid;
gap: 1rem;
}
.members-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 400px;
overflow-y: auto;
margin-bottom: 1rem;
}
.member-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
border-radius: 4px;
background: var(--sl-color-neutral-50);
}
.member-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.member-name {
font-weight: 500;
}
.member-email {
font-size: 0.875rem;
color: var(--sl-color-neutral-600);
}
.add-member-section {
padding: 1rem;
background: var(--sl-color-neutral-50);
border-radius: 4px;
margin-top: 1rem;
}
.add-member-form {
display: flex;
gap: 0.5rem;
align-items: end;
}
.role-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 300px;
overflow-y: auto;
}
.role-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
background: var(--sl-color-neutral-50);
}
.error {
color: var(--sl-color-danger-600);
background: var(--sl-color-danger-50);
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 4rem;
}
`;
async connectedCallback() {
super.connectedCallback();
await Promise.all([
this.fetchTeams(),
this.fetchUsers(),
this.fetchRoles(),
]);
}
async fetchTeams() {
this.isLoading = true;
this.error = null;
try {
const response = await getTeams();
this.teams = response.teams;
} catch (error) {
this.error =
error instanceof Error ? error.message : 'Failed to fetch teams';
} finally {
this.isLoading = false;
}
}
async fetchUsers() {
try {
const response = await getUsers();
this.users = response.users;
} catch (error) {
console.error('Failed to fetch users:', error);
}
}
async fetchRoles() {
try {
const response = await getRoles();
this.roles = response.roles;
} catch (error) {
console.error('Failed to fetch roles:', error);
}
}
async handleCreateTeam() {
if (!this.newTeam.name) {
return;
}
try {
await createTeam(this.newTeam as TeamCreate);
this.isCreateModalOpen = false;
this.newTeam = {};
await this.fetchTeams();
} catch (error) {
this.error =
error instanceof Error ? error.message : 'Failed to create team';
}
}
async handleEditTeam() {
if (!this.selectedTeam) return;
try {
await updateTeam(this.selectedTeam.id, this.editTeam);
this.isEditModalOpen = false;
this.selectedTeam = null;
this.editTeam = {};
await this.fetchTeams();
} catch (error) {
this.error =
error instanceof Error ? error.message : 'Failed to update team';
}
}
async handleDeleteTeam(team: Team) {
if (!confirm(`Are you sure you want to delete team "${team.name}"?`)) {
return;
}
try {
await deleteTeam(team.id);
await this.fetchTeams();
} catch (error) {
this.error =
error instanceof Error ? error.message : 'Failed to delete team';
}
}
async openMembersModal(team: Team) {
this.selectedTeam = team;
this.isMembersModalOpen = true;
try {
this.teamMembers = await getTeamMembers(team.id);
} catch (error) {
console.error('Failed to fetch team members:', error);
this.teamMembers = [];
}
}
async handleAddMember() {
if (!this.selectedTeam || !this.selectedUserId) return;
try {
await addTeamMember(this.selectedTeam.id, this.selectedUserId);
this.selectedUserId = '';
this.teamMembers = await getTeamMembers(this.selectedTeam.id);
} catch (error) {
this.error =
error instanceof Error ? error.message : 'Failed to add team member';
}
}
async handleRemoveMember(userId: string) {
if (!this.selectedTeam) return;
if (!confirm('Are you sure you want to remove this member?')) {
return;
}
try {
await removeTeamMember(this.selectedTeam.id, userId);
this.teamMembers = await getTeamMembers(this.selectedTeam.id);
} catch (error) {
this.error =
error instanceof Error ? error.message : 'Failed to remove team member';
}
}
openEditModal(team: Team) {
this.selectedTeam = team;
this.editTeam = {
name: team.name,
description: team.description || undefined,
};
this.isEditModalOpen = true;
}
async openRoleModal(team: Team) {
this.selectedTeam = team;
this.isRoleModalOpen = true;
try {
this.teamRoles = await getTeamRoles(team.id);
} catch (error) {
console.error('Failed to fetch team roles:', error);
this.teamRoles = [];
}
}
async handleToggleRole(roleId: string, isChecked: boolean) {
if (!this.selectedTeam) return;
try {
if (isChecked) {
await assignTeamRole(this.selectedTeam.id, roleId);
} else {
await removeTeamRole(this.selectedTeam.id, roleId);
}
// Refresh team roles
this.teamRoles = await getTeamRoles(this.selectedTeam.id);
// Refresh teams to update role display in cards
await this.fetchTeams();
} catch (error) {
this.error =
error instanceof Error ? error.message : 'Failed to update role';
}
}
render() {
if (this.isLoading) {
return html`
<div class="loading">
<sl-spinner style="font-size: 3rem;"></sl-spinner>
</div>
`;
}
return html`
<div class="header">
<h1>Team Management</h1>
<sl-button
variant="primary"
@click=${() => (this.isCreateModalOpen = true)}
>
<sl-icon slot="prefix" name="people-fill"></sl-icon>
Create Team
</sl-button>
</div>
${this.error ? html`<div class="error">${this.error}</div>` : ''}
<div class="teams-grid">
${repeat(
this.teams,
(team) => team.id,
(team) => html`
<sl-card>
<div class="team-card-content">
<div class="team-icon">
<sl-icon name="people-fill"></sl-icon>
</div>
<div class="team-details">
<h3 class="team-name">${team.name}</h3>
${team.description
? html`<p class="team-description">${team.description}</p>`
: ''}
${(team as any).roles && (team as any).roles.length > 0
? html`
<div class="team-roles">
<strong>Roles:</strong>
${(team as any).roles.map(
(role: any) =>
html`<sl-badge variant="primary"
>${role.name}</sl-badge
>`
)}
</div>
`
: ''}
</div>
<div class="team-actions">
<sl-button
size="small"
@click=${() => this.openRoleModal(team)}
>
<sl-icon name="shield-check"></sl-icon>
</sl-button>
<sl-button
size="small"
@click=${() => this.openMembersModal(team)}
>
<sl-icon name="person-lines-fill"></sl-icon>
</sl-button>
<sl-button
size="small"
@click=${() => this.openEditModal(team)}
>
<sl-icon name="pencil"></sl-icon>
</sl-button>
<sl-button
size="small"
variant="danger"
@click=${() => this.handleDeleteTeam(team)}
>
<sl-icon name="trash"></sl-icon>
</sl-button>
</div>
</div>
</sl-card>
`
)}
</div>
<!-- Create Team Modal -->
<sl-dialog
label="Create Team"
?open=${this.isCreateModalOpen}
@sl-request-close=${() => (this.isCreateModalOpen = false)}
>
<div class="form-grid">
<sl-input
label="Team Name"
placeholder="Enter team name"
value=${this.newTeam.name || ''}
@sl-input=${(e: any) => (this.newTeam.name = e.target.value)}
></sl-input>
<sl-textarea
label="Description"
placeholder="Enter team description (optional)"
value=${this.newTeam.description || ''}
@sl-input=${(e: any) => (this.newTeam.description = e.target.value)}
></sl-textarea>
</div>
<sl-button
slot="footer"
variant="primary"
@click=${this.handleCreateTeam}
>
Create Team
</sl-button>
<sl-button
slot="footer"
variant="default"
@click=${() => (this.isCreateModalOpen = false)}
>
Cancel
</sl-button>
</sl-dialog>
<!-- Edit Team Modal -->
<sl-dialog
label="Edit Team"
?open=${this.isEditModalOpen}
@sl-request-close=${() => (this.isEditModalOpen = false)}
>
<div class="form-grid">
<sl-input
label="Team Name"
value=${this.editTeam.name || ''}
@sl-input=${(e: any) => (this.editTeam.name = e.target.value)}
></sl-input>
<sl-textarea
label="Description"
value=${this.editTeam.description || ''}
@sl-input=${(e: any) =>
(this.editTeam.description = e.target.value)}
></sl-textarea>
</div>
<sl-button
slot="footer"
variant="primary"
@click=${this.handleEditTeam}
>
Save Changes
</sl-button>
<sl-button
slot="footer"
variant="default"
@click=${() => (this.isEditModalOpen = false)}
>
Cancel
</sl-button>
</sl-dialog>
<!-- Team Members Modal -->
<sl-dialog
label="Team Members"
?open=${this.isMembersModalOpen}
@sl-request-close=${() => (this.isMembersModalOpen = false)}
>
<div class="members-list">
${this.teamMembers.length === 0
? html`<p>No members in this team yet.</p>`
: this.teamMembers.map((member) => {
const user = this.users.find((u) => u.id === member.user_id);
return html`
<div class="member-item">
<div class="member-info">
<span class="member-name">
${user?.full_name || user?.username || 'Unknown User'}
</span>
<span class="member-email">${user?.email || ''}</span>
</div>
<sl-button
size="small"
variant="danger"
@click=${() => this.handleRemoveMember(member.user_id)}
>
<sl-icon name="x-lg"></sl-icon>
</sl-button>
</div>
`;
})}
</div>
<div class="add-member-section">
<h4>Add Member</h4>
<div class="add-member-form">
<sl-select
placeholder="Select user"
value=${this.selectedUserId}
@sl-change=${(e: any) => (this.selectedUserId = e.target.value)}
style="flex: 1;"
>
${this.users
.filter(
(u) => !this.teamMembers.some((m) => m.user_id === u.id)
)
.map(
(user) => html`
<sl-option value=${user.id}>
${user.full_name || user.username} (${user.email})
</sl-option>
`
)}
</sl-select>
<sl-button @click=${this.handleAddMember}>Add</sl-button>
</div>
</div>
<sl-button
slot="footer"
variant="primary"
@click=${() => (this.isMembersModalOpen = false)}
>
Done
</sl-button>
</sl-dialog>
<!-- Manage Team Roles Modal -->
<sl-dialog
label="Manage Team Roles"
?open=${this.isRoleModalOpen}
@sl-request-close=${() => (this.isRoleModalOpen = false)}
>
<div class="role-list">
${this.roles.map((role) => {
const isAssigned = this.teamRoles.some((r) => r.id === role.id);
return html`
<div class="role-item">
<sl-checkbox
?checked=${isAssigned}
@sl-change=${(e: any) =>
this.handleToggleRole(role.id, e.target.checked)}
>
${role.name}
</sl-checkbox>
${role.description
? html`<span
style="font-size: 0.875rem; color: var(--sl-color-neutral-600);"
>
${role.description}
</span>`
: ''}
</div>
`;
})}
</div>
<sl-button
slot="footer"
variant="primary"
@click=${() => (this.isRoleModalOpen = false)}
>
Done
</sl-button>
</sl-dialog>
`;
}
}