/**
* @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:check-square', 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')}
${this.paymentSchedule.length > 0
? html`
| Description |
Amount |
Due Date |
Status |
${!this.readonly ? html` | ` : ''}
${this.paymentSchedule.map(
(payment) => html`
| ${payment.description} |
${this.formatCurrency(payment.amount, payment.currency)} |
${this.formatDate(payment.dueDate)} |
${payment.status} |
${!this.readonly
? html`
|
`
: ''}
`
)}
`
: 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}%
${this.milestones.length > 0
? html`
| Milestone |
Description |
Due Date |
Status |
${!this.readonly ? html` | ` : ''}
${this.milestones.map(
(milestone) => html`
| ${milestone.name} |
${milestone.description || '—'} |
${this.formatDate(milestone.dueDate)} |
${milestone.status.replace('_', ' ')} |
${!this.readonly
? html`
|
`
: ''}
`
)}
`
: 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.
${this.obligations.length > 0
? html`
| Obligation |
Responsible Party |
Deadline |
Status |
${!this.readonly ? html` | ` : ''}
${this.obligations.map(
(obligation) => html`
| ${obligation.description} |
${this.getPartyName(obligation.responsibleParty)} |
${this.formatDate(obligation.deadline)} |
${obligation.status} |
${!this.readonly
? html`
|
`
: ''}
`
)}
`
: html`
No Obligations
Add obligations to track party responsibilities
`}
`;
}
}