/**
* @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:file-text', color: '#3b82f6', label: 'Document' },
image: { icon: 'lucide:image', color: '#10b981', label: 'Image' },
spreadsheet: { icon: 'lucide:sheet', color: '#22c55e', label: 'Spreadsheet' },
pdf: { icon: 'lucide:file-type', 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`
${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
`}
${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`
`
: ''}
`;
}
}