/** * @file sdig-contract-terms.ts * @description Contract terms editor - tabbed container for financial, time, and obligation terms */ 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-terms': SdigContractTerms; } } // Term types type TTermTab = 'financial' | 'time' | 'obligations'; interface ITermTabConfig { id: TTermTab; label: string; icon: string; description: string; } const TERM_TABS: ITermTabConfig[] = [ { id: 'financial', label: 'Financial Terms', icon: 'lucide:Banknote', description: 'Payment schedules, rates, and penalties' }, { id: 'time', label: 'Time Terms', icon: 'lucide:Calendar', description: 'Milestones, deadlines, and renewal' }, { id: 'obligations', label: 'Obligations', icon: 'lucide:CheckSquare', description: 'Deliverables, SLAs, and warranties' }, ]; // Extended contract terms interfaces (for future interface updates) interface IPaymentScheduleItem { id: string; description: string; amount: number; currency: string; dueDate: string; status: 'pending' | 'paid' | 'overdue'; } interface IMilestone { id: string; name: string; description: string; dueDate: string; status: 'pending' | 'in_progress' | 'completed' | 'delayed'; dependencies: string[]; } interface IObligation { id: string; description: string; responsibleParty: string; deadline: string; status: 'pending' | 'completed' | 'waived'; } @customElement('sdig-contract-terms') export class SdigContractTerms extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .terms-container { display: flex; flex-direction: column; gap: 16px; } /* Section card */ .section-card { background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 12px; overflow: hidden; } /* Tab navigation */ .tabs-nav { display: flex; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; } .tab-btn { display: flex; align-items: center; gap: 8px; padding: 16px 24px; font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; transition: all 0.15s ease; } .tab-btn:hover { color: ${cssManager.bdTheme('#374151', '#d1d5db')}; background: ${cssManager.bdTheme('#f3f4f6', '#18181b')}; } .tab-btn.active { color: ${cssManager.bdTheme('#111111', '#fafafa')}; border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; } .tab-btn dees-icon { font-size: 16px; } /* Tab content */ .tab-content { padding: 24px; } /* Sub-sections */ .sub-section { margin-bottom: 24px; } .sub-section:last-child { margin-bottom: 0; } .sub-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .sub-section-title { font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .sub-section-description { font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; margin-top: 4px; } /* Form groups */ .form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 16px; } .form-group { display: flex; flex-direction: column; gap: 6px; } .form-label { font-size: 13px; font-weight: 500; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .form-input { padding: 10px 12px; font-size: 14px; color: ${cssManager.bdTheme('#111111', '#fafafa')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 6px; outline: none; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .form-input:focus { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')}; } .form-input::placeholder { color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; } select.form-input { cursor: pointer; } /* Data table */ .data-table { width: 100%; border-collapse: collapse; font-size: 14px; } .data-table th { text-align: left; padding: 12px 16px; font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .data-table td { padding: 12px 16px; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .data-table tr:last-child td { border-bottom: none; } .data-table tr:hover td { background: ${cssManager.bdTheme('#f9fafb', '#111111')}; } /* Status badges */ .status-badge { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 9999px; font-size: 12px; font-weight: 500; } .status-badge.pending { background: ${cssManager.bdTheme('#fef3c7', '#422006')}; color: ${cssManager.bdTheme('#92400e', '#fcd34d')}; } .status-badge.paid, .status-badge.completed { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#065f46', '#6ee7b7')}; } .status-badge.overdue, .status-badge.delayed { background: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; color: ${cssManager.bdTheme('#991b1b', '#fca5a5')}; } .status-badge.in_progress { background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; } /* Amount display */ .amount { font-weight: 600; font-family: 'Roboto Mono', monospace; } .amount.positive { color: ${cssManager.bdTheme('#059669', '#34d399')}; } .amount.negative { color: ${cssManager.bdTheme('#dc2626', '#f87171')}; } /* Summary card */ .summary-card { display: flex; gap: 32px; padding: 20px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border-radius: 8px; margin-bottom: 24px; } .summary-item { display: flex; flex-direction: column; gap: 4px; } .summary-label { font-size: 12px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; text-transform: uppercase; letter-spacing: 0.05em; } .summary-value { font-size: 24px; font-weight: 700; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .summary-value.currency { font-family: 'Roboto Mono', monospace; } /* 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')}; } .btn-ghost:hover { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } /* Add row button */ .add-row { display: flex; justify-content: center; padding: 16px; border-top: 1px dashed ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } /* Info banner */ .info-banner { display: flex; align-items: flex-start; gap: 12px; padding: 16px; background: ${cssManager.bdTheme('#eff6ff', '#172554')}; border: 1px solid ${cssManager.bdTheme('#bfdbfe', '#1e40af')}; border-radius: 8px; margin-bottom: 24px; } .info-banner dees-icon { font-size: 20px; color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; flex-shrink: 0; } .info-banner-content { flex: 1; } .info-banner-title { font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; margin-bottom: 4px; } .info-banner-text { font-size: 13px; color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; @property({ type: Boolean }) public accessor readonly: boolean = false; // ============================================================================ // STATE // ============================================================================ @state() private accessor activeTab: TTermTab = 'financial'; // Demo data for terms (will be replaced with actual contract data when interface is extended) @state() private accessor paymentSchedule: IPaymentScheduleItem[] = [ { id: '1', description: 'Initial deposit', amount: 5000, currency: 'EUR', dueDate: '2024-02-01', status: 'paid' }, { id: '2', description: 'Monthly payment - March', amount: 1000, currency: 'EUR', dueDate: '2024-03-01', status: 'paid' }, { id: '3', description: 'Monthly payment - April', amount: 1000, currency: 'EUR', dueDate: '2024-04-01', status: 'pending' }, ]; @state() private accessor milestones: IMilestone[] = [ { id: '1', name: 'Project Kickoff', description: 'Initial planning and setup', dueDate: '2024-02-15', status: 'completed', dependencies: [] }, { id: '2', name: 'Phase 1 Delivery', description: 'First deliverable milestone', dueDate: '2024-03-15', status: 'in_progress', dependencies: ['1'] }, { id: '3', name: 'Final Delivery', description: 'Complete project delivery', dueDate: '2024-05-01', status: 'pending', dependencies: ['2'] }, ]; @state() private accessor obligations: IObligation[] = [ { id: '1', description: 'Provide access credentials', responsibleParty: 'employer', deadline: '2024-02-01', status: 'completed' }, { id: '2', description: 'Submit monthly reports', responsibleParty: 'employee', deadline: '2024-03-01', status: 'pending' }, { id: '3', description: 'Conduct quarterly review', responsibleParty: 'employer', deadline: '2024-04-01', status: 'pending' }, ]; // ============================================================================ // EVENT HANDLERS // ============================================================================ private handleFieldChange(path: string, value: unknown) { this.dispatchEvent( new CustomEvent('field-change', { detail: { path, value }, bubbles: true, composed: true, }) ); } private handleTabChange(tab: TTermTab) { this.activeTab = tab; } private handleAddPayment() { const newPayment: IPaymentScheduleItem = { id: `pay-${Date.now()}`, description: 'New payment', amount: 0, currency: 'EUR', dueDate: new Date().toISOString().split('T')[0], status: 'pending', }; this.paymentSchedule = [...this.paymentSchedule, newPayment]; } private handleAddMilestone() { const newMilestone: IMilestone = { id: `ms-${Date.now()}`, name: 'New Milestone', description: '', dueDate: new Date().toISOString().split('T')[0], status: 'pending', dependencies: [], }; this.milestones = [...this.milestones, newMilestone]; } private handleAddObligation() { const newObligation: IObligation = { id: `obl-${Date.now()}`, description: 'New obligation', responsibleParty: '', deadline: new Date().toISOString().split('T')[0], status: 'pending', }; this.obligations = [...this.obligations, newObligation]; } // ============================================================================ // HELPERS // ============================================================================ private formatCurrency(amount: number, currency: string): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency, }).format(amount); } private formatDate(dateStr: string): string { return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); } private getTotalAmount(): number { return this.paymentSchedule.reduce((sum, p) => sum + p.amount, 0); } private getPaidAmount(): number { return this.paymentSchedule.filter((p) => p.status === 'paid').reduce((sum, p) => sum + p.amount, 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
`; } return html`
${this.activeTab === 'financial' ? this.renderFinancialTerms() : this.activeTab === 'time' ? this.renderTimeTerms() : this.renderObligations()}
`; } private renderFinancialTerms(): TemplateResult { const totalAmount = this.getTotalAmount(); const paidAmount = this.getPaidAmount(); const pendingAmount = totalAmount - paidAmount; return html`
Total Value ${this.formatCurrency(totalAmount, 'EUR')}
Paid ${this.formatCurrency(paidAmount, 'EUR')}
Pending ${this.formatCurrency(pendingAmount, 'EUR')}
Payment Schedule
Scheduled payments and their status
${!this.readonly ? html` ` : ''}
${this.paymentSchedule.length > 0 ? html` ${!this.readonly ? html`` : ''} ${this.paymentSchedule.map( (payment) => html` ${!this.readonly ? html` ` : ''} ` )}
Description Amount Due Date Status
${payment.description} ${this.formatCurrency(payment.amount, payment.currency)} ${this.formatDate(payment.dueDate)} ${payment.status}
` : html`

No Payment Schedule

Add payment terms to track financial obligations

`}
`; } private renderTimeTerms(): TemplateResult { const completedCount = this.milestones.filter((m) => m.status === 'completed').length; const totalCount = this.milestones.length; return html`
Total Milestones ${totalCount}
Completed ${completedCount}
Progress ${totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0}%
Milestones
Key project milestones and deadlines
${!this.readonly ? html` ` : ''}
${this.milestones.length > 0 ? html` ${!this.readonly ? html`` : ''} ${this.milestones.map( (milestone) => html` ${!this.readonly ? html` ` : ''} ` )}
Milestone Description Due Date Status
${milestone.name} ${milestone.description || '—'} ${this.formatDate(milestone.dueDate)} ${milestone.status.replace('_', ' ')}
` : html`

No Milestones

Add milestones to track project progress

`}
`; } private renderObligations(): TemplateResult { const completedCount = this.obligations.filter((o) => o.status === 'completed').length; return html`
Total Obligations ${this.obligations.length}
Completed ${completedCount}
Pending ${this.obligations.length - completedCount}
Contractual Obligations
Track responsibilities assigned to each party. Mark obligations as completed when fulfilled.
Party Obligations
Responsibilities and deliverables by party
${!this.readonly ? html` ` : ''}
${this.obligations.length > 0 ? html` ${!this.readonly ? html`` : ''} ${this.obligations.map( (obligation) => html` ${!this.readonly ? html` ` : ''} ` )}
Obligation Responsible Party Deadline Status
${obligation.description} ${this.getPartyName(obligation.responsibleParty)} ${this.formatDate(obligation.deadline)} ${obligation.status}
` : html`

No Obligations

Add obligations to track party responsibilities

`}
`; } }