/** * @file sdig-contract-signatures.ts * @description Contract signature fields manager 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-signatures': SdigContractSignatures; } } // Signature field interface (for future interface updates) interface ISignatureField { id: string; name: string; assignedPartyId: string | null; roleId: string; type: 'signature' | 'initials' | 'date' | 'text'; required: boolean; status: 'pending' | 'ready' | 'signed' | 'declined'; signedAt?: number; signatureData?: any; position: { paragraphId?: string; pageNumber?: number; x: number; y: number; }; } // Signature status configuration const SIGNATURE_STATUSES = [ { value: 'pending', label: 'Pending', color: '#f59e0b', icon: 'lucide:clock' }, { value: 'ready', label: 'Ready to Sign', color: '#3b82f6', icon: 'lucide:pen-tool' }, { value: 'signed', label: 'Signed', color: '#10b981', icon: 'lucide:check-circle' }, { value: 'declined', label: 'Declined', color: '#ef4444', icon: 'lucide:x-circle' }, ]; const FIELD_TYPES = [ { value: 'signature', label: 'Full Signature', icon: 'lucide:pen-tool' }, { value: 'initials', label: 'Initials', icon: 'lucide:type' }, { value: 'date', label: 'Date', icon: 'lucide:calendar' }, { value: 'text', label: 'Text Field', icon: 'lucide:text-cursor' }, ]; @customElement('sdig-contract-signatures') export class SdigContractSignatures extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .signatures-container { display: flex; flex-direction: column; gap: 24px; } /* Summary cards */ .summary-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; } .summary-card { display: flex; flex-direction: column; gap: 8px; padding: 20px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 12px; } .summary-card-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; } .summary-card-icon.pending { background: ${cssManager.bdTheme('#fef3c7', '#422006')}; color: ${cssManager.bdTheme('#f59e0b', '#fcd34d')}; } .summary-card-icon.ready { background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; } .summary-card-icon.signed { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#10b981', '#34d399')}; } .summary-card-icon.declined { background: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; color: ${cssManager.bdTheme('#ef4444', '#f87171')}; } .summary-card-value { font-size: 28px; font-weight: 700; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .summary-card-label { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } /* Section card */ .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; } /* Signature fields list */ .fields-list { display: flex; flex-direction: column; gap: 12px; } .field-card { display: flex; align-items: center; gap: 16px; padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; transition: all 0.15s ease; } .field-card:hover { border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; } .field-card.selected { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; background: ${cssManager.bdTheme('#eff6ff', '#172554')}; } .field-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; flex-shrink: 0; } .field-info { flex: 1; min-width: 0; } .field-name { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 4px; } .field-meta { display: flex; flex-wrap: wrap; gap: 12px; font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .field-meta-item { display: flex; align-items: center; gap: 4px; } .field-meta-item dees-icon { font-size: 14px; } .field-status { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 9999px; font-size: 12px; font-weight: 500; } .field-status.pending { background: ${cssManager.bdTheme('#fef3c7', '#422006')}; color: ${cssManager.bdTheme('#92400e', '#fcd34d')}; } .field-status.ready { background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; } .field-status.signed { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#065f46', '#6ee7b7')}; } .field-status.declined { background: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; color: ${cssManager.bdTheme('#991b1b', '#fca5a5')}; } .field-actions { display: flex; gap: 8px; flex-shrink: 0; } /* Signer progress */ .signers-section { margin-top: 24px; } .signers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; } .signer-card { display: flex; align-items: center; gap: 14px; padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; } .signer-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; } .signer-info { flex: 1; min-width: 0; } .signer-name { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 4px; } .signer-role { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; margin-bottom: 8px; } .signer-progress { display: flex; align-items: center; gap: 8px; } .progress-bar { flex: 1; height: 6px; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; border-radius: 3px; overflow: hidden; } .progress-fill { height: 100%; background: ${cssManager.bdTheme('#10b981', '#34d399')}; transition: width 0.3s ease; } .progress-text { font-size: 12px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; min-width: 36px; text-align: right; } /* Signature preview */ .signature-preview { position: relative; padding: 24px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; text-align: center; } .signature-preview-label { font-size: 12px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; margin-bottom: 12px; } .signature-preview-image { max-width: 200px; max-height: 80px; margin: 0 auto; } .signature-preview-placeholder { display: flex; flex-direction: column; align-items: center; gap: 8px; color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; } .signature-preview-placeholder dees-icon { font-size: 32px; opacity: 0.5; } /* Empty state */ .empty-state { display: flex; flex-direction: column; align-items: center; padding: 48px 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 0 20px; font-size: 14px; } /* Buttons */ .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')}; padding: 6px; } .btn-ghost:hover { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .btn-success { background: ${cssManager.bdTheme('#10b981', '#059669')}; color: white; } .btn-success:hover { background: ${cssManager.bdTheme('#059669', '#047857')}; } /* Type badge */ .type-badge { 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')}; } /* Signing order */ .signing-order-badge { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; font-size: 12px; font-weight: 600; background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; @property({ type: Boolean }) public accessor readonly: boolean = false; // ============================================================================ // STATE // ============================================================================ @state() private accessor selectedFieldId: string | null = null; // Demo signature fields data @state() private accessor signatureFields: ISignatureField[] = []; // ============================================================================ // LIFECYCLE // ============================================================================ public async firstUpdated() { // Generate demo signature fields based on contract parties if (this.contract && this.contract.involvedParties.length > 0) { this.signatureFields = this.contract.involvedParties.map((party, index) => ({ id: `sig-${index + 1}`, name: `Signature - ${this.getPartyRoleName(party.role)}`, assignedPartyId: null, roleId: party.role, type: 'signature' as const, required: true, status: index === 0 ? 'signed' : index === 1 ? 'ready' : 'pending', signedAt: index === 0 ? Date.now() - 86400000 : undefined, position: { x: 0, y: 0 }, })); } } // ============================================================================ // EVENT HANDLERS // ============================================================================ private handleFieldChange(path: string, value: unknown) { this.dispatchEvent( new CustomEvent('field-change', { detail: { path, value }, bubbles: true, composed: true, }) ); } private handleSelectField(fieldId: string) { this.selectedFieldId = this.selectedFieldId === fieldId ? null : fieldId; } private handleAddField() { const newField: ISignatureField = { id: `sig-${Date.now()}`, name: 'New Signature Field', assignedPartyId: null, roleId: '', type: 'signature', required: true, status: 'pending', position: { x: 0, y: 0 }, }; this.signatureFields = [...this.signatureFields, newField]; } private handleDeleteField(fieldId: string) { this.signatureFields = this.signatureFields.filter((f) => f.id !== fieldId); if (this.selectedFieldId === fieldId) { this.selectedFieldId = null; } } // ============================================================================ // HELPERS // ============================================================================ private getPartyRoleName(roleId: string): string { const role = this.contract?.availableRoles.find((r) => r.id === roleId); return role?.name || roleId; } private getStatusConfig(status: string) { return SIGNATURE_STATUSES.find((s) => s.value === status) || SIGNATURE_STATUSES[0]; } private getFieldTypeConfig(type: string) { return FIELD_TYPES.find((t) => t.value === type) || FIELD_TYPES[0]; } private getSignatureStats() { const total = this.signatureFields.length; const signed = this.signatureFields.filter((f) => f.status === 'signed').length; const ready = this.signatureFields.filter((f) => f.status === 'ready').length; const pending = this.signatureFields.filter((f) => f.status === 'pending').length; const declined = this.signatureFields.filter((f) => f.status === 'declined').length; return { total, signed, ready, pending, declined }; } private getPartyColor(index: number): string { const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']; return colors[index % colors.length]; } private formatDate(timestamp: number): string { return new Date(timestamp).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } // ============================================================================ // RENDER // ============================================================================ public render(): TemplateResult { if (!this.contract) { return html`
No contract loaded
`; } const stats = this.getSignatureStats(); return html`
${stats.pending}
Pending
${stats.ready}
Ready to Sign
${stats.signed}
Signed
${stats.declined}
Declined
Signature Fields
${!this.readonly ? html` ` : ''}
${this.signatureFields.length > 0 ? html`
${this.signatureFields.map((field, index) => this.renderSignatureField(field, index))}
` : html`

No Signature Fields

Add signature fields to define where parties should sign the contract

${!this.readonly ? html` ` : ''}
`}
${this.contract.involvedParties.length > 0 ? html`
Signers Progress
${this.contract.involvedParties.map((party, index) => this.renderSignerCard(party, index))}
` : ''}
`; } private renderSignatureField(field: ISignatureField, index: number): TemplateResult { const isSelected = this.selectedFieldId === field.id; const statusConfig = this.getStatusConfig(field.status); const typeConfig = this.getFieldTypeConfig(field.type); return html`
this.handleSelectField(field.id)} >
${index + 1}
${field.name}
${this.getPartyRoleName(field.roleId)} ${typeConfig.label} ${field.required ? html` Required ` : ''} ${field.signedAt ? html` ${this.formatDate(field.signedAt)} ` : ''}
${statusConfig.label}
${!this.readonly ? html`
` : ''}
`; } private renderSignerCard(party: plugins.sdInterfaces.IInvolvedParty, index: number): TemplateResult { const partyFields = this.signatureFields.filter((f) => f.roleId === party.role); const signedFields = partyFields.filter((f) => f.status === 'signed').length; const totalFields = partyFields.length; const progress = totalFields > 0 ? Math.round((signedFields / totalFields) * 100) : 0; const roleName = this.getPartyRoleName(party.role); return html`
${roleName.charAt(0).toUpperCase()}
${roleName}
${signedFields} of ${totalFields} signatures
${progress}%
`; } }