import * as plugins from '../../../plugins.js'; import { customElement, DeesElement, html, cssManager, css, state, type TemplateResult, } from '@design.estate/dees-element'; import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js'; import * as accountState from '../../../states/accountstate.js'; import { IdpState } from '../../../states/idp.state.js'; declare global { interface HTMLElementTagNameMap { 'lele-accountview-users': UsersView; } } interface IMemberDisplay { userId: string; name: string; email: string; roles: string[]; isOwner: boolean; } interface IInvitationDisplay { id: string; email: string; roles: string[]; invitedAt: number; expiresAt: number; } @customElement('lele-accountview-users') export class UsersView extends DeesElement { @state() accessor members: IMemberDisplay[] = []; @state() accessor invitations: IInvitationDisplay[] = []; @state() accessor loading: boolean = true; @state() accessor activeTab: 'members' | 'pending' | 'invite' = 'members'; @state() accessor organizationId: string = ''; @state() accessor organizationName: string = ''; @state() accessor inviteEmail: string = ''; @state() accessor inviteRoles: string[] = ['viewer']; @state() accessor isAdmin: boolean = false; @state() accessor currentUserId: string = ''; @state() accessor submitting: boolean = false; @state() accessor actionMessage: { type: 'success' | 'error'; text: string } | null = null; private static readonly AVAILABLE_ROLES = ['owner', 'admin', 'editor', 'viewer', 'guest']; public static styles = [ cssManager.defaultStyles, accountDesignTokens, cardStyles, typographyStyles, css` :host { display: block; padding: 48px; max-width: 1000px; margin: 0 auto; } .tabs { display: flex; gap: 4px; margin-bottom: 32px; border-bottom: 1px solid var(--border); padding-bottom: 8px; } .tab { padding: 10px 20px; border-radius: 8px 8px 0 0; font-size: 14px; font-weight: 500; color: var(--muted-foreground); cursor: pointer; transition: all 0.15s ease; border: none; background: transparent; } .tab:hover { color: var(--foreground); background: var(--muted); } .tab.active { color: var(--foreground); background: var(--muted); } .member-list { display: flex; flex-direction: column; gap: 12px; } .member-card { display: flex; align-items: center; justify-content: space-between; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px 20px; transition: all 0.15s ease; } .member-card:hover { border-color: var(--muted-foreground); } .member-info { display: flex; align-items: center; gap: 16px; } .member-avatar { width: 40px; height: 40px; border-radius: 50%; background: var(--muted); display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 600; color: var(--foreground); } .member-details { display: flex; flex-direction: column; gap: 2px; } .member-name { font-size: 14px; font-weight: 600; color: var(--foreground); } .member-email { font-size: 13px; color: var(--muted-foreground); } .member-roles { display: flex; gap: 6px; flex-wrap: wrap; } .role-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; } .role-badge.owner { background: rgba(234, 179, 8, 0.2); color: #eab308; } .role-badge.admin { background: rgba(59, 130, 246, 0.2); color: #3b82f6; } .role-badge.editor { background: rgba(34, 197, 94, 0.2); color: #22c55e; } .role-badge.viewer { background: rgba(148, 163, 184, 0.2); color: #94a3b8; } .role-badge.guest { background: rgba(168, 162, 158, 0.2); color: #a8a29e; } .member-actions { display: flex; gap: 8px; } .action-button { padding: 8px 12px; border-radius: 6px; font-size: 12px; font-weight: 500; border: 1px solid var(--border); background: transparent; color: var(--muted-foreground); cursor: pointer; transition: all 0.15s ease; } .action-button:hover { border-color: var(--foreground); color: var(--foreground); } .action-button.danger:hover { border-color: #ef4444; color: #ef4444; } .action-button:disabled { opacity: 0.5; cursor: not-allowed; } .invitation-card { display: flex; align-items: center; justify-content: space-between; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px 20px; } .invitation-info { display: flex; flex-direction: column; gap: 4px; } .invitation-email { font-size: 14px; font-weight: 500; color: var(--foreground); } .invitation-meta { font-size: 12px; color: var(--muted-foreground); } .invite-form { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 24px; } .form-group { margin-bottom: 20px; } .form-label { display: block; font-size: 13px; font-weight: 500; color: var(--foreground); margin-bottom: 8px; } .role-selector { display: flex; flex-wrap: wrap; gap: 8px; } .role-option { padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; border: 1px solid var(--border); background: transparent; color: var(--muted-foreground); cursor: pointer; transition: all 0.15s ease; } .role-option:hover { border-color: var(--foreground); color: var(--foreground); } .role-option.selected { border-color: #3b82f6; background: rgba(59, 130, 246, 0.1); color: #3b82f6; } .message { padding: 12px 16px; border-radius: 8px; font-size: 13px; margin-bottom: 20px; } .message.success { background: rgba(34, 197, 94, 0.1); color: #22c55e; border: 1px solid rgba(34, 197, 94, 0.3); } .message.error { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); } .empty-state { text-align: center; padding: 48px; color: var(--muted-foreground); } .empty-state dees-icon { font-size: 48px; opacity: 0.5; margin-bottom: 16px; } .loading { display: flex; align-items: center; justify-content: center; padding: 48px; color: var(--muted-foreground); } .you-badge { font-size: 10px; padding: 2px 6px; background: rgba(59, 130, 246, 0.2); color: #3b82f6; border-radius: 4px; margin-left: 8px; } `, ]; public render() { return html`

Users

Manage members and invitations for ${this.organizationName || 'your organization'}.

${this.actionMessage ? html`
${this.actionMessage.text}
` : ''}
${this.isAdmin ? html` ` : ''}
${this.renderTabContent()} `; } private renderTabContent() { if (this.loading) { return html`
Loading users...
`; } switch (this.activeTab) { case 'members': return this.renderMembers(); case 'pending': return this.renderPendingInvitations(); case 'invite': return this.renderInviteForm(); } } private renderMembers() { if (this.members.length === 0) { return html`

No Members

This organization has no members yet.

`; } return html`
${this.members.map(member => html`
${member.name.charAt(0).toUpperCase()}
${member.name} ${member.userId === this.currentUserId ? html`You` : ''} ${member.email}
${member.roles.map(role => html` ${role} `)}
${this.isAdmin && member.userId !== this.currentUserId ? html`
` : ''}
`)}
`; } private renderPendingInvitations() { if (this.invitations.length === 0) { return html`

No Pending Invitations

There are no pending invitations for this organization.

`; } return html`
${this.invitations.map(inv => html`
${inv.email} Invited ${this.formatDate(inv.invitedAt)} ยท Expires ${this.formatDate(inv.expiresAt)}
${inv.roles.map(role => html` ${role} `)}
${this.isAdmin ? html`
` : ''}
`)}
`; } private renderInviteForm(): TemplateResult { return html`
${UsersView.AVAILABLE_ROLES.filter(r => r !== 'owner').map(role => html` `)}
this.handleSendInvitation()} >
`; } public async firstUpdated() { // Subscribe to email input changes await this.updateComplete; const emailInput = this.shadowRoot?.querySelector('dees-input-text') as any; if (emailInput) { emailInput.changeSubject?.subscribe((element: any) => { this.inviteEmail = element.value; }); } await this.loadData(); } private async loadData() { this.loading = true; try { // Get the organization from URL const pathParts = window.location.pathname.split('/'); const orgSlug = pathParts[3]; const currentState = accountState.accountState.getState(); const selectedOrg = currentState.organizations.find(org => org.data.slug === orgSlug); if (!selectedOrg) { console.error('Organization not found'); this.loading = false; return; } this.organizationId = selectedOrg.id; this.organizationName = selectedOrg.data.name; this.currentUserId = currentState.user?.id || ''; // Check if current user is admin const currentUserRole = currentState.roles.find( r => r.data.organizationId === this.organizationId && r.data.userId === this.currentUserId ); this.isAdmin = currentUserRole?.data?.roles?.some(r => ['owner', 'admin'].includes(r)) ?? false; // Get JWT from IdpState const idpState = await IdpState.getSingletonInstance(); const jwt = await idpState.idpClient.getJwt(); // Fetch members const membersRequest = idpState.idpClient.typedsocket.createTypedRequest( 'getOrgMembers' ); const membersResponse = await membersRequest.fire({ jwt, organizationId: this.organizationId, }); this.members = membersResponse.members.map(m => ({ userId: m.user.id, name: m.user.data.name || m.user.data.username || 'Unknown', email: m.user.data.email, roles: m.role.data.roles || [], isOwner: m.role.data.roles?.includes('owner') ?? false, })); // Fetch invitations if admin if (this.isAdmin) { const invitationsRequest = idpState.idpClient.typedsocket.createTypedRequest( 'getOrgInvitations' ); const invitationsResponse = await invitationsRequest.fire({ jwt, organizationId: this.organizationId, }); this.invitations = invitationsResponse.invitations.map(inv => { const orgRef = inv.data.organizationRefs.find(ref => ref.organizationId === this.organizationId); return { id: inv.id, email: inv.data.email, roles: orgRef?.roles || [], invitedAt: orgRef?.invitedAt || inv.data.createdAt, expiresAt: inv.data.expiresAt, }; }); } } catch (error) { console.error('Error loading users:', error); } finally { this.loading = false; } } private toggleRole(role: string) { if (this.inviteRoles.includes(role)) { this.inviteRoles = this.inviteRoles.filter(r => r !== role); } else { this.inviteRoles = [...this.inviteRoles, role]; } // Ensure at least one role is selected if (this.inviteRoles.length === 0) { this.inviteRoles = ['viewer']; } } private async handleSendInvitation() { if (!this.inviteEmail.trim()) { this.showMessage('error', 'Please enter an email address.'); return; } if (this.inviteRoles.length === 0) { this.showMessage('error', 'Please select at least one role.'); return; } this.submitting = true; this.actionMessage = null; try { const idpState = await IdpState.getSingletonInstance(); const jwt = await idpState.idpClient.getJwt(); const request = idpState.idpClient.typedsocket.createTypedRequest( 'createInvitation' ); const response = await request.fire({ jwt, organizationId: this.organizationId, email: this.inviteEmail.trim(), roles: this.inviteRoles, }); if (response.success) { this.showMessage('success', response.message || 'Invitation sent successfully!'); this.inviteEmail = ''; this.inviteRoles = ['viewer']; await this.loadData(); this.activeTab = 'pending'; } else { this.showMessage('error', response.message || 'Failed to send invitation.'); } } catch (error) { console.error('Error sending invitation:', error); this.showMessage('error', 'Failed to send invitation. Please try again.'); } finally { this.submitting = false; } } private async handleResendInvitation(invitationId: string) { this.submitting = true; this.actionMessage = null; try { const idpState = await IdpState.getSingletonInstance(); const jwt = await idpState.idpClient.getJwt(); const request = idpState.idpClient.typedsocket.createTypedRequest( 'resendInvitation' ); const response = await request.fire({ jwt, organizationId: this.organizationId, invitationId, }); if (response.success) { this.showMessage('success', 'Invitation resent successfully!'); await this.loadData(); } else { this.showMessage('error', response.message || 'Failed to resend invitation.'); } } catch (error) { console.error('Error resending invitation:', error); this.showMessage('error', 'Failed to resend invitation. Please try again.'); } finally { this.submitting = false; } } private async handleCancelInvitation(invitationId: string, email: string) { if (!confirm(`Cancel invitation for ${email}?`)) { return; } this.submitting = true; this.actionMessage = null; try { const idpState = await IdpState.getSingletonInstance(); const jwt = await idpState.idpClient.getJwt(); const request = idpState.idpClient.typedsocket.createTypedRequest( 'cancelInvitation' ); const response = await request.fire({ jwt, organizationId: this.organizationId, invitationId, }); if (response.success) { this.showMessage('success', 'Invitation cancelled.'); await this.loadData(); } else { this.showMessage('error', response.message || 'Failed to cancel invitation.'); } } catch (error) { console.error('Error cancelling invitation:', error); this.showMessage('error', 'Failed to cancel invitation. Please try again.'); } finally { this.submitting = false; } } private async handleRemoveMember(userId: string, name: string) { if (!confirm(`Remove ${name} from this organization?`)) { return; } this.submitting = true; this.actionMessage = null; try { const idpState = await IdpState.getSingletonInstance(); const jwt = await idpState.idpClient.getJwt(); const request = idpState.idpClient.typedsocket.createTypedRequest( 'removeMember' ); const response = await request.fire({ jwt, organizationId: this.organizationId, userId, }); if (response.success) { this.showMessage('success', `${name} has been removed from the organization.`); await this.loadData(); } else { this.showMessage('error', response.message || 'Failed to remove member.'); } } catch (error) { console.error('Error removing member:', error); this.showMessage('error', 'Failed to remove member. Please try again.'); } finally { this.submitting = false; } } private showMessage(type: 'success' | 'error', text: string) { this.actionMessage = { type, text }; // Auto-hide after 5 seconds setTimeout(() => { this.actionMessage = null; }, 5000); } private formatDate(timestamp: number): string { return new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); } }