/** * @file sdig-contract-audit.ts * @description Contract audit log and lifecycle history 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-audit': SdigContractAudit; } } // Audit event interface interface IAuditEvent { id: string; timestamp: number; type: 'created' | 'updated' | 'status_change' | 'signature' | 'comment' | 'attachment' | 'viewed' | 'shared'; userId: string; userName: string; userColor: string; description: string; details?: { field?: string; oldValue?: string; newValue?: string; attachmentName?: string; signatureStatus?: string; }; } // Status workflow configuration const STATUS_WORKFLOW = [ { id: 'draft', label: 'Draft', icon: 'lucide:FileEdit', color: '#f59e0b' }, { id: 'review', label: 'Review', icon: 'lucide:Eye', color: '#3b82f6' }, { id: 'pending', label: 'Pending Signatures', icon: 'lucide:PenTool', color: '#8b5cf6' }, { id: 'signed', label: 'Signed', icon: 'lucide:CheckCircle', color: '#10b981' }, { id: 'executed', label: 'Executed', icon: 'lucide:ShieldCheck', color: '#059669' }, ]; // Event type configuration const EVENT_TYPES = { created: { icon: 'lucide:PlusCircle', color: '#10b981', label: 'Created' }, updated: { icon: 'lucide:Pencil', color: '#3b82f6', label: 'Updated' }, status_change: { icon: 'lucide:ArrowRightCircle', color: '#8b5cf6', label: 'Status Changed' }, signature: { icon: 'lucide:PenTool', color: '#10b981', label: 'Signature' }, comment: { icon: 'lucide:MessageCircle', color: '#f59e0b', label: 'Comment' }, attachment: { icon: 'lucide:Paperclip', color: '#6366f1', label: 'Attachment' }, viewed: { icon: 'lucide:Eye', color: '#6b7280', label: 'Viewed' }, shared: { icon: 'lucide:Share2', color: '#ec4899', label: 'Shared' }, }; @customElement('sdig-contract-audit') export class SdigContractAudit extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .audit-container { display: flex; flex-direction: column; gap: 24px; } /* Lifecycle status */ .lifecycle-card { background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 12px; padding: 24px; } .lifecycle-title { font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 20px; } .status-workflow { display: flex; align-items: center; gap: 8px; overflow-x: auto; padding-bottom: 8px; } .status-step { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 100px; } .status-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; transition: all 0.2s ease; } .status-step.completed .status-icon { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#059669', '#34d399')}; } .status-step.current .status-icon { background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; box-shadow: 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(96, 165, 250, 0.2)')}; } .status-label { font-size: 12px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; text-align: center; } .status-step.completed .status-label, .status-step.current .status-label { color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .status-connector { flex: 1; height: 2px; background: ${cssManager.bdTheme('#e5e5e5', '#27272a')}; min-width: 40px; } .status-connector.completed { background: ${cssManager.bdTheme('#10b981', '#34d399')}; } /* 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; } /* Filter controls */ .filter-row { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; } .filter-select { padding: 8px 12px; font-size: 13px; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 6px; outline: none; cursor: pointer; } .search-input { flex: 1; padding: 8px 12px; font-size: 13px; color: ${cssManager.bdTheme('#111111', '#fafafa')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 6px; outline: none; } .search-input:focus { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; } /* Timeline */ .timeline { position: relative; padding-left: 32px; } .timeline::before { content: ''; position: absolute; left: 11px; top: 0; bottom: 0; width: 2px; background: ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .timeline-item { position: relative; padding-bottom: 24px; } .timeline-item:last-child { padding-bottom: 0; } .timeline-dot { position: absolute; left: -32px; top: 0; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 2px solid; } .timeline-content { padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; } .timeline-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .timeline-title { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .timeline-time { font-size: 12px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .timeline-user { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .timeline-avatar { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: white; } .timeline-username { font-size: 13px; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .timeline-description { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .timeline-details { margin-top: 10px; padding: 10px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border-radius: 6px; font-size: 12px; font-family: 'Roboto Mono', monospace; } .detail-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } .detail-row:last-child { margin-bottom: 0; } .detail-label { font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .detail-value { color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .detail-old { text-decoration: line-through; color: ${cssManager.bdTheme('#ef4444', '#f87171')}; } .detail-new { color: ${cssManager.bdTheme('#10b981', '#34d399')}; } /* Event type badge */ .event-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; } /* Stats row */ .stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; } .stat-card { padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; text-align: center; } .stat-value { font-size: 28px; font-weight: 700; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 4px; } .stat-label { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } /* 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; } /* 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-secondary { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .btn-secondary:hover { background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')}; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; // ============================================================================ // STATE // ============================================================================ @state() private accessor filterType: string = 'all'; @state() private accessor searchQuery: string = ''; // Demo audit events @state() private accessor auditEvents: IAuditEvent[] = [ { id: '1', timestamp: Date.now() - 3600000, type: 'signature', userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', description: 'Signed the contract', details: { signatureStatus: 'completed' }, }, { id: '2', timestamp: Date.now() - 7200000, type: 'status_change', userId: '2', userName: 'Bob Johnson', userColor: '#10b981', description: 'Changed status from Review to Pending Signatures', details: { field: 'status', oldValue: 'review', newValue: 'pending' }, }, { id: '3', timestamp: Date.now() - 86400000, type: 'updated', userId: '2', userName: 'Bob Johnson', userColor: '#10b981', description: 'Updated compensation amount', details: { field: 'paragraphs.2.content', oldValue: '[Salary Amount]', newValue: '€520/month' }, }, { id: '4', timestamp: Date.now() - 86400000 * 2, type: 'comment', userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', description: 'Added a comment on Compensation section', }, { id: '5', timestamp: Date.now() - 86400000 * 3, type: 'attachment', userId: '3', userName: 'Carol Davis', userColor: '#f59e0b', description: 'Uploaded ID verification document', details: { attachmentName: 'ID_Verification.pdf' }, }, { id: '6', timestamp: Date.now() - 86400000 * 5, type: 'created', userId: '2', userName: 'Bob Johnson', userColor: '#10b981', description: 'Created the contract', }, ]; // ============================================================================ // HELPERS // ============================================================================ private getEventConfig(type: IAuditEvent['type']) { return EVENT_TYPES[type] || EVENT_TYPES.updated; } private getFilteredEvents(): IAuditEvent[] { let events = this.auditEvents; if (this.filterType !== 'all') { events = events.filter((e) => e.type === this.filterType); } if (this.searchQuery) { const query = this.searchQuery.toLowerCase(); events = events.filter( (e) => e.description.toLowerCase().includes(query) || e.userName.toLowerCase().includes(query) ); } return events; } private formatDate(timestamp: number): string { return new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } private formatTimeAgo(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } private getCurrentStatusIndex(): number { // Demo: Return a fixed position return 2; // Pending Signatures } private getEventStats() { const total = this.auditEvents.length; const updates = this.auditEvents.filter((e) => e.type === 'updated').length; const signatures = this.auditEvents.filter((e) => e.type === 'signature').length; const comments = this.auditEvents.filter((e) => e.type === 'comment').length; return { total, updates, signatures, comments }; } // ============================================================================ // RENDER // ============================================================================ public render(): TemplateResult { if (!this.contract) { return html`
No contract loaded
`; } const currentStatusIndex = this.getCurrentStatusIndex(); const filteredEvents = this.getFilteredEvents(); const stats = this.getEventStats(); return html`
Contract Lifecycle
${STATUS_WORKFLOW.map((status, index) => html`
${status.label}
${index < STATUS_WORKFLOW.length - 1 ? html`
` : ''} `)}
${stats.total}
Total Events
${stats.updates}
Updates
${stats.signatures}
Signatures
${stats.comments}
Comments
Activity Log
(this.searchQuery = (e.target as HTMLInputElement).value)} />
${filteredEvents.length > 0 ? html`
${filteredEvents.map((event) => this.renderTimelineItem(event))}
` : html`

No Events Found

No activity matches your current filters

`}
`; } private renderTimelineItem(event: IAuditEvent): TemplateResult { const config = this.getEventConfig(event.type); return html`
${config.label}
${this.formatTimeAgo(event.timestamp)}
${event.userName.charAt(0)}
${event.userName}
${event.description}
${event.details ? html`
${event.details.field ? html`
Field: ${event.details.field}
` : ''} ${event.details.oldValue && event.details.newValue ? html`
${event.details.oldValue} ${event.details.newValue}
` : ''} ${event.details.attachmentName ? html`
File: ${event.details.attachmentName}
` : ''}
` : ''}
`; } }