/** * @file sdig-contract-parties.ts * @description Contract parties and roles management component */ import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, state, } from '@design.estate/dees-element'; import * as plugins from '../../plugins.js'; declare global { interface HTMLElementTagNameMap { 'sdig-contract-parties': SdigContractParties; } } // Party role display configuration const PARTY_ROLES: Array<{ value: plugins.sdInterfaces.TPartyRole; label: string; icon: string }> = [ { value: 'signer', label: 'Signer', icon: 'lucide:pen-tool' }, { value: 'witness', label: 'Witness', icon: 'lucide:eye' }, { value: 'notary', label: 'Notary', icon: 'lucide:stamp' }, { value: 'cc', label: 'CC (Copy)', icon: 'lucide:mail' }, { value: 'approver', label: 'Approver', icon: 'lucide:check-circle' }, { value: 'guarantor', label: 'Guarantor', icon: 'lucide:shield' }, { value: 'beneficiary', label: 'Beneficiary', icon: 'lucide:user-check' }, ]; const SIGNING_DEPENDENCIES: Array<{ value: plugins.sdInterfaces.TSigningDependency; label: string }> = [ { value: 'none', label: 'No dependency' }, { value: 'sequential', label: 'Sequential (in order)' }, { value: 'parallel', label: 'Parallel (any order)' }, { value: 'after_specific', label: 'After specific parties' }, ]; @customElement('sdig-contract-parties') export class SdigContractParties extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .parties-container { display: flex; flex-direction: column; gap: 24px; } /* Section cards */ .section-card { background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 12px; overflow: hidden; } .section-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .section-title { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .section-title dees-icon { font-size: 18px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .section-content { padding: 20px; } /* Roles list */ .roles-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; } .role-card { display: flex; flex-direction: column; padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; transition: all 0.15s ease; } .role-card:hover { border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; } .role-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .role-name { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .role-name dees-icon { font-size: 16px; padding: 6px; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; border-radius: 6px; } .role-badge { font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 4px; background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; } .role-description { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; margin-bottom: 12px; } .role-meta { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .role-meta-item { display: flex; align-items: center; gap: 4px; } /* Parties list */ .parties-list { display: flex; flex-direction: column; gap: 16px; } .party-card { display: flex; align-items: flex-start; gap: 16px; padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; transition: all 0.15s ease; } .party-card:hover { border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; } .party-card.selected { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; background: ${cssManager.bdTheme('#eff6ff', '#172554')}; } .party-avatar { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; color: white; flex-shrink: 0; } .party-info { flex: 1; min-width: 0; } .party-name { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 4px; } .party-role-tag { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; margin-bottom: 8px; } .party-details { display: flex; flex-wrap: wrap; gap: 16px; font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .party-detail { display: flex; align-items: center; gap: 6px; } .party-detail dees-icon { font-size: 14px; } .party-status { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; } .signature-status { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 9999px; font-size: 12px; font-weight: 500; } .signature-status.pending { background: ${cssManager.bdTheme('#fef3c7', '#422006')}; color: ${cssManager.bdTheme('#92400e', '#fcd34d')}; } .signature-status.signed { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#065f46', '#6ee7b7')}; } .signature-status.declined { background: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; color: ${cssManager.bdTheme('#991b1b', '#fca5a5')}; } .signing-order { display: flex; align-items: center; gap: 4px; font-size: 12px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .order-number { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } /* Add button */ .add-button { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 20px; background: transparent; border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 10px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; } .add-button:hover { border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; background: ${cssManager.bdTheme('#f9fafb', '#18181b')}; } /* Empty state */ .empty-state { display: flex; flex-direction: column; align-items: center; padding: 40px 20px; text-align: center; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .empty-state dees-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; } .empty-state h4 { margin: 0 0 8px; font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .empty-state p { margin: 0; font-size: 14px; } /* Action buttons */ .action-buttons { display: flex; gap: 8px; } .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; font-size: 13px; font-weight: 500; border-radius: 6px; border: none; cursor: pointer; transition: all 0.15s ease; } .btn-sm { padding: 6px 10px; font-size: 12px; } .btn-primary { background: ${cssManager.bdTheme('#111111', '#fafafa')}; color: ${cssManager.bdTheme('#ffffff', '#09090b')}; } .btn-primary:hover { background: ${cssManager.bdTheme('#333333', '#e5e5e5')}; } .btn-secondary { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .btn-secondary:hover { background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')}; } .btn-ghost { background: transparent; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .btn-ghost:hover { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; @property({ type: Boolean }) public accessor readonly: boolean = false; // ============================================================================ // STATE // ============================================================================ @state() private accessor selectedPartyId: string | null = null; @state() private accessor showRoleEditor: boolean = false; @state() private accessor showPartyEditor: boolean = false; // ============================================================================ // EVENT HANDLERS // ============================================================================ private handleFieldChange(path: string, value: unknown) { this.dispatchEvent( new CustomEvent('field-change', { detail: { path, value }, bubbles: true, composed: true, }) ); } private handleSelectParty(partyId: string) { this.selectedPartyId = this.selectedPartyId === partyId ? null : partyId; this.dispatchEvent( new CustomEvent('party-select', { detail: { partyId: this.selectedPartyId }, bubbles: true, composed: true, }) ); } private handleAddRole() { this.showRoleEditor = true; // TODO: Open role editor modal } private handleAddParty() { this.showPartyEditor = true; // TODO: Open party editor modal } private handleRemoveParty(partyId: string) { if (!this.contract) return; const updatedParties = this.contract.involvedParties.filter((p) => p.partyId !== partyId); this.handleFieldChange('involvedParties', updatedParties); } // ============================================================================ // HELPERS // ============================================================================ private getPartyDisplayName(party: plugins.sdInterfaces.IInvolvedParty): string { if (!party) return 'Unknown Party'; const contact = party.contact; if (!contact) return party.deliveryEmail || 'Unknown Party'; if ('name' in contact && contact.name) { return contact.name as string; } if ('firstName' in contact && 'lastName' in contact) { return `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || party.deliveryEmail || 'Unknown Party'; } return party.deliveryEmail || 'Unknown Party'; } private getPartyInitials(party: plugins.sdInterfaces.IInvolvedParty): string { const name = this.getPartyDisplayName(party); if (!name || name.length === 0) return '??'; const parts = name.split(' '); if (parts.length >= 2 && parts[0].length > 0 && parts[parts.length - 1].length > 0) { return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); } return name.substring(0, Math.min(2, name.length)).toUpperCase(); } private getPartyColor(party: plugins.sdInterfaces.IInvolvedParty): string { // Generate a consistent color based on party ID const colors = [ '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1', ]; const idStr = party?.partyId || 'default'; const hash = idStr.split('').reduce((a, b) => a + b.charCodeAt(0), 0); return colors[hash % colors.length]; } private getRoleName(roleId: string): string { if (!roleId) return 'Unknown Role'; const role = this.contract?.availableRoles.find((r) => r.id === roleId); return role?.name || roleId.charAt(0).toUpperCase() + roleId.slice(1); } private getSignatureStatusClass(status: string): string { if (status === 'signed') return 'signed'; if (status === 'declined') return 'declined'; return 'pending'; } private formatSignatureStatus(status: string): string { return status.charAt(0).toUpperCase() + status.slice(1); } // ============================================================================ // RENDER // ============================================================================ public render(): TemplateResult { if (!this.contract) { return html`
No contract loaded
`; } const roles = this.contract.availableRoles; const parties = this.contract.involvedParties; return html`
Available Roles
${!this.readonly ? html` ` : ''}
${roles.length > 0 ? html`
${roles.map((role) => this.renderRoleCard(role))}
` : html`

No Roles Defined

Add roles to define the types of parties in this contract

`}
Involved Parties (${parties.length})
${!this.readonly ? html` ` : ''}
${parties.length > 0 ? html`
${parties.map((party) => this.renderPartyCard(party))}
` : html`

No Parties Added

Add parties who will be involved in this contract

`}
`; } private renderRoleCard(role: plugins.sdInterfaces.IRole): TemplateResult { return html`
${role.name}
${role.category}
${role.description || 'No description'}
${role.signatureRequired ? html` Signature required ` : ''} ${role.defaultSigningOrder > 0 ? html` Order: ${role.defaultSigningOrder} ` : ''} ${role.minParties ? html` Min: ${role.minParties}${role.maxParties ? `, Max: ${role.maxParties}` : ''} ` : ''}
`; } private renderPartyCard(party: plugins.sdInterfaces.IInvolvedParty): TemplateResult { // Handle both full IInvolvedParty and minimal demo data const partyId = (party as any).partyId || (party as any).role || 'unknown'; const roleId = (party as any).roleId || (party as any).role || ''; const partyRole = (party as any).partyRole || 'signer'; const signatureStatus = (party as any).signature?.status || 'pending'; const signingOrder = (party as any).signingOrder || 0; const deliveryEmail = (party as any).deliveryEmail; const deliveryPhone = (party as any).deliveryPhone; const actingAsProxy = (party as any).actingAsProxy; const isSelected = this.selectedPartyId === partyId; return html`
this.handleSelectParty(partyId)} >
${this.getPartyInitials(party)}
${this.getPartyDisplayName(party)}
${this.getRoleName(roleId)} (${PARTY_ROLES.find((r) => r.value === partyRole)?.label || partyRole})
${deliveryEmail ? html`
${deliveryEmail}
` : ''} ${deliveryPhone ? html`
${deliveryPhone}
` : ''} ${actingAsProxy ? html`
Acting as proxy
` : ''}
${this.formatSignatureStatus(signatureStatus)} ${signingOrder > 0 ? html`
${signingOrder} Signing order
` : ''}
`; } }