/** * @file sdig-contract-attachments.ts * @description Contract attachments and prior contracts manager */ 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-attachments': SdigContractAttachments; } } // Attachment interface interface IAttachment { id: string; name: string; type: 'document' | 'image' | 'spreadsheet' | 'pdf' | 'other'; mimeType: string; size: number; uploadedAt: number; uploadedBy: string; description?: string; url?: string; } // File type configuration const FILE_TYPES = { document: { icon: 'lucide:FileText', color: '#3b82f6', label: 'Document' }, image: { icon: 'lucide:Image', color: '#10b981', label: 'Image' }, spreadsheet: { icon: 'lucide:Sheet', color: '#22c55e', label: 'Spreadsheet' }, pdf: { icon: 'lucide:FileType', color: '#ef4444', label: 'PDF' }, other: { icon: 'lucide:File', color: '#6b7280', label: 'File' }, }; @customElement('sdig-contract-attachments') export class SdigContractAttachments extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .attachments-container { display: flex; flex-direction: column; gap: 24px; } /* 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-count { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .section-content { padding: 20px; } /* Upload zone */ .upload-zone { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 12px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; cursor: pointer; transition: all 0.15s ease; } .upload-zone:hover { border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')}; background: ${cssManager.bdTheme('#f3f4f6', '#18181b')}; } .upload-zone.dragging { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; background: ${cssManager.bdTheme('#eff6ff', '#172554')}; } .upload-zone-icon { width: 64px; height: 64px; border-radius: 16px; display: flex; align-items: center; justify-content: center; font-size: 28px; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; margin-bottom: 16px; } .upload-zone-title { font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 8px; } .upload-zone-subtitle { font-size: 14px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; margin-bottom: 16px; } .upload-zone-hint { font-size: 12px; color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; } /* Attachments list */ .attachments-list { display: flex; flex-direction: column; gap: 12px; } .attachment-item { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; transition: all 0.15s ease; } .attachment-item:hover { border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; } .attachment-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; } .attachment-info { flex: 1; min-width: 0; } .attachment-name { font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .attachment-meta { display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .attachment-meta-item { display: flex; align-items: center; gap: 4px; } .attachment-actions { display: flex; gap: 4px; flex-shrink: 0; } /* Prior contracts */ .prior-contracts-list { display: flex; flex-direction: column; gap: 12px; } .prior-contract-item { display: flex; align-items: center; gap: 14px; padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; cursor: pointer; transition: all 0.15s ease; } .prior-contract-item:hover { border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; background: ${cssManager.bdTheme('#f3f4f6', '#18181b')}; } .prior-contract-icon { width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 22px; background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; flex-shrink: 0; } .prior-contract-info { flex: 1; min-width: 0; } .prior-contract-title { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin-bottom: 4px; } .prior-contract-context { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .prior-contract-actions { display: flex; gap: 8px; flex-shrink: 0; } /* 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; } /* Storage summary */ .storage-summary { display: flex; align-items: center; gap: 16px; padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; margin-bottom: 20px; } .storage-info { flex: 1; } .storage-label { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; margin-bottom: 6px; } .storage-bar { height: 6px; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; border-radius: 3px; overflow: hidden; } .storage-fill { height: 100%; background: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; transition: width 0.3s ease; } .storage-text { font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; white-space: nowrap; } /* 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-danger { color: ${cssManager.bdTheme('#dc2626', '#f87171')}; } .btn-danger:hover { background: ${cssManager.bdTheme('#fef2f2', '#450a0a')}; } /* 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')}; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; @property({ type: Boolean }) public accessor readonly: boolean = false; // ============================================================================ // STATE // ============================================================================ @state() private accessor isDragging: boolean = false; // Demo attachments data @state() private accessor attachments: IAttachment[] = [ { id: '1', name: 'Employment_Terms_v2.pdf', type: 'pdf', mimeType: 'application/pdf', size: 245760, uploadedAt: Date.now() - 86400000 * 3, uploadedBy: 'employer', description: 'Original employment terms document', }, { id: '2', name: 'ID_Verification.png', type: 'image', mimeType: 'image/png', size: 1024000, uploadedAt: Date.now() - 86400000, uploadedBy: 'employee', }, { id: '3', name: 'Tax_Information.xlsx', type: 'spreadsheet', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 52480, uploadedAt: Date.now() - 86400000 * 2, uploadedBy: 'employer', }, ]; // ============================================================================ // EVENT HANDLERS // ============================================================================ private handleFieldChange(path: string, value: unknown) { this.dispatchEvent( new CustomEvent('field-change', { detail: { path, value }, bubbles: true, composed: true, }) ); } private handleDragEnter(e: DragEvent) { e.preventDefault(); e.stopPropagation(); this.isDragging = true; } private handleDragLeave(e: DragEvent) { e.preventDefault(); e.stopPropagation(); this.isDragging = false; } private handleDragOver(e: DragEvent) { e.preventDefault(); e.stopPropagation(); } private handleDrop(e: DragEvent) { e.preventDefault(); e.stopPropagation(); this.isDragging = false; const files = e.dataTransfer?.files; if (files && files.length > 0) { this.handleFiles(files); } } private handleFileSelect() { const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.onchange = () => { if (input.files && input.files.length > 0) { this.handleFiles(input.files); } }; input.click(); } private handleFiles(files: FileList) { // Demo: just add to list Array.from(files).forEach((file) => { const newAttachment: IAttachment = { id: `att-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: file.name, type: this.getFileType(file.type), mimeType: file.type, size: file.size, uploadedAt: Date.now(), uploadedBy: 'user', }; this.attachments = [...this.attachments, newAttachment]; }); } private handleDeleteAttachment(attachmentId: string) { this.attachments = this.attachments.filter((a) => a.id !== attachmentId); } private handleAddPriorContract() { // TODO: Open prior contract picker modal } private handleRemovePriorContract(index: number) { if (!this.contract) return; const updatedPriorContracts = [...this.contract.priorContracts]; updatedPriorContracts.splice(index, 1); this.handleFieldChange('priorContracts', updatedPriorContracts); } // ============================================================================ // HELPERS // ============================================================================ private getFileType(mimeType: string): IAttachment['type'] { if (mimeType.includes('pdf')) return 'pdf'; if (mimeType.includes('image')) return 'image'; if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'spreadsheet'; if (mimeType.includes('document') || mimeType.includes('word')) return 'document'; return 'other'; } private getFileTypeConfig(type: IAttachment['type']) { return FILE_TYPES[type] || FILE_TYPES.other; } private formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } private formatDate(timestamp: number): string { return new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); } private getTotalSize(): number { return this.attachments.reduce((sum, a) => sum + a.size, 0); } private getPartyName(roleId: string): string { const role = this.contract?.availableRoles.find((r) => r.id === roleId); return role?.name || roleId; } // ============================================================================ // RENDER // ============================================================================ public render(): TemplateResult { if (!this.contract) { return html`
No contract loaded
`; } const totalSize = this.getTotalSize(); const maxSize = 50 * 1024 * 1024; // 50MB demo limit const usagePercent = Math.min((totalSize / maxSize) * 100, 100); return html`
Attachments
${this.attachments.length} files
Storage used
${this.formatFileSize(totalSize)} / ${this.formatFileSize(maxSize)}
${!this.readonly ? html`
Drop files here or click to upload
Add supporting documents, images, or spreadsheets
PDF, DOCX, XLSX, PNG, JPG up to 10MB each
` : ''} ${this.attachments.length > 0 ? html`
${this.attachments.map((attachment) => this.renderAttachmentItem(attachment))}
` : html`

No Attachments

Upload files to attach them to this contract

`}
Prior Contracts
${!this.readonly ? html` ` : ''}
${this.contract.priorContracts.length > 0 ? html`
${this.contract.priorContracts.map((priorContract, index) => this.renderPriorContractItem(priorContract, index) )}
` : html`

No Prior Contracts

Link related or predecessor contracts here

`}
`; } private renderAttachmentItem(attachment: IAttachment): TemplateResult { const typeConfig = this.getFileTypeConfig(attachment.type); return html`
${attachment.name}
${typeConfig.label} ${this.formatFileSize(attachment.size)} ${this.formatDate(attachment.uploadedAt)} ${this.getPartyName(attachment.uploadedBy)}
${!this.readonly ? html` ` : ''}
`; } private renderPriorContractItem(priorContract: plugins.sdInterfaces.IPortableContract, index: number): TemplateResult { return html`
${priorContract.title}
${priorContract.context || 'No description'}
${!this.readonly ? html` ` : ''}
`; } }