feat(catalog): add ContractEditor and many editor subcomponents; implement SignPad and SignBox; update README and bump dependencies
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@signature.digital/catalog',
|
||||
version: '1.0.59',
|
||||
version: '1.1.0',
|
||||
description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.'
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
export * from './sdig-contracteditor.js';
|
||||
export * from './sdig-signbox.js';
|
||||
export * from './sdig-signpad.js';
|
||||
// Contract Editor (main module)
|
||||
export * from './sdig-contracteditor/index.js';
|
||||
|
||||
// Contract sub-components
|
||||
export * from './sdig-contract-header/index.js';
|
||||
export * from './sdig-contract-metadata/index.js';
|
||||
export * from './sdig-contract-parties/index.js';
|
||||
export * from './sdig-contract-content/index.js';
|
||||
export * from './sdig-contract-terms/index.js';
|
||||
export * from './sdig-contract-signatures/index.js';
|
||||
export * from './sdig-contract-attachments/index.js';
|
||||
export * from './sdig-contract-collaboration/index.js';
|
||||
export * from './sdig-contract-audit/index.js';
|
||||
|
||||
// Signature components
|
||||
export * from './sdig-signbox/index.js';
|
||||
export * from './sdig-signpad/index.js';
|
||||
|
||||
1
ts_web/elements/sdig-contract-attachments/index.ts
Normal file
1
ts_web/elements/sdig-contract-attachments/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-attachments.js';
|
||||
@@ -0,0 +1,806 @@
|
||||
/**
|
||||
* @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`
|
||||
<sdig-contract-attachments
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-attachments>
|
||||
`;
|
||||
|
||||
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`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
const totalSize = this.getTotalSize();
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB demo limit
|
||||
const usagePercent = Math.min((totalSize / maxSize) * 100, 100);
|
||||
|
||||
return html`
|
||||
<div class="attachments-container">
|
||||
<!-- Attachments Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:paperclip'}></dees-icon>
|
||||
Attachments
|
||||
</div>
|
||||
<span class="section-count">${this.attachments.length} files</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<!-- Storage summary -->
|
||||
<div class="storage-summary">
|
||||
<div class="storage-info">
|
||||
<div class="storage-label">Storage used</div>
|
||||
<div class="storage-bar">
|
||||
<div class="storage-fill" style="width: ${usagePercent}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="storage-text">
|
||||
${this.formatFileSize(totalSize)} / ${this.formatFileSize(maxSize)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload zone -->
|
||||
${!this.readonly
|
||||
? html`
|
||||
<div
|
||||
class="upload-zone ${this.isDragging ? 'dragging' : ''}"
|
||||
@dragenter=${this.handleDragEnter}
|
||||
@dragleave=${this.handleDragLeave}
|
||||
@dragover=${this.handleDragOver}
|
||||
@drop=${this.handleDrop}
|
||||
@click=${this.handleFileSelect}
|
||||
>
|
||||
<div class="upload-zone-icon">
|
||||
<dees-icon .iconFA=${'lucide:upload-cloud'}></dees-icon>
|
||||
</div>
|
||||
<div class="upload-zone-title">Drop files here or click to upload</div>
|
||||
<div class="upload-zone-subtitle">Add supporting documents, images, or spreadsheets</div>
|
||||
<div class="upload-zone-hint">PDF, DOCX, XLSX, PNG, JPG up to 10MB each</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<!-- Attachments list -->
|
||||
${this.attachments.length > 0
|
||||
? html`
|
||||
<div class="attachments-list" style="margin-top: 20px;">
|
||||
${this.attachments.map((attachment) => this.renderAttachmentItem(attachment))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state" style="margin-top: 20px;">
|
||||
<dees-icon .iconFA=${'lucide:file-x'}></dees-icon>
|
||||
<h4>No Attachments</h4>
|
||||
<p>Upload files to attach them to this contract</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prior Contracts Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:files'}></dees-icon>
|
||||
Prior Contracts
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-secondary" @click=${this.handleAddPriorContract}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Link Contract
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="section-content">
|
||||
${this.contract.priorContracts.length > 0
|
||||
? html`
|
||||
<div class="prior-contracts-list">
|
||||
${this.contract.priorContracts.map((priorContract, index) =>
|
||||
this.renderPriorContractItem(priorContract, index)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:link'}></dees-icon>
|
||||
<h4>No Prior Contracts</h4>
|
||||
<p>Link related or predecessor contracts here</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAttachmentItem(attachment: IAttachment): TemplateResult {
|
||||
const typeConfig = this.getFileTypeConfig(attachment.type);
|
||||
|
||||
return html`
|
||||
<div class="attachment-item">
|
||||
<div class="attachment-icon" style="background: ${typeConfig.color}20; color: ${typeConfig.color}">
|
||||
<dees-icon .iconFA=${typeConfig.icon}></dees-icon>
|
||||
</div>
|
||||
<div class="attachment-info">
|
||||
<div class="attachment-name">${attachment.name}</div>
|
||||
<div class="attachment-meta">
|
||||
<span class="type-badge">${typeConfig.label}</span>
|
||||
<span class="attachment-meta-item">
|
||||
${this.formatFileSize(attachment.size)}
|
||||
</span>
|
||||
<span class="attachment-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:calendar'}></dees-icon>
|
||||
${this.formatDate(attachment.uploadedAt)}
|
||||
</span>
|
||||
<span class="attachment-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:user'}></dees-icon>
|
||||
${this.getPartyName(attachment.uploadedBy)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachment-actions">
|
||||
<button class="btn btn-ghost" title="Download">
|
||||
<dees-icon .iconFA=${'lucide:download'}></dees-icon>
|
||||
</button>
|
||||
<button class="btn btn-ghost" title="Preview">
|
||||
<dees-icon .iconFA=${'lucide:eye'}></dees-icon>
|
||||
</button>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-ghost btn-danger"
|
||||
title="Delete"
|
||||
@click=${() => this.handleDeleteAttachment(attachment.id)}
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:trash-2'}></dees-icon>
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPriorContractItem(priorContract: plugins.sdInterfaces.IPortableContract, index: number): TemplateResult {
|
||||
return html`
|
||||
<div class="prior-contract-item">
|
||||
<div class="prior-contract-icon">
|
||||
<dees-icon .iconFA=${'lucide:file-text'}></dees-icon>
|
||||
</div>
|
||||
<div class="prior-contract-info">
|
||||
<div class="prior-contract-title">${priorContract.title}</div>
|
||||
<div class="prior-contract-context">${priorContract.context || 'No description'}</div>
|
||||
</div>
|
||||
<div class="prior-contract-actions">
|
||||
<button class="btn btn-secondary btn-sm">
|
||||
<dees-icon .iconFA=${'lucide:external-link'}></dees-icon>
|
||||
View
|
||||
</button>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-ghost btn-danger"
|
||||
@click=${() => this.handleRemovePriorContract(index)}
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:unlink'}></dees-icon>
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-audit/index.ts
Normal file
1
ts_web/elements/sdig-contract-audit/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-audit.js';
|
||||
772
ts_web/elements/sdig-contract-audit/sdig-contract-audit.ts
Normal file
772
ts_web/elements/sdig-contract-audit/sdig-contract-audit.ts
Normal file
@@ -0,0 +1,772 @@
|
||||
/**
|
||||
* @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:file-edit', color: '#f59e0b' },
|
||||
{ id: 'review', label: 'Review', icon: 'lucide:eye', color: '#3b82f6' },
|
||||
{ id: 'pending', label: 'Pending Signatures', icon: 'lucide:pen-tool', color: '#8b5cf6' },
|
||||
{ id: 'signed', label: 'Signed', icon: 'lucide:check-circle', color: '#10b981' },
|
||||
{ id: 'executed', label: 'Executed', icon: 'lucide:shield-check', color: '#059669' },
|
||||
];
|
||||
|
||||
// Event type configuration
|
||||
const EVENT_TYPES = {
|
||||
created: { icon: 'lucide:plus-circle', color: '#10b981', label: 'Created' },
|
||||
updated: { icon: 'lucide:pencil', color: '#3b82f6', label: 'Updated' },
|
||||
status_change: { icon: 'lucide:arrow-right-circle', color: '#8b5cf6', label: 'Status Changed' },
|
||||
signature: { icon: 'lucide:pen-tool', color: '#10b981', label: 'Signature' },
|
||||
comment: { icon: 'lucide:message-circle', color: '#f59e0b', label: 'Comment' },
|
||||
attachment: { icon: 'lucide:paperclip', color: '#6366f1', label: 'Attachment' },
|
||||
viewed: { icon: 'lucide:eye', color: '#6b7280', label: 'Viewed' },
|
||||
shared: { icon: 'lucide:share-2', color: '#ec4899', label: 'Shared' },
|
||||
};
|
||||
|
||||
@customElement('sdig-contract-audit')
|
||||
export class SdigContractAudit extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contract-audit
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-audit>
|
||||
`;
|
||||
|
||||
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`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
const currentStatusIndex = this.getCurrentStatusIndex();
|
||||
const filteredEvents = this.getFilteredEvents();
|
||||
const stats = this.getEventStats();
|
||||
|
||||
return html`
|
||||
<div class="audit-container">
|
||||
<!-- Lifecycle Status -->
|
||||
<div class="lifecycle-card">
|
||||
<div class="lifecycle-title">Contract Lifecycle</div>
|
||||
<div class="status-workflow">
|
||||
${STATUS_WORKFLOW.map((status, index) => html`
|
||||
<div class="status-step ${index < currentStatusIndex ? 'completed' : ''} ${index === currentStatusIndex ? 'current' : ''}">
|
||||
<div class="status-icon">
|
||||
<dees-icon .iconFA=${status.icon}></dees-icon>
|
||||
</div>
|
||||
<div class="status-label">${status.label}</div>
|
||||
</div>
|
||||
${index < STATUS_WORKFLOW.length - 1
|
||||
? html`<div class="status-connector ${index < currentStatusIndex ? 'completed' : ''}"></div>`
|
||||
: ''}
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${stats.total}</div>
|
||||
<div class="stat-label">Total Events</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${stats.updates}</div>
|
||||
<div class="stat-label">Updates</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${stats.signatures}</div>
|
||||
<div class="stat-label">Signatures</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">${stats.comments}</div>
|
||||
<div class="stat-label">Comments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Log -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:history'}></dees-icon>
|
||||
Activity Log
|
||||
</div>
|
||||
<button class="btn btn-secondary">
|
||||
<dees-icon .iconFA=${'lucide:download'}></dees-icon>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<!-- Filters -->
|
||||
<div class="filter-row">
|
||||
<select
|
||||
class="filter-select"
|
||||
.value=${this.filterType}
|
||||
@change=${(e: Event) => (this.filterType = (e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="all">All Events</option>
|
||||
${Object.entries(EVENT_TYPES).map(
|
||||
([key, config]) => html`<option value=${key}>${config.label}</option>`
|
||||
)}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search events..."
|
||||
.value=${this.searchQuery}
|
||||
@input=${(e: Event) => (this.searchQuery = (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
${filteredEvents.length > 0
|
||||
? html`
|
||||
<div class="timeline">
|
||||
${filteredEvents.map((event) => this.renderTimelineItem(event))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:clock'}></dees-icon>
|
||||
<h4>No Events Found</h4>
|
||||
<p>No activity matches your current filters</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTimelineItem(event: IAuditEvent): TemplateResult {
|
||||
const config = this.getEventConfig(event.type);
|
||||
|
||||
return html`
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot" style="border-color: ${config.color}; color: ${config.color}">
|
||||
<dees-icon .iconFA=${config.icon}></dees-icon>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-header">
|
||||
<div class="timeline-title">
|
||||
<span class="event-badge" style="background: ${config.color}20; color: ${config.color}">
|
||||
${config.label}
|
||||
</span>
|
||||
</div>
|
||||
<span class="timeline-time" title="${this.formatDate(event.timestamp)}">
|
||||
${this.formatTimeAgo(event.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="timeline-user">
|
||||
<div class="timeline-avatar" style="background: ${event.userColor}">
|
||||
${event.userName.charAt(0)}
|
||||
</div>
|
||||
<span class="timeline-username">${event.userName}</span>
|
||||
</div>
|
||||
<div class="timeline-description">${event.description}</div>
|
||||
${event.details
|
||||
? html`
|
||||
<div class="timeline-details">
|
||||
${event.details.field
|
||||
? html`
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Field:</span>
|
||||
<span class="detail-value">${event.details.field}</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${event.details.oldValue && event.details.newValue
|
||||
? html`
|
||||
<div class="detail-row">
|
||||
<span class="detail-old">${event.details.oldValue}</span>
|
||||
<span>→</span>
|
||||
<span class="detail-new">${event.details.newValue}</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${event.details.attachmentName
|
||||
? html`
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">File:</span>
|
||||
<span class="detail-value">${event.details.attachmentName}</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-collaboration/index.ts
Normal file
1
ts_web/elements/sdig-contract-collaboration/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-collaboration.js';
|
||||
@@ -0,0 +1,972 @@
|
||||
/**
|
||||
* @file sdig-contract-collaboration.ts
|
||||
* @description Contract collaboration - comments, suggestions, and presence
|
||||
*/
|
||||
|
||||
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-collaboration': SdigContractCollaboration;
|
||||
}
|
||||
}
|
||||
|
||||
// Comment interface
|
||||
interface IComment {
|
||||
id: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userColor: string;
|
||||
content: string;
|
||||
createdAt: number;
|
||||
updatedAt?: number;
|
||||
anchorPath?: string;
|
||||
anchorText?: string;
|
||||
resolved: boolean;
|
||||
replies: IComment[];
|
||||
}
|
||||
|
||||
// Suggestion interface
|
||||
interface ISuggestion {
|
||||
id: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userColor: string;
|
||||
originalText: string;
|
||||
suggestedText: string;
|
||||
path: string;
|
||||
status: 'pending' | 'accepted' | 'rejected';
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
// Presence interface
|
||||
interface IPresence {
|
||||
userId: string;
|
||||
userName: string;
|
||||
userColor: string;
|
||||
currentSection: string;
|
||||
cursorPosition?: { path: string; offset: number };
|
||||
lastActive: number;
|
||||
}
|
||||
|
||||
@customElement('sdig-contract-collaboration')
|
||||
export class SdigContractCollaboration extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contract-collaboration
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-collaboration>
|
||||
`;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.collaboration-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Presence bar */
|
||||
.presence-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.presence-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.presence-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.presence-avatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.presence-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-left: -8px;
|
||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.presence-avatar:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.presence-avatar .status-dot {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
}
|
||||
|
||||
.presence-avatar .status-dot.away {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.presence-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-left: -8px;
|
||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.share-btn:hover {
|
||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
||||
}
|
||||
|
||||
/* 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-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Comments list */
|
||||
.comments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.comment-thread {
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.comment-thread.resolved {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.comment-anchor {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.comment-anchor:hover {
|
||||
background: ${cssManager.bdTheme('#fde68a', '#713f12')};
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.comment-replies {
|
||||
margin-top: 16px;
|
||||
padding-left: 16px;
|
||||
border-left: 2px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
}
|
||||
|
||||
.reply-item {
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.reply-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Suggestions list */
|
||||
.suggestions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.suggestion-card {
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.suggestion-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.suggestion-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.suggestion-status.pending {
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
||||
}
|
||||
|
||||
.suggestion-status.accepted {
|
||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
||||
}
|
||||
|
||||
.suggestion-status.rejected {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
||||
}
|
||||
|
||||
.suggestion-diff {
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 8px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.diff-removed {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
||||
text-decoration: line-through;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.diff-added {
|
||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.suggestion-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* New comment input */
|
||||
.new-comment {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.new-comment-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
resize: none;
|
||||
min-height: 80px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.new-comment-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)')};
|
||||
}
|
||||
|
||||
.new-comment-input::placeholder {
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Filter tabs */
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.filter-tab:hover {
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
}
|
||||
|
||||
/* 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')};
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: ${cssManager.bdTheme('#10b981', '#059669')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: ${cssManager.bdTheme('#059669', '#047857')};
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: ${cssManager.bdTheme('#fecaca', '#7f1d1d')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 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: 'comments' | 'suggestions' = 'comments';
|
||||
|
||||
@state()
|
||||
private accessor commentFilter: 'all' | 'open' | 'resolved' = 'all';
|
||||
|
||||
@state()
|
||||
private accessor newCommentText: string = '';
|
||||
|
||||
// Demo presence data
|
||||
@state()
|
||||
private accessor presenceList: IPresence[] = [
|
||||
{ userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', currentSection: 'content', lastActive: Date.now() },
|
||||
{ userId: '2', userName: 'Bob Johnson', userColor: '#10b981', currentSection: 'parties', lastActive: Date.now() - 60000 },
|
||||
{ userId: '3', userName: 'Carol Davis', userColor: '#f59e0b', currentSection: 'terms', lastActive: Date.now() - 300000 },
|
||||
];
|
||||
|
||||
// Demo comments data
|
||||
@state()
|
||||
private accessor comments: IComment[] = [
|
||||
{
|
||||
id: '1',
|
||||
userId: '1',
|
||||
userName: 'Alice Smith',
|
||||
userColor: '#3b82f6',
|
||||
content: 'Can we clarify the payment terms in paragraph 3? The current wording seems ambiguous.',
|
||||
createdAt: Date.now() - 3600000,
|
||||
anchorPath: 'paragraphs.2',
|
||||
anchorText: 'Compensation',
|
||||
resolved: false,
|
||||
replies: [
|
||||
{
|
||||
id: '1-1',
|
||||
userId: '2',
|
||||
userName: 'Bob Johnson',
|
||||
userColor: '#10b981',
|
||||
content: 'Good point. I\'ll update the wording to be more specific.',
|
||||
createdAt: Date.now() - 1800000,
|
||||
resolved: false,
|
||||
replies: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
userId: '3',
|
||||
userName: 'Carol Davis',
|
||||
userColor: '#f59e0b',
|
||||
content: 'The termination clause needs to comply with the latest regulations.',
|
||||
createdAt: Date.now() - 86400000,
|
||||
resolved: true,
|
||||
replies: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Demo suggestions data
|
||||
@state()
|
||||
private accessor suggestions: ISuggestion[] = [
|
||||
{
|
||||
id: '1',
|
||||
userId: '1',
|
||||
userName: 'Alice Smith',
|
||||
userColor: '#3b82f6',
|
||||
originalText: 'monthly salary',
|
||||
suggestedText: 'monthly gross salary',
|
||||
path: 'paragraphs.2.content',
|
||||
status: 'pending',
|
||||
createdAt: Date.now() - 7200000,
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
private handleFieldChange(path: string, value: unknown) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('field-change', {
|
||||
detail: { path, value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleAddComment() {
|
||||
if (!this.newCommentText.trim()) return;
|
||||
|
||||
const newComment: IComment = {
|
||||
id: `comment-${Date.now()}`,
|
||||
userId: 'current-user',
|
||||
userName: 'You',
|
||||
userColor: '#6366f1',
|
||||
content: this.newCommentText,
|
||||
createdAt: Date.now(),
|
||||
resolved: false,
|
||||
replies: [],
|
||||
};
|
||||
|
||||
this.comments = [newComment, ...this.comments];
|
||||
this.newCommentText = '';
|
||||
}
|
||||
|
||||
private handleResolveComment(commentId: string) {
|
||||
this.comments = this.comments.map((c) =>
|
||||
c.id === commentId ? { ...c, resolved: !c.resolved } : c
|
||||
);
|
||||
}
|
||||
|
||||
private handleAcceptSuggestion(suggestionId: string) {
|
||||
this.suggestions = this.suggestions.map((s) =>
|
||||
s.id === suggestionId ? { ...s, status: 'accepted' as const } : s
|
||||
);
|
||||
}
|
||||
|
||||
private handleRejectSuggestion(suggestionId: string) {
|
||||
this.suggestions = this.suggestions.map((s) =>
|
||||
s.id === suggestionId ? { ...s, status: 'rejected' as const } : s
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
private getFilteredComments(): IComment[] {
|
||||
if (this.commentFilter === 'all') return this.comments;
|
||||
if (this.commentFilter === 'open') return this.comments.filter((c) => !c.resolved);
|
||||
return this.comments.filter((c) => c.resolved);
|
||||
}
|
||||
|
||||
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 getActivePresence(): IPresence[] {
|
||||
const fiveMinutesAgo = Date.now() - 300000;
|
||||
return this.presenceList.filter((p) => p.lastActive > fiveMinutesAgo);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
const activePresence = this.getActivePresence();
|
||||
const openComments = this.comments.filter((c) => !c.resolved).length;
|
||||
const pendingSuggestions = this.suggestions.filter((s) => s.status === 'pending').length;
|
||||
|
||||
return html`
|
||||
<div class="collaboration-container">
|
||||
<!-- Presence Bar -->
|
||||
<div class="presence-bar">
|
||||
<div class="presence-info">
|
||||
<span class="presence-label">Currently viewing:</span>
|
||||
<div class="presence-avatars">
|
||||
${activePresence.slice(0, 4).map(
|
||||
(p) => html`
|
||||
<div
|
||||
class="presence-avatar"
|
||||
style="background: ${p.userColor}"
|
||||
title="${p.userName} - ${p.currentSection}"
|
||||
>
|
||||
${p.userName.charAt(0)}
|
||||
<span class="status-dot ${Date.now() - p.lastActive > 60000 ? 'away' : ''}"></span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${activePresence.length > 4
|
||||
? html`<div class="presence-count">+${activePresence.length - 4}</div>`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
<button class="share-btn">
|
||||
<dees-icon .iconFA=${'lucide:share-2'}></dees-icon>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:message-circle'}></dees-icon>
|
||||
Comments
|
||||
${openComments > 0 ? html`<span class="section-badge">${openComments} open</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<!-- Filter tabs -->
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
class="filter-tab ${this.commentFilter === 'all' ? 'active' : ''}"
|
||||
@click=${() => (this.commentFilter = 'all')}
|
||||
>
|
||||
All (${this.comments.length})
|
||||
</button>
|
||||
<button
|
||||
class="filter-tab ${this.commentFilter === 'open' ? 'active' : ''}"
|
||||
@click=${() => (this.commentFilter = 'open')}
|
||||
>
|
||||
Open (${openComments})
|
||||
</button>
|
||||
<button
|
||||
class="filter-tab ${this.commentFilter === 'resolved' ? 'active' : ''}"
|
||||
@click=${() => (this.commentFilter = 'resolved')}
|
||||
>
|
||||
Resolved (${this.comments.length - openComments})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- New comment input -->
|
||||
${!this.readonly
|
||||
? html`
|
||||
<div class="new-comment">
|
||||
<textarea
|
||||
class="new-comment-input"
|
||||
placeholder="Add a comment..."
|
||||
.value=${this.newCommentText}
|
||||
@input=${(e: Event) => (this.newCommentText = (e.target as HTMLTextAreaElement).value)}
|
||||
></textarea>
|
||||
<button class="btn btn-primary" @click=${this.handleAddComment}>
|
||||
<dees-icon .iconFA=${'lucide:send'}></dees-icon>
|
||||
Comment
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<!-- Comments list -->
|
||||
${this.getFilteredComments().length > 0
|
||||
? html`
|
||||
<div class="comments-list" style="margin-top: 16px;">
|
||||
${this.getFilteredComments().map((comment) => this.renderComment(comment))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state" style="margin-top: 16px;">
|
||||
<dees-icon .iconFA=${'lucide:message-square'}></dees-icon>
|
||||
<h4>No Comments</h4>
|
||||
<p>Start a discussion by adding a comment</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Suggestions Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:git-pull-request'}></dees-icon>
|
||||
Suggestions
|
||||
${pendingSuggestions > 0 ? html`<span class="section-badge">${pendingSuggestions} pending</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
${this.suggestions.length > 0
|
||||
? html`
|
||||
<div class="suggestions-list">
|
||||
${this.suggestions.map((suggestion) => this.renderSuggestion(suggestion))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:edit-3'}></dees-icon>
|
||||
<h4>No Suggestions</h4>
|
||||
<p>Suggested changes will appear here</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderComment(comment: IComment): TemplateResult {
|
||||
return html`
|
||||
<div class="comment-thread ${comment.resolved ? 'resolved' : ''}">
|
||||
<div class="comment-header">
|
||||
<div class="comment-avatar" style="background: ${comment.userColor}">
|
||||
${comment.userName.charAt(0)}
|
||||
</div>
|
||||
<div class="comment-meta">
|
||||
<div class="comment-author">${comment.userName}</div>
|
||||
<div class="comment-time">${this.formatTimeAgo(comment.createdAt)}</div>
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
@click=${() => this.handleResolveComment(comment.id)}
|
||||
>
|
||||
<dees-icon .iconFA=${comment.resolved ? 'lucide:rotate-ccw' : 'lucide:check'}></dees-icon>
|
||||
${comment.resolved ? 'Reopen' : 'Resolve'}
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
${comment.anchorText
|
||||
? html`
|
||||
<div class="comment-anchor">
|
||||
<dees-icon .iconFA=${'lucide:link'}></dees-icon>
|
||||
${comment.anchorText}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<div class="comment-content">${comment.content}</div>
|
||||
|
||||
${comment.replies.length > 0
|
||||
? html`
|
||||
<div class="comment-replies">
|
||||
${comment.replies.map(
|
||||
(reply) => html`
|
||||
<div class="reply-item">
|
||||
<div class="comment-header">
|
||||
<div class="comment-avatar" style="background: ${reply.userColor}; width: 28px; height: 28px; font-size: 11px;">
|
||||
${reply.userName.charAt(0)}
|
||||
</div>
|
||||
<div class="comment-meta">
|
||||
<div class="comment-author" style="font-size: 13px;">${reply.userName}</div>
|
||||
<div class="comment-time">${this.formatTimeAgo(reply.createdAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-content" style="font-size: 13px; margin-bottom: 0;">${reply.content}</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSuggestion(suggestion: ISuggestion): TemplateResult {
|
||||
return html`
|
||||
<div class="suggestion-card">
|
||||
<div class="suggestion-header">
|
||||
<div class="suggestion-user">
|
||||
<div class="comment-avatar" style="background: ${suggestion.userColor}; width: 28px; height: 28px; font-size: 11px;">
|
||||
${suggestion.userName.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="comment-author" style="font-size: 13px;">${suggestion.userName}</div>
|
||||
<div class="comment-time">${this.formatTimeAgo(suggestion.createdAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="suggestion-status ${suggestion.status}">
|
||||
<dees-icon .iconFA=${suggestion.status === 'pending' ? 'lucide:clock' : suggestion.status === 'accepted' ? 'lucide:check' : 'lucide:x'}></dees-icon>
|
||||
${suggestion.status.charAt(0).toUpperCase() + suggestion.status.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="suggestion-diff">
|
||||
<span class="diff-removed">${suggestion.originalText}</span>
|
||||
<span> → </span>
|
||||
<span class="diff-added">${suggestion.suggestedText}</span>
|
||||
</div>
|
||||
|
||||
${suggestion.status === 'pending' && !this.readonly
|
||||
? html`
|
||||
<div class="suggestion-actions">
|
||||
<button class="btn btn-success btn-sm" @click=${() => this.handleAcceptSuggestion(suggestion.id)}>
|
||||
<dees-icon .iconFA=${'lucide:check'}></dees-icon>
|
||||
Accept
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" @click=${() => this.handleRejectSuggestion(suggestion.id)}>
|
||||
<dees-icon .iconFA=${'lucide:x'}></dees-icon>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-content/index.ts
Normal file
1
ts_web/elements/sdig-contract-content/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-content.js';
|
||||
920
ts_web/elements/sdig-contract-content/sdig-contract-content.ts
Normal file
920
ts_web/elements/sdig-contract-content/sdig-contract-content.ts
Normal file
@@ -0,0 +1,920 @@
|
||||
/**
|
||||
* @file sdig-contract-content.ts
|
||||
* @description Contract content/paragraphs editor 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-content': SdigContractContent;
|
||||
}
|
||||
}
|
||||
|
||||
// Paragraph type configuration
|
||||
const PARAGRAPH_TYPES = [
|
||||
{ value: 'section', label: 'Section', icon: 'lucide:heading' },
|
||||
{ value: 'clause', label: 'Clause', icon: 'lucide:file-text' },
|
||||
{ value: 'definition', label: 'Definition', icon: 'lucide:book-open' },
|
||||
{ value: 'obligation', label: 'Obligation', icon: 'lucide:check-square' },
|
||||
{ value: 'condition', label: 'Condition', icon: 'lucide:git-branch' },
|
||||
{ value: 'schedule', label: 'Schedule', icon: 'lucide:calendar' },
|
||||
];
|
||||
|
||||
@customElement('sdig-contract-content')
|
||||
export class SdigContractContent extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contract-content
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-content>
|
||||
`;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.content-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
outline: none;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
}
|
||||
|
||||
.search-box dees-icon {
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
/* 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')};
|
||||
}
|
||||
|
||||
.paragraph-count {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Paragraph list */
|
||||
.paragraphs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.paragraph-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.paragraph-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.paragraph-item:hover {
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
}
|
||||
|
||||
.paragraph-item.selected {
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
border-color: ${cssManager.bdTheme('#bfdbfe', '#1e40af')};
|
||||
}
|
||||
|
||||
.paragraph-item.editing {
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
}
|
||||
|
||||
.paragraph-drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.paragraph-drag-handle:hover {
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.paragraph-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paragraph-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.paragraph-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.paragraph-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.paragraph-title-input {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.paragraph-title-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)')};
|
||||
}
|
||||
|
||||
.paragraph-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')};
|
||||
}
|
||||
|
||||
.paragraph-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.paragraph-body.expanded {
|
||||
-webkit-line-clamp: unset;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.paragraph-body-textarea {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.paragraph-body-textarea: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)')};
|
||||
}
|
||||
|
||||
.paragraph-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.paragraph-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.paragraph-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paragraph-edit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Variable highlighting */
|
||||
.variable {
|
||||
display: inline;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 13px;
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
||||
}
|
||||
|
||||
/* Child paragraphs */
|
||||
.child-paragraphs {
|
||||
margin-left: 48px;
|
||||
border-left: 2px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.child-paragraphs .paragraph-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.child-paragraphs .paragraph-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Add paragraph button */
|
||||
.add-paragraph-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px dashed ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
}
|
||||
|
||||
.add-paragraph-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: transparent;
|
||||
border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
border-radius: 8px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.add-paragraph-btn:hover {
|
||||
border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#7f1d1d')};
|
||||
}
|
||||
|
||||
/* View mode toggle */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.view-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.view-toggle-btn.active {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#3f3f46')};
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.view-toggle-btn dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// PROPERTIES
|
||||
// ============================================================================
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public accessor readonly: boolean = false;
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
@state()
|
||||
private accessor selectedParagraphId: string | null = null;
|
||||
|
||||
@state()
|
||||
private accessor editingParagraphId: string | null = null;
|
||||
|
||||
@state()
|
||||
private accessor searchQuery: string = '';
|
||||
|
||||
@state()
|
||||
private accessor viewMode: 'list' | 'outline' = 'list';
|
||||
|
||||
@state()
|
||||
private accessor expandedParagraphs: Set<string> = new Set();
|
||||
|
||||
// Editing state
|
||||
@state()
|
||||
private accessor editTitle: string = '';
|
||||
|
||||
@state()
|
||||
private accessor editContent: string = '';
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
private handleFieldChange(path: string, value: unknown) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('field-change', {
|
||||
detail: { path, value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleSelectParagraph(paragraphId: string) {
|
||||
this.selectedParagraphId = this.selectedParagraphId === paragraphId ? null : paragraphId;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('paragraph-select', {
|
||||
detail: { paragraphId: this.selectedParagraphId },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleEditParagraph(paragraph: plugins.sdInterfaces.IParagraph) {
|
||||
this.editingParagraphId = paragraph.uniqueId;
|
||||
this.editTitle = paragraph.title;
|
||||
this.editContent = paragraph.content;
|
||||
}
|
||||
|
||||
private handleSaveEdit() {
|
||||
if (!this.contract || !this.editingParagraphId) return;
|
||||
|
||||
const updatedParagraphs = this.contract.paragraphs.map((p) => {
|
||||
if (p.uniqueId === this.editingParagraphId) {
|
||||
return { ...p, title: this.editTitle, content: this.editContent };
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
this.handleFieldChange('paragraphs', updatedParagraphs);
|
||||
this.editingParagraphId = null;
|
||||
this.editTitle = '';
|
||||
this.editContent = '';
|
||||
}
|
||||
|
||||
private handleCancelEdit() {
|
||||
this.editingParagraphId = null;
|
||||
this.editTitle = '';
|
||||
this.editContent = '';
|
||||
}
|
||||
|
||||
private handleAddParagraph(parentId: string | null = null) {
|
||||
if (!this.contract) return;
|
||||
|
||||
const newParagraph: plugins.sdInterfaces.IParagraph = {
|
||||
uniqueId: `p-${Date.now()}`,
|
||||
parent: parentId ? this.contract.paragraphs.find((p) => p.uniqueId === parentId) || null : null,
|
||||
title: 'New Paragraph',
|
||||
content: 'Enter paragraph content here...',
|
||||
};
|
||||
|
||||
const updatedParagraphs = [...this.contract.paragraphs, newParagraph];
|
||||
this.handleFieldChange('paragraphs', updatedParagraphs);
|
||||
|
||||
// Start editing the new paragraph
|
||||
this.handleEditParagraph(newParagraph);
|
||||
}
|
||||
|
||||
private handleDeleteParagraph(paragraphId: string) {
|
||||
if (!this.contract) return;
|
||||
|
||||
// Remove the paragraph and any children
|
||||
const idsToRemove = new Set<string>([paragraphId]);
|
||||
|
||||
// Find child paragraphs recursively
|
||||
const findChildren = (parentId: string) => {
|
||||
this.contract!.paragraphs.forEach((p) => {
|
||||
if (p.parent?.uniqueId === parentId) {
|
||||
idsToRemove.add(p.uniqueId);
|
||||
findChildren(p.uniqueId);
|
||||
}
|
||||
});
|
||||
};
|
||||
findChildren(paragraphId);
|
||||
|
||||
const updatedParagraphs = this.contract.paragraphs.filter((p) => !idsToRemove.has(p.uniqueId));
|
||||
this.handleFieldChange('paragraphs', updatedParagraphs);
|
||||
|
||||
if (this.selectedParagraphId === paragraphId) {
|
||||
this.selectedParagraphId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleMoveParagraph(paragraphId: string, direction: 'up' | 'down') {
|
||||
if (!this.contract) return;
|
||||
|
||||
const paragraphs = [...this.contract.paragraphs];
|
||||
const index = paragraphs.findIndex((p) => p.uniqueId === paragraphId);
|
||||
|
||||
if (index === -1) return;
|
||||
if (direction === 'up' && index === 0) return;
|
||||
if (direction === 'down' && index === paragraphs.length - 1) return;
|
||||
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
[paragraphs[index], paragraphs[newIndex]] = [paragraphs[newIndex], paragraphs[index]];
|
||||
|
||||
this.handleFieldChange('paragraphs', paragraphs);
|
||||
}
|
||||
|
||||
private handleSearchChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.searchQuery = input.value;
|
||||
}
|
||||
|
||||
private toggleExpanded(paragraphId: string) {
|
||||
const expanded = new Set(this.expandedParagraphs);
|
||||
if (expanded.has(paragraphId)) {
|
||||
expanded.delete(paragraphId);
|
||||
} else {
|
||||
expanded.add(paragraphId);
|
||||
}
|
||||
this.expandedParagraphs = expanded;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
private getRootParagraphs(): plugins.sdInterfaces.IParagraph[] {
|
||||
if (!this.contract) return [];
|
||||
return this.contract.paragraphs.filter((p) => !p.parent);
|
||||
}
|
||||
|
||||
private getChildParagraphs(parentId: string): plugins.sdInterfaces.IParagraph[] {
|
||||
if (!this.contract) return [];
|
||||
return this.contract.paragraphs.filter((p) => p.parent?.uniqueId === parentId);
|
||||
}
|
||||
|
||||
private filterParagraphs(paragraphs: plugins.sdInterfaces.IParagraph[]): plugins.sdInterfaces.IParagraph[] {
|
||||
if (!this.searchQuery) return paragraphs;
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
return paragraphs.filter(
|
||||
(p) =>
|
||||
p.title.toLowerCase().includes(query) ||
|
||||
p.content.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
private highlightVariables(content: string): TemplateResult {
|
||||
// Match {{variableName}} patterns
|
||||
const parts = content.split(/(\{\{[^}]+\}\})/g);
|
||||
return html`${parts.map((part) =>
|
||||
part.startsWith('{{') && part.endsWith('}}')
|
||||
? html`<span class="variable">${part}</span>`
|
||||
: part
|
||||
)}`;
|
||||
}
|
||||
|
||||
private getParagraphNumber(paragraph: plugins.sdInterfaces.IParagraph, index: number): string {
|
||||
// Simple numbering - can be enhanced for hierarchical numbering
|
||||
return String(index + 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
const rootParagraphs = this.getRootParagraphs();
|
||||
const filteredParagraphs = this.filterParagraphs(rootParagraphs);
|
||||
|
||||
return html`
|
||||
<div class="content-container">
|
||||
<!-- Toolbar -->
|
||||
<div class="content-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<div class="search-box">
|
||||
<dees-icon .iconFA=${'lucide:search'}></dees-icon>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search paragraphs..."
|
||||
.value=${this.searchQuery}
|
||||
@input=${this.handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-toggle-btn ${this.viewMode === 'list' ? 'active' : ''}"
|
||||
@click=${() => (this.viewMode = 'list')}
|
||||
title="List view"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:list'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="view-toggle-btn ${this.viewMode === 'outline' ? 'active' : ''}"
|
||||
@click=${() => (this.viewMode = 'outline')}
|
||||
title="Outline view"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:layout-list'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary" @click=${() => this.handleAddParagraph()}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Paragraph
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:file-text'}></dees-icon>
|
||||
Contract Content
|
||||
</div>
|
||||
<span class="paragraph-count">${this.contract.paragraphs.length} paragraphs</span>
|
||||
</div>
|
||||
|
||||
<div class="section-content">
|
||||
${filteredParagraphs.length > 0
|
||||
? html`
|
||||
<div class="paragraphs-list">
|
||||
${filteredParagraphs.map((paragraph, index) =>
|
||||
this.renderParagraph(paragraph, index)
|
||||
)}
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<div class="add-paragraph-row">
|
||||
<button class="add-paragraph-btn" @click=${() => this.handleAddParagraph()}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add paragraph
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:file-plus'}></dees-icon>
|
||||
<h4>No Paragraphs Yet</h4>
|
||||
<p>Start building your contract by adding paragraphs. Each paragraph can contain clauses, definitions, or obligations.</p>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary" @click=${() => this.handleAddParagraph()}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add First Paragraph
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderParagraph(paragraph: plugins.sdInterfaces.IParagraph, index: number): TemplateResult {
|
||||
const isSelected = this.selectedParagraphId === paragraph.uniqueId;
|
||||
const isEditing = this.editingParagraphId === paragraph.uniqueId;
|
||||
const isExpanded = this.expandedParagraphs.has(paragraph.uniqueId);
|
||||
const childParagraphs = this.getChildParagraphs(paragraph.uniqueId);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="paragraph-item ${isSelected ? 'selected' : ''} ${isEditing ? 'editing' : ''}"
|
||||
@click=${() => !isEditing && this.handleSelectParagraph(paragraph.uniqueId)}
|
||||
>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<div class="paragraph-drag-handle">
|
||||
<dees-icon .iconFA=${'lucide:grip-vertical'}></dees-icon>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<div class="paragraph-number">${this.getParagraphNumber(paragraph, index)}</div>
|
||||
|
||||
<div class="paragraph-content">
|
||||
${isEditing
|
||||
? html`
|
||||
<input
|
||||
type="text"
|
||||
class="paragraph-title-input"
|
||||
.value=${this.editTitle}
|
||||
@input=${(e: Event) => (this.editTitle = (e.target as HTMLInputElement).value)}
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
placeholder="Paragraph title"
|
||||
/>
|
||||
<textarea
|
||||
class="paragraph-body-textarea"
|
||||
.value=${this.editContent}
|
||||
@input=${(e: Event) => (this.editContent = (e.target as HTMLTextAreaElement).value)}
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
placeholder="Paragraph content..."
|
||||
></textarea>
|
||||
<div class="paragraph-edit-actions">
|
||||
<button class="btn btn-primary" @click=${(e: Event) => { e.stopPropagation(); this.handleSaveEdit(); }}>
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click=${(e: Event) => { e.stopPropagation(); this.handleCancelEdit(); }}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="paragraph-title-row">
|
||||
<span class="paragraph-title">${paragraph.title}</span>
|
||||
</div>
|
||||
<div class="paragraph-body ${isExpanded ? 'expanded' : ''}">${this.highlightVariables(paragraph.content)}</div>
|
||||
${paragraph.content.length > 200
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.toggleExpanded(paragraph.uniqueId); }}
|
||||
style="margin-top: 8px;"
|
||||
>
|
||||
${isExpanded ? 'Show less' : 'Show more'}
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
`}
|
||||
</div>
|
||||
|
||||
${!this.readonly && !isEditing
|
||||
? html`
|
||||
<div class="paragraph-actions">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.handleEditParagraph(paragraph); }}
|
||||
title="Edit"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.handleMoveParagraph(paragraph.uniqueId, 'up'); }}
|
||||
title="Move up"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:chevron-up'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.handleMoveParagraph(paragraph.uniqueId, 'down'); }}
|
||||
title="Move down"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:chevron-down'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-danger"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.handleDeleteParagraph(paragraph.uniqueId); }}
|
||||
title="Delete"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:trash-2'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
${childParagraphs.length > 0
|
||||
? html`
|
||||
<div class="child-paragraphs">
|
||||
${childParagraphs.map((child, childIndex) => this.renderParagraph(child, childIndex))}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-header/index.ts
Normal file
1
ts_web/elements/sdig-contract-header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-header.js';
|
||||
558
ts_web/elements/sdig-contract-header/sdig-contract-header.ts
Normal file
558
ts_web/elements/sdig-contract-header/sdig-contract-header.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* @file sdig-contract-header.ts
|
||||
* @description Contract header component with title, status, and quick actions
|
||||
*/
|
||||
|
||||
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-header': SdigContractHeader;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-contract-header')
|
||||
export class SdigContractHeader extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contract-header
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-header>
|
||||
`;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contract-number {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.title-input:focus {
|
||||
border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.title-input::placeholder {
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
}
|
||||
|
||||
.status-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* shadcn-style badge */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: 1px solid transparent;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status-badge:hover:not(:disabled) {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.status-badge:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.status-badge.draft {
|
||||
background: ${cssManager.bdTheme('hsl(48 96% 89%)', 'hsl(48 96% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(25 95% 30%)', 'hsl(48 96% 70%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(48 96% 76%)', 'hsl(48 96% 25%)')};
|
||||
}
|
||||
|
||||
.status-badge.review {
|
||||
background: ${cssManager.bdTheme('hsl(214 95% 93%)', 'hsl(214 95% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(214 95% 35%)', 'hsl(214 95% 70%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(214 95% 80%)', 'hsl(214 95% 25%)')};
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: ${cssManager.bdTheme('hsl(38 92% 90%)', 'hsl(38 92% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(25 95% 35%)', 'hsl(38 92% 65%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(38 92% 75%)', 'hsl(38 92% 25%)')};
|
||||
}
|
||||
|
||||
.status-badge.signed,
|
||||
.status-badge.active {
|
||||
background: ${cssManager.bdTheme('hsl(142 76% 90%)', 'hsl(142 76% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(142 76% 28%)', 'hsl(142 76% 65%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(142 76% 75%)', 'hsl(142 76% 25%)')};
|
||||
}
|
||||
|
||||
.status-badge.terminated {
|
||||
background: ${cssManager.bdTheme('hsl(0 84% 92%)', 'hsl(0 84% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 84% 35%)', 'hsl(0 84% 65%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 84% 80%)', 'hsl(0 84% 25%)')};
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Meta info row */
|
||||
.meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.meta-value.clickable {
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
|
||||
}
|
||||
|
||||
.meta-value.clickable:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
/* Quick actions */
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
}
|
||||
|
||||
.action-btn dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Status dropdown */
|
||||
.status-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
min-width: 200px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
.status-option:hover {
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
}
|
||||
|
||||
.status-option.selected {
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// PROPERTIES
|
||||
// ============================================================================
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public accessor readonly: boolean = false;
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
@state()
|
||||
private accessor showStatusDropdown: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor editingTitle: boolean = false;
|
||||
|
||||
// ============================================================================
|
||||
// STATUS CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
private statusOptions: Array<{
|
||||
value: plugins.sdInterfaces.TContractStatus;
|
||||
label: string;
|
||||
category: string;
|
||||
}> = [
|
||||
{ value: 'draft', label: 'Draft', category: 'draft' },
|
||||
{ value: 'internal_review', label: 'Internal Review', category: 'review' },
|
||||
{ value: 'legal_review', label: 'Legal Review', category: 'review' },
|
||||
{ value: 'negotiation', label: 'Negotiation', category: 'review' },
|
||||
{ value: 'pending_approval', label: 'Pending Approval', category: 'pending' },
|
||||
{ value: 'pending_signature', label: 'Pending Signature', category: 'pending' },
|
||||
{ value: 'partially_signed', label: 'Partially Signed', category: 'pending' },
|
||||
{ value: 'signed', label: 'Signed', category: 'signed' },
|
||||
{ value: 'executed', label: 'Executed', category: 'signed' },
|
||||
{ value: 'active', label: 'Active', category: 'active' },
|
||||
{ value: 'expired', label: 'Expired', category: 'terminated' },
|
||||
{ value: 'terminated', label: 'Terminated', category: 'terminated' },
|
||||
{ value: 'cancelled', label: 'Cancelled', category: 'terminated' },
|
||||
{ value: 'voided', label: 'Voided', category: 'terminated' },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
private handleTitleChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('field-change', {
|
||||
detail: { path: 'title', value: input.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleStatusChange(status: plugins.sdInterfaces.TContractStatus) {
|
||||
this.showStatusDropdown = false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('field-change', {
|
||||
detail: { path: 'lifecycle.currentStatus', value: status },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private toggleStatusDropdown() {
|
||||
if (!this.readonly) {
|
||||
this.showStatusDropdown = !this.showStatusDropdown;
|
||||
}
|
||||
}
|
||||
|
||||
private handleExport() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('action', {
|
||||
detail: { action: 'export' },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleDuplicate() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('action', {
|
||||
detail: { action: 'duplicate' },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleShare() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('action', {
|
||||
detail: { action: 'share' },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
private getStatusCategory(status: string): string {
|
||||
const option = this.statusOptions.find((o) => o.value === status);
|
||||
return option?.category || 'draft';
|
||||
}
|
||||
|
||||
private formatStatus(status: string): string {
|
||||
const option = this.statusOptions.find((o) => o.value === status);
|
||||
return option?.label || status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
private formatContractType(type: string): string {
|
||||
return type.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.contract) {
|
||||
return html`<div class="header-card">No contract loaded</div>`;
|
||||
}
|
||||
|
||||
const status = this.contract.lifecycle?.currentStatus || 'draft';
|
||||
const statusCategory = this.getStatusCategory(status);
|
||||
|
||||
return html`
|
||||
<div class="header-card">
|
||||
<div class="header-top">
|
||||
<div class="title-section">
|
||||
${this.contract.metadata?.contractNumber
|
||||
? html`<div class="contract-number">#${this.contract.metadata.contractNumber}</div>`
|
||||
: ''}
|
||||
<div class="title-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
class="title-input"
|
||||
.value=${this.contract.title}
|
||||
placeholder="Contract Title"
|
||||
?disabled=${this.readonly}
|
||||
@input=${this.handleTitleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-section">
|
||||
<div style="position: relative;">
|
||||
<button
|
||||
class="status-badge ${statusCategory}"
|
||||
@click=${this.toggleStatusDropdown}
|
||||
?disabled=${this.readonly}
|
||||
>
|
||||
<span class="status-dot"></span>
|
||||
${this.formatStatus(status)}
|
||||
${!this.readonly
|
||||
? html`<dees-icon .iconFA=${'lucide:chevron-down'} style="font-size: 14px;"></dees-icon>`
|
||||
: ''}
|
||||
</button>
|
||||
|
||||
${this.showStatusDropdown
|
||||
? html`
|
||||
<div class="status-dropdown">
|
||||
${this.statusOptions.map(
|
||||
(option) => html`
|
||||
<div
|
||||
class="status-option ${status === option.value ? 'selected' : ''}"
|
||||
@click=${() => this.handleStatusChange(option.value)}
|
||||
>
|
||||
<span
|
||||
class="status-dot"
|
||||
style="background: ${option.category === 'draft'
|
||||
? '#f59e0b'
|
||||
: option.category === 'review'
|
||||
? '#3b82f6'
|
||||
: option.category === 'pending'
|
||||
? '#f59e0b'
|
||||
: option.category === 'signed' || option.category === 'active'
|
||||
? '#10b981'
|
||||
: '#ef4444'}"
|
||||
></span>
|
||||
${option.label}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<button class="action-btn" @click=${this.handleExport} title="Export">
|
||||
<dees-icon .iconFA=${'lucide:download'}></dees-icon>
|
||||
</button>
|
||||
<button class="action-btn" @click=${this.handleDuplicate} title="Duplicate">
|
||||
<dees-icon .iconFA=${'lucide:copy'}></dees-icon>
|
||||
</button>
|
||||
<button class="action-btn" @click=${this.handleShare} title="Share">
|
||||
<dees-icon .iconFA=${'lucide:share-2'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.contract.metadata ? html`
|
||||
<div class="meta-row">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Type</span>
|
||||
<span class="meta-value">${this.formatContractType(this.contract.metadata.contractType)}</span>
|
||||
</div>
|
||||
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Category</span>
|
||||
<span class="meta-value">${this.formatContractType(this.contract.metadata.category)}</span>
|
||||
</div>
|
||||
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Language</span>
|
||||
<span class="meta-value">${this.contract.metadata.language?.toUpperCase() || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Jurisdiction</span>
|
||||
<span class="meta-value">
|
||||
${this.contract.metadata.governingLaw?.country || 'Not specified'}
|
||||
${this.contract.metadata.governingLaw?.state
|
||||
? `, ${this.contract.metadata.governingLaw.state}`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Created</span>
|
||||
<span class="meta-value">${this.formatDate(this.contract.createdAt)}</span>
|
||||
</div>
|
||||
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Parties</span>
|
||||
<span class="meta-value clickable">${this.contract.involvedParties?.length || 0} parties</span>
|
||||
</div>
|
||||
|
||||
${this.contract.metadata.tags?.length > 0
|
||||
? html`
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Tags</span>
|
||||
<div class="tags-container">
|
||||
${this.contract.metadata.tags.map((tag) => html`<span class="tag">${tag}</span>`)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-metadata/index.ts
Normal file
1
ts_web/elements/sdig-contract-metadata/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-metadata.js';
|
||||
820
ts_web/elements/sdig-contract-metadata/sdig-contract-metadata.ts
Normal file
820
ts_web/elements/sdig-contract-metadata/sdig-contract-metadata.ts
Normal file
@@ -0,0 +1,820 @@
|
||||
/**
|
||||
* @file sdig-contract-metadata.ts
|
||||
* @description Contract metadata editor 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-metadata': SdigContractMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe options arrays
|
||||
const CONTRACT_CATEGORIES: Array<{ value: plugins.sdInterfaces.TContractCategory; label: string }> = [
|
||||
{ value: 'employment', label: 'Employment' },
|
||||
{ value: 'service', label: 'Service Agreement' },
|
||||
{ value: 'sales', label: 'Sales' },
|
||||
{ value: 'lease', label: 'Lease / Rental' },
|
||||
{ value: 'license', label: 'License' },
|
||||
{ value: 'partnership', label: 'Partnership' },
|
||||
{ value: 'confidentiality', label: 'Confidentiality / NDA' },
|
||||
{ value: 'financial', label: 'Financial' },
|
||||
{ value: 'real_estate', label: 'Real Estate' },
|
||||
{ value: 'intellectual_property', label: 'Intellectual Property' },
|
||||
{ value: 'government', label: 'Government' },
|
||||
{ value: 'construction', label: 'Construction' },
|
||||
{ value: 'healthcare', label: 'Healthcare' },
|
||||
{ value: 'insurance', label: 'Insurance' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
];
|
||||
|
||||
const CONFIDENTIALITY_LEVELS: Array<{ value: plugins.sdInterfaces.TConfidentialityLevel; label: string }> = [
|
||||
{ value: 'public', label: 'Public' },
|
||||
{ value: 'internal', label: 'Internal' },
|
||||
{ value: 'confidential', label: 'Confidential' },
|
||||
{ value: 'restricted', label: 'Restricted' },
|
||||
];
|
||||
|
||||
const DISPUTE_RESOLUTIONS: Array<{ value: plugins.sdInterfaces.TDisputeResolution; label: string }> = [
|
||||
{ value: 'litigation', label: 'Litigation' },
|
||||
{ value: 'arbitration', label: 'Arbitration' },
|
||||
{ value: 'mediation', label: 'Mediation' },
|
||||
{ value: 'negotiation', label: 'Negotiation' },
|
||||
];
|
||||
|
||||
const COMMON_LANGUAGES = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'de', label: 'German' },
|
||||
{ value: 'fr', label: 'French' },
|
||||
{ value: 'es', label: 'Spanish' },
|
||||
{ value: 'it', label: 'Italian' },
|
||||
{ value: 'pt', label: 'Portuguese' },
|
||||
{ value: 'nl', label: 'Dutch' },
|
||||
{ value: 'pl', label: 'Polish' },
|
||||
{ value: 'sv', label: 'Swedish' },
|
||||
{ value: 'da', label: 'Danish' },
|
||||
{ value: 'fi', label: 'Finnish' },
|
||||
{ value: 'no', label: 'Norwegian' },
|
||||
{ value: 'zh', label: 'Chinese' },
|
||||
{ value: 'ja', label: 'Japanese' },
|
||||
{ value: 'ko', label: 'Korean' },
|
||||
{ value: 'ar', label: 'Arabic' },
|
||||
{ value: 'ru', label: 'Russian' },
|
||||
];
|
||||
|
||||
@customElement('sdig-contract-metadata')
|
||||
export class SdigContractMetadata extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contract-metadata
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-metadata>
|
||||
`;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.metadata-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Section cards */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Form grid */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
.form-label .required {
|
||||
color: #ef4444;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.form-description {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Input styles */
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
border-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(255,255,255,0.05)')};
|
||||
}
|
||||
|
||||
.form-input::placeholder,
|
||||
.form-textarea::placeholder {
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
}
|
||||
|
||||
.form-input:disabled,
|
||||
.form-select:disabled,
|
||||
.form-textarea:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 10px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px;
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Radio group */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.radio-option span {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
/* Tags input */
|
||||
.tags-input-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
min-height: 44px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
border-radius: 8px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.tags-input-container:focus-within {
|
||||
border-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(255,255,255,0.05)')};
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tag-remove:hover {
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.tags-input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tags-input::placeholder {
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.section-divider {
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Collapsible sections */
|
||||
.collapsible-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible-header:hover .collapse-icon {
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.collapse-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.collapsible-content.expanded {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// PROPERTIES
|
||||
// ============================================================================
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public accessor readonly: boolean = false;
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
@state()
|
||||
private accessor showArbitrationFields: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor newTag: string = '';
|
||||
|
||||
// ============================================================================
|
||||
// LIFECYCLE
|
||||
// ============================================================================
|
||||
|
||||
public updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('contract') && this.contract?.metadata?.governingLaw) {
|
||||
this.showArbitrationFields = this.contract.metadata.governingLaw.disputeResolution === 'arbitration';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
private handleFieldChange(path: string, value: unknown) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('field-change', {
|
||||
detail: { path, value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleInputChange(path: string, e: Event) {
|
||||
const input = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
||||
this.handleFieldChange(path, input.value);
|
||||
}
|
||||
|
||||
private handleRadioChange(path: string, value: string) {
|
||||
this.handleFieldChange(path, value);
|
||||
|
||||
// Special handling for dispute resolution
|
||||
if (path === 'metadata.governingLaw.disputeResolution') {
|
||||
this.showArbitrationFields = value === 'arbitration';
|
||||
}
|
||||
}
|
||||
|
||||
private handleTagAdd(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && this.newTag.trim()) {
|
||||
e.preventDefault();
|
||||
const currentTags = this.contract?.metadata.tags || [];
|
||||
if (!currentTags.includes(this.newTag.trim())) {
|
||||
this.handleFieldChange('metadata.tags', [...currentTags, this.newTag.trim()]);
|
||||
}
|
||||
this.newTag = '';
|
||||
}
|
||||
}
|
||||
|
||||
private handleTagRemove(tag: string) {
|
||||
const currentTags = this.contract?.metadata.tags || [];
|
||||
this.handleFieldChange(
|
||||
'metadata.tags',
|
||||
currentTags.filter((t) => t !== tag)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
if (!this.contract.metadata) {
|
||||
return html`<div class="metadata-container"><div class="section-card">Contract metadata not available</div></div>`;
|
||||
}
|
||||
|
||||
const metadata = this.contract.metadata;
|
||||
|
||||
return html`
|
||||
<div class="metadata-container">
|
||||
<!-- Basic Information -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:info'}></dees-icon>
|
||||
Basic Information
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Contract Number</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.contractNumber || ''}
|
||||
placeholder="e.g., CNT-2024-001"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.contractNumber', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Category <span class="required">*</span></label>
|
||||
<select
|
||||
class="form-select"
|
||||
.value=${metadata.category}
|
||||
?disabled=${this.readonly}
|
||||
@change=${(e: Event) => this.handleInputChange('metadata.category', e)}
|
||||
>
|
||||
${CONTRACT_CATEGORIES.map(
|
||||
(cat) => html`
|
||||
<option value=${cat.value} ?selected=${metadata.category === cat.value}>
|
||||
${cat.label}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Contract Type <span class="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.contractType}
|
||||
placeholder="e.g., employment_minijob"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.contractType', e)}
|
||||
/>
|
||||
<p class="form-description">Specific contract type identifier</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Confidentiality Level</label>
|
||||
<div class="radio-group">
|
||||
${CONFIDENTIALITY_LEVELS.map(
|
||||
(level) => html`
|
||||
<label class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="confidentiality"
|
||||
.value=${level.value}
|
||||
?checked=${metadata.confidentialityLevel === level.value}
|
||||
?disabled=${this.readonly}
|
||||
@change=${() => this.handleRadioChange('metadata.confidentialityLevel', level.value)}
|
||||
/>
|
||||
<span>${level.label}</span>
|
||||
</label>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Tags</label>
|
||||
<div class="tags-input-container">
|
||||
${metadata.tags.map(
|
||||
(tag) => html`
|
||||
<span class="tag-item">
|
||||
${tag}
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="tag-remove" @click=${() => this.handleTagRemove(tag)}>×</button>
|
||||
`
|
||||
: ''}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
${!this.readonly
|
||||
? html`
|
||||
<input
|
||||
type="text"
|
||||
class="tags-input"
|
||||
.value=${this.newTag}
|
||||
placeholder="Add tag and press Enter"
|
||||
@input=${(e: Event) => (this.newTag = (e.target as HTMLInputElement).value)}
|
||||
@keydown=${this.handleTagAdd}
|
||||
/>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language Settings -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:globe'}></dees-icon>
|
||||
Language Settings
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Primary Language <span class="required">*</span></label>
|
||||
<select
|
||||
class="form-select"
|
||||
.value=${metadata.language}
|
||||
?disabled=${this.readonly}
|
||||
@change=${(e: Event) => this.handleInputChange('metadata.language', e)}
|
||||
>
|
||||
${COMMON_LANGUAGES.map(
|
||||
(lang) => html`
|
||||
<option value=${lang.value} ?selected=${metadata.language === lang.value}>
|
||||
${lang.label}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Binding Language</label>
|
||||
<select
|
||||
class="form-select"
|
||||
.value=${metadata.bindingLanguage || ''}
|
||||
?disabled=${this.readonly}
|
||||
@change=${(e: Event) => this.handleInputChange('metadata.bindingLanguage', e)}
|
||||
>
|
||||
<option value="">Same as primary</option>
|
||||
${COMMON_LANGUAGES.map(
|
||||
(lang) => html`
|
||||
<option value=${lang.value} ?selected=${metadata.bindingLanguage === lang.value}>
|
||||
${lang.label}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
<p class="form-description">Language that takes precedence in case of conflicts</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Governing Law -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:scale'}></dees-icon>
|
||||
Governing Law & Jurisdiction
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Country <span class="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.governingLaw.country}
|
||||
placeholder="e.g., Germany"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.country', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">State / Province</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.governingLaw.state || ''}
|
||||
placeholder="e.g., Bavaria"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.state', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Dispute Jurisdiction</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.governingLaw.disputeJurisdiction || ''}
|
||||
placeholder="e.g., Munich Courts"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.disputeJurisdiction', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Dispute Resolution Method</label>
|
||||
<div class="radio-group">
|
||||
${DISPUTE_RESOLUTIONS.map(
|
||||
(res) => html`
|
||||
<label class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="disputeResolution"
|
||||
.value=${res.value}
|
||||
?checked=${metadata.governingLaw.disputeResolution === res.value}
|
||||
?disabled=${this.readonly}
|
||||
@change=${() => this.handleRadioChange('metadata.governingLaw.disputeResolution', res.value)}
|
||||
/>
|
||||
<span>${res.label}</span>
|
||||
</label>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.showArbitrationFields
|
||||
? html`
|
||||
<div class="section-divider full-width"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Arbitration Institution</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.governingLaw.arbitrationInstitution || ''}
|
||||
placeholder="e.g., ICC, LCIA, AAA"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.arbitrationInstitution', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Arbitration Rules</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.governingLaw.arbitrationRules || ''}
|
||||
placeholder="e.g., ICC Rules of Arbitration"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.arbitrationRules', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Seat of Arbitration</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.governingLaw.arbitrationSeat || ''}
|
||||
placeholder="e.g., Paris, London"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.arbitrationSeat', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Number of Arbitrators</label>
|
||||
<select
|
||||
class="form-select"
|
||||
.value=${String(metadata.governingLaw.numberOfArbitrators || 1)}
|
||||
?disabled=${this.readonly}
|
||||
@change=${(e: Event) =>
|
||||
this.handleFieldChange(
|
||||
'metadata.governingLaw.numberOfArbitrators',
|
||||
parseInt((e.target as HTMLSelectElement).value, 10)
|
||||
)}
|
||||
>
|
||||
<option value="1">1 Arbitrator</option>
|
||||
<option value="3">3 Arbitrators</option>
|
||||
<option value="5">5 Arbitrators</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Proceedings Language</label>
|
||||
<select
|
||||
class="form-select"
|
||||
.value=${metadata.governingLaw.proceedingsLanguage || ''}
|
||||
?disabled=${this.readonly}
|
||||
@change=${(e: Event) => this.handleInputChange('metadata.governingLaw.proceedingsLanguage', e)}
|
||||
>
|
||||
<option value="">Select language</option>
|
||||
${COMMON_LANGUAGES.map(
|
||||
(lang) => html`
|
||||
<option
|
||||
value=${lang.value}
|
||||
?selected=${metadata.governingLaw.proceedingsLanguage === lang.value}
|
||||
>
|
||||
${lang.label}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- References -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:link'}></dees-icon>
|
||||
References & Integration
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Internal Reference</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.internalReference || ''}
|
||||
placeholder="Internal tracking reference"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.internalReference', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">External Reference</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.externalReference || ''}
|
||||
placeholder="External system reference"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.externalReference', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Template ID</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.templateId || ''}
|
||||
placeholder="Source template ID"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.templateId', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Parent Contract ID</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-input"
|
||||
.value=${metadata.parentContractId || ''}
|
||||
placeholder="Parent/master contract"
|
||||
?disabled=${this.readonly}
|
||||
@input=${(e: Event) => this.handleInputChange('metadata.parentContractId', e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-parties/index.ts
Normal file
1
ts_web/elements/sdig-contract-parties/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-parties.js';
|
||||
736
ts_web/elements/sdig-contract-parties/sdig-contract-parties.ts
Normal file
736
ts_web/elements/sdig-contract-parties/sdig-contract-parties.ts
Normal file
@@ -0,0 +1,736 @@
|
||||
/**
|
||||
* @file sdig-contract-parties.ts
|
||||
* @description Contract parties and roles management 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-parties': SdigContractParties;
|
||||
}
|
||||
}
|
||||
|
||||
// Party role display configuration
|
||||
const PARTY_ROLES: Array<{ value: plugins.sdInterfaces.TPartyRole; label: string; icon: string }> = [
|
||||
{ value: 'signer', label: 'Signer', icon: 'lucide:pen-tool' },
|
||||
{ value: 'witness', label: 'Witness', icon: 'lucide:eye' },
|
||||
{ value: 'notary', label: 'Notary', icon: 'lucide:stamp' },
|
||||
{ value: 'cc', label: 'CC (Copy)', icon: 'lucide:mail' },
|
||||
{ value: 'approver', label: 'Approver', icon: 'lucide:check-circle' },
|
||||
{ value: 'guarantor', label: 'Guarantor', icon: 'lucide:shield' },
|
||||
{ value: 'beneficiary', label: 'Beneficiary', icon: 'lucide:user-check' },
|
||||
];
|
||||
|
||||
const SIGNING_DEPENDENCIES: Array<{ value: plugins.sdInterfaces.TSigningDependency; label: string }> = [
|
||||
{ value: 'none', label: 'No dependency' },
|
||||
{ value: 'sequential', label: 'Sequential (in order)' },
|
||||
{ value: 'parallel', label: 'Parallel (any order)' },
|
||||
{ value: 'after_specific', label: 'After specific parties' },
|
||||
];
|
||||
|
||||
@customElement('sdig-contract-parties')
|
||||
export class SdigContractParties extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contract-parties
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-parties>
|
||||
`;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.parties-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Section cards */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Roles list */
|
||||
.roles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
}
|
||||
|
||||
.role-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.role-name dees-icon {
|
||||
font-size: 16px;
|
||||
padding: 6px;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
||||
}
|
||||
|
||||
.role-description {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.role-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.role-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Parties list */
|
||||
.parties-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.party-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.party-card:hover {
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
}
|
||||
|
||||
.party-card.selected {
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
}
|
||||
|
||||
.party-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.party-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.party-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.party-role-tag {
|
||||
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')};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.party-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.party-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.party-detail dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.party-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.signature-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.signature-status.pending {
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
||||
}
|
||||
|
||||
.signature-status.signed {
|
||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
||||
}
|
||||
|
||||
.signature-status.declined {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
||||
}
|
||||
|
||||
.signing-order {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.order-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
.add-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
background: transparent;
|
||||
border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
border-radius: 10px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// PROPERTIES
|
||||
// ============================================================================
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public accessor readonly: boolean = false;
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
@state()
|
||||
private accessor selectedPartyId: string | null = null;
|
||||
|
||||
@state()
|
||||
private accessor showRoleEditor: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor showPartyEditor: boolean = false;
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
private handleFieldChange(path: string, value: unknown) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('field-change', {
|
||||
detail: { path, value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleSelectParty(partyId: string) {
|
||||
this.selectedPartyId = this.selectedPartyId === partyId ? null : partyId;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('party-select', {
|
||||
detail: { partyId: this.selectedPartyId },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleAddRole() {
|
||||
this.showRoleEditor = true;
|
||||
// TODO: Open role editor modal
|
||||
}
|
||||
|
||||
private handleAddParty() {
|
||||
this.showPartyEditor = true;
|
||||
// TODO: Open party editor modal
|
||||
}
|
||||
|
||||
private handleRemoveParty(partyId: string) {
|
||||
if (!this.contract) return;
|
||||
const updatedParties = this.contract.involvedParties.filter((p) => p.partyId !== partyId);
|
||||
this.handleFieldChange('involvedParties', updatedParties);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
private getPartyDisplayName(party: plugins.sdInterfaces.IInvolvedParty): string {
|
||||
if (!party) return 'Unknown Party';
|
||||
const contact = party.contact;
|
||||
if (!contact) return party.deliveryEmail || 'Unknown Party';
|
||||
if ('name' in contact && contact.name) {
|
||||
return contact.name as string;
|
||||
}
|
||||
if ('firstName' in contact && 'lastName' in contact) {
|
||||
return `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || party.deliveryEmail || 'Unknown Party';
|
||||
}
|
||||
return party.deliveryEmail || 'Unknown Party';
|
||||
}
|
||||
|
||||
private getPartyInitials(party: plugins.sdInterfaces.IInvolvedParty): string {
|
||||
const name = this.getPartyDisplayName(party);
|
||||
if (!name || name.length === 0) return '??';
|
||||
const parts = name.split(' ');
|
||||
if (parts.length >= 2 && parts[0].length > 0 && parts[parts.length - 1].length > 0) {
|
||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
}
|
||||
return name.substring(0, Math.min(2, name.length)).toUpperCase();
|
||||
}
|
||||
|
||||
private getPartyColor(party: plugins.sdInterfaces.IInvolvedParty): string {
|
||||
// Generate a consistent color based on party ID
|
||||
const colors = [
|
||||
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6',
|
||||
'#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1',
|
||||
];
|
||||
const idStr = party?.partyId || 'default';
|
||||
const hash = idStr.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
|
||||
return colors[hash % colors.length];
|
||||
}
|
||||
|
||||
private getRoleName(roleId: string): string {
|
||||
if (!roleId) return 'Unknown Role';
|
||||
const role = this.contract?.availableRoles.find((r) => r.id === roleId);
|
||||
return role?.name || roleId.charAt(0).toUpperCase() + roleId.slice(1);
|
||||
}
|
||||
|
||||
private getSignatureStatusClass(status: string): string {
|
||||
if (status === 'signed') return 'signed';
|
||||
if (status === 'declined') return 'declined';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
private formatSignatureStatus(status: string): string {
|
||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
const roles = this.contract.availableRoles;
|
||||
const parties = this.contract.involvedParties;
|
||||
|
||||
return html`
|
||||
<div class="parties-container">
|
||||
<!-- Roles Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:users-2'}></dees-icon>
|
||||
Available Roles
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-secondary btn-sm" @click=${this.handleAddRole}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Role
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="section-content">
|
||||
${roles.length > 0
|
||||
? html`
|
||||
<div class="roles-grid">
|
||||
${roles.map((role) => this.renderRoleCard(role))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:users'}></dees-icon>
|
||||
<h4>No Roles Defined</h4>
|
||||
<p>Add roles to define the types of parties in this contract</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parties Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:user-plus'}></dees-icon>
|
||||
Involved Parties (${parties.length})
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary btn-sm" @click=${this.handleAddParty}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Party
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="section-content">
|
||||
${parties.length > 0
|
||||
? html`
|
||||
<div class="parties-list">
|
||||
${parties.map((party) => this.renderPartyCard(party))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:user-plus'}></dees-icon>
|
||||
<h4>No Parties Added</h4>
|
||||
<p>Add parties who will be involved in this contract</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRoleCard(role: plugins.sdInterfaces.IRole): TemplateResult {
|
||||
return html`
|
||||
<div class="role-card">
|
||||
<div class="role-header">
|
||||
<div class="role-name">
|
||||
<dees-icon
|
||||
.iconFA=${role.icon || 'lucide:user'}
|
||||
style="color: ${role.displayColor || 'inherit'}"
|
||||
></dees-icon>
|
||||
${role.name}
|
||||
</div>
|
||||
<span class="role-badge">${role.category}</span>
|
||||
</div>
|
||||
<div class="role-description">${role.description || 'No description'}</div>
|
||||
<div class="role-meta">
|
||||
${role.signatureRequired
|
||||
? html`
|
||||
<span class="role-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:pen-tool'}></dees-icon>
|
||||
Signature required
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
${role.defaultSigningOrder > 0
|
||||
? html`
|
||||
<span class="role-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:list-ordered'}></dees-icon>
|
||||
Order: ${role.defaultSigningOrder}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
${role.minParties
|
||||
? html`
|
||||
<span class="role-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:users'}></dees-icon>
|
||||
Min: ${role.minParties}${role.maxParties ? `, Max: ${role.maxParties}` : ''}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPartyCard(party: plugins.sdInterfaces.IInvolvedParty): TemplateResult {
|
||||
// Handle both full IInvolvedParty and minimal demo data
|
||||
const partyId = (party as any).partyId || (party as any).role || 'unknown';
|
||||
const roleId = (party as any).roleId || (party as any).role || '';
|
||||
const partyRole = (party as any).partyRole || 'signer';
|
||||
const signatureStatus = (party as any).signature?.status || 'pending';
|
||||
const signingOrder = (party as any).signingOrder || 0;
|
||||
const deliveryEmail = (party as any).deliveryEmail;
|
||||
const deliveryPhone = (party as any).deliveryPhone;
|
||||
const actingAsProxy = (party as any).actingAsProxy;
|
||||
|
||||
const isSelected = this.selectedPartyId === partyId;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="party-card ${isSelected ? 'selected' : ''}"
|
||||
@click=${() => this.handleSelectParty(partyId)}
|
||||
>
|
||||
<div
|
||||
class="party-avatar"
|
||||
style="background: ${this.getPartyColor(party)}"
|
||||
>
|
||||
${this.getPartyInitials(party)}
|
||||
</div>
|
||||
|
||||
<div class="party-info">
|
||||
<div class="party-name">${this.getPartyDisplayName(party)}</div>
|
||||
<div class="party-role-tag">
|
||||
${this.getRoleName(roleId)} (${PARTY_ROLES.find((r) => r.value === partyRole)?.label || partyRole})
|
||||
</div>
|
||||
<div class="party-details">
|
||||
${deliveryEmail
|
||||
? html`
|
||||
<div class="party-detail">
|
||||
<dees-icon .iconFA=${'lucide:mail'}></dees-icon>
|
||||
${deliveryEmail}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${deliveryPhone
|
||||
? html`
|
||||
<div class="party-detail">
|
||||
<dees-icon .iconFA=${'lucide:phone'}></dees-icon>
|
||||
${deliveryPhone}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${actingAsProxy
|
||||
? html`
|
||||
<div class="party-detail">
|
||||
<dees-icon .iconFA=${'lucide:users'}></dees-icon>
|
||||
Acting as proxy
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="party-status">
|
||||
<span class="signature-status ${this.getSignatureStatusClass(signatureStatus)}">
|
||||
${this.formatSignatureStatus(signatureStatus)}
|
||||
</span>
|
||||
${signingOrder > 0
|
||||
? html`
|
||||
<div class="signing-order">
|
||||
<span class="order-number">${signingOrder}</span>
|
||||
<span>Signing order</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-signatures/index.ts
Normal file
1
ts_web/elements/sdig-contract-signatures/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-signatures.js';
|
||||
@@ -0,0 +1,840 @@
|
||||
/**
|
||||
* @file sdig-contract-signatures.ts
|
||||
* @description Contract signature fields manager 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-signatures': SdigContractSignatures;
|
||||
}
|
||||
}
|
||||
|
||||
// Signature field interface (for future interface updates)
|
||||
interface ISignatureField {
|
||||
id: string;
|
||||
name: string;
|
||||
assignedPartyId: string | null;
|
||||
roleId: string;
|
||||
type: 'signature' | 'initials' | 'date' | 'text';
|
||||
required: boolean;
|
||||
status: 'pending' | 'ready' | 'signed' | 'declined';
|
||||
signedAt?: number;
|
||||
signatureData?: any;
|
||||
position: {
|
||||
paragraphId?: string;
|
||||
pageNumber?: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Signature status configuration
|
||||
const SIGNATURE_STATUSES = [
|
||||
{ value: 'pending', label: 'Pending', color: '#f59e0b', icon: 'lucide:clock' },
|
||||
{ value: 'ready', label: 'Ready to Sign', color: '#3b82f6', icon: 'lucide:pen-tool' },
|
||||
{ value: 'signed', label: 'Signed', color: '#10b981', icon: 'lucide:check-circle' },
|
||||
{ value: 'declined', label: 'Declined', color: '#ef4444', icon: 'lucide:x-circle' },
|
||||
];
|
||||
|
||||
const FIELD_TYPES = [
|
||||
{ value: 'signature', label: 'Full Signature', icon: 'lucide:pen-tool' },
|
||||
{ value: 'initials', label: 'Initials', icon: 'lucide:type' },
|
||||
{ value: 'date', label: 'Date', icon: 'lucide:calendar' },
|
||||
{ value: 'text', label: 'Text Field', icon: 'lucide:text-cursor' },
|
||||
];
|
||||
|
||||
@customElement('sdig-contract-signatures')
|
||||
export class SdigContractSignatures extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contract-signatures
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-signatures>
|
||||
`;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.signatures-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Summary cards */
|
||||
.summary-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.summary-card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.summary-card-icon.pending {
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
||||
color: ${cssManager.bdTheme('#f59e0b', '#fcd34d')};
|
||||
}
|
||||
|
||||
.summary-card-icon.ready {
|
||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.summary-card-icon.signed {
|
||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
||||
color: ${cssManager.bdTheme('#10b981', '#34d399')};
|
||||
}
|
||||
|
||||
.summary-card-icon.declined {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
||||
}
|
||||
|
||||
.summary-card-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.summary-card-label {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Signature fields list */
|
||||
.fields-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.field-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.field-card:hover {
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
||||
}
|
||||
|
||||
.field-card.selected {
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.field-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.field-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.field-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.field-meta-item dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.field-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.field-status.pending {
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
||||
}
|
||||
|
||||
.field-status.ready {
|
||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
||||
}
|
||||
|
||||
.field-status.signed {
|
||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
||||
}
|
||||
|
||||
.field-status.declined {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
||||
}
|
||||
|
||||
.field-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Signer progress */
|
||||
.signers-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.signers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.signer-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.signer-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.signer-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.signer-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.signer-role {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.signer-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#10b981', '#34d399')};
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
min-width: 36px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Signature preview */
|
||||
.signature-preview {
|
||||
position: relative;
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signature-preview-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.signature-preview-image {
|
||||
max-width: 200px;
|
||||
max-height: 80px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.signature-preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||
}
|
||||
|
||||
.signature-preview-placeholder dees-icon {
|
||||
font-size: 32px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 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')};
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: ${cssManager.bdTheme('#10b981', '#059669')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: ${cssManager.bdTheme('#059669', '#047857')};
|
||||
}
|
||||
|
||||
/* 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')};
|
||||
}
|
||||
|
||||
/* Signing order */
|
||||
.signing-order-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// PROPERTIES
|
||||
// ============================================================================
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public accessor readonly: boolean = false;
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
@state()
|
||||
private accessor selectedFieldId: string | null = null;
|
||||
|
||||
// Demo signature fields data
|
||||
@state()
|
||||
private accessor signatureFields: ISignatureField[] = [];
|
||||
|
||||
// ============================================================================
|
||||
// LIFECYCLE
|
||||
// ============================================================================
|
||||
|
||||
public async firstUpdated() {
|
||||
// Generate demo signature fields based on contract parties
|
||||
if (this.contract && this.contract.involvedParties.length > 0) {
|
||||
this.signatureFields = this.contract.involvedParties.map((party, index) => ({
|
||||
id: `sig-${index + 1}`,
|
||||
name: `Signature - ${this.getPartyRoleName(party.role)}`,
|
||||
assignedPartyId: null,
|
||||
roleId: party.role,
|
||||
type: 'signature' as const,
|
||||
required: true,
|
||||
status: index === 0 ? 'signed' : index === 1 ? 'ready' : 'pending',
|
||||
signedAt: index === 0 ? Date.now() - 86400000 : undefined,
|
||||
position: { x: 0, y: 0 },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
private handleFieldChange(path: string, value: unknown) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('field-change', {
|
||||
detail: { path, value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleSelectField(fieldId: string) {
|
||||
this.selectedFieldId = this.selectedFieldId === fieldId ? null : fieldId;
|
||||
}
|
||||
|
||||
private handleAddField() {
|
||||
const newField: ISignatureField = {
|
||||
id: `sig-${Date.now()}`,
|
||||
name: 'New Signature Field',
|
||||
assignedPartyId: null,
|
||||
roleId: '',
|
||||
type: 'signature',
|
||||
required: true,
|
||||
status: 'pending',
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
this.signatureFields = [...this.signatureFields, newField];
|
||||
}
|
||||
|
||||
private handleDeleteField(fieldId: string) {
|
||||
this.signatureFields = this.signatureFields.filter((f) => f.id !== fieldId);
|
||||
if (this.selectedFieldId === fieldId) {
|
||||
this.selectedFieldId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
private getPartyRoleName(roleId: string): string {
|
||||
const role = this.contract?.availableRoles.find((r) => r.id === roleId);
|
||||
return role?.name || roleId;
|
||||
}
|
||||
|
||||
private getStatusConfig(status: string) {
|
||||
return SIGNATURE_STATUSES.find((s) => s.value === status) || SIGNATURE_STATUSES[0];
|
||||
}
|
||||
|
||||
private getFieldTypeConfig(type: string) {
|
||||
return FIELD_TYPES.find((t) => t.value === type) || FIELD_TYPES[0];
|
||||
}
|
||||
|
||||
private getSignatureStats() {
|
||||
const total = this.signatureFields.length;
|
||||
const signed = this.signatureFields.filter((f) => f.status === 'signed').length;
|
||||
const ready = this.signatureFields.filter((f) => f.status === 'ready').length;
|
||||
const pending = this.signatureFields.filter((f) => f.status === 'pending').length;
|
||||
const declined = this.signatureFields.filter((f) => f.status === 'declined').length;
|
||||
return { total, signed, ready, pending, declined };
|
||||
}
|
||||
|
||||
private getPartyColor(index: number): string {
|
||||
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
const stats = this.getSignatureStats();
|
||||
|
||||
return html`
|
||||
<div class="signatures-container">
|
||||
<!-- Summary Cards -->
|
||||
<div class="summary-row">
|
||||
<div class="summary-card">
|
||||
<div class="summary-card-icon pending">
|
||||
<dees-icon .iconFA=${'lucide:clock'}></dees-icon>
|
||||
</div>
|
||||
<div class="summary-card-value">${stats.pending}</div>
|
||||
<div class="summary-card-label">Pending</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-card-icon ready">
|
||||
<dees-icon .iconFA=${'lucide:pen-tool'}></dees-icon>
|
||||
</div>
|
||||
<div class="summary-card-value">${stats.ready}</div>
|
||||
<div class="summary-card-label">Ready to Sign</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-card-icon signed">
|
||||
<dees-icon .iconFA=${'lucide:check-circle'}></dees-icon>
|
||||
</div>
|
||||
<div class="summary-card-value">${stats.signed}</div>
|
||||
<div class="summary-card-label">Signed</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-card-icon declined">
|
||||
<dees-icon .iconFA=${'lucide:x-circle'}></dees-icon>
|
||||
</div>
|
||||
<div class="summary-card-value">${stats.declined}</div>
|
||||
<div class="summary-card-label">Declined</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature Fields Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:pen-tool'}></dees-icon>
|
||||
Signature Fields
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary" @click=${this.handleAddField}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Field
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="section-content">
|
||||
${this.signatureFields.length > 0
|
||||
? html`
|
||||
<div class="fields-list">
|
||||
${this.signatureFields.map((field, index) => this.renderSignatureField(field, index))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:pen-tool'}></dees-icon>
|
||||
<h4>No Signature Fields</h4>
|
||||
<p>Add signature fields to define where parties should sign the contract</p>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary" @click=${this.handleAddField}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Signature Field
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signers Progress Section -->
|
||||
${this.contract.involvedParties.length > 0
|
||||
? html`
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:users'}></dees-icon>
|
||||
Signers Progress
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="signers-grid">
|
||||
${this.contract.involvedParties.map((party, index) => this.renderSignerCard(party, index))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSignatureField(field: ISignatureField, index: number): TemplateResult {
|
||||
const isSelected = this.selectedFieldId === field.id;
|
||||
const statusConfig = this.getStatusConfig(field.status);
|
||||
const typeConfig = this.getFieldTypeConfig(field.type);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="field-card ${isSelected ? 'selected' : ''}"
|
||||
@click=${() => this.handleSelectField(field.id)}
|
||||
>
|
||||
<div class="signing-order-badge">${index + 1}</div>
|
||||
|
||||
<div class="field-icon">
|
||||
<dees-icon .iconFA=${typeConfig.icon}></dees-icon>
|
||||
</div>
|
||||
|
||||
<div class="field-info">
|
||||
<div class="field-name">${field.name}</div>
|
||||
<div class="field-meta">
|
||||
<span class="field-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:user'}></dees-icon>
|
||||
${this.getPartyRoleName(field.roleId)}
|
||||
</span>
|
||||
<span class="type-badge">
|
||||
<dees-icon .iconFA=${typeConfig.icon}></dees-icon>
|
||||
${typeConfig.label}
|
||||
</span>
|
||||
${field.required
|
||||
? html`
|
||||
<span class="field-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:asterisk'}></dees-icon>
|
||||
Required
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
${field.signedAt
|
||||
? html`
|
||||
<span class="field-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:calendar'}></dees-icon>
|
||||
${this.formatDate(field.signedAt)}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-status ${field.status}">
|
||||
<dees-icon .iconFA=${statusConfig.icon}></dees-icon>
|
||||
${statusConfig.label}
|
||||
</div>
|
||||
|
||||
${!this.readonly
|
||||
? html`
|
||||
<div class="field-actions">
|
||||
<button class="btn btn-ghost" @click=${(e: Event) => { e.stopPropagation(); }} title="Edit">
|
||||
<dees-icon .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.handleDeleteField(field.id); }}
|
||||
title="Delete"
|
||||
style="color: #ef4444;"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:trash-2'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSignerCard(party: plugins.sdInterfaces.IInvolvedParty, index: number): TemplateResult {
|
||||
const partyFields = this.signatureFields.filter((f) => f.roleId === party.role);
|
||||
const signedFields = partyFields.filter((f) => f.status === 'signed').length;
|
||||
const totalFields = partyFields.length;
|
||||
const progress = totalFields > 0 ? Math.round((signedFields / totalFields) * 100) : 0;
|
||||
const roleName = this.getPartyRoleName(party.role);
|
||||
|
||||
return html`
|
||||
<div class="signer-card">
|
||||
<div class="signer-avatar" style="background: ${this.getPartyColor(index)}">
|
||||
${roleName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div class="signer-info">
|
||||
<div class="signer-name">${roleName}</div>
|
||||
<div class="signer-role">${signedFields} of ${totalFields} signatures</div>
|
||||
<div class="signer-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${progress}%"></div>
|
||||
</div>
|
||||
<span class="progress-text">${progress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-terms/index.ts
Normal file
1
ts_web/elements/sdig-contract-terms/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-terms.js';
|
||||
873
ts_web/elements/sdig-contract-terms/sdig-contract-terms.ts
Normal file
873
ts_web/elements/sdig-contract-terms/sdig-contract-terms.ts
Normal file
@@ -0,0 +1,873 @@
|
||||
/**
|
||||
* @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`
|
||||
<sdig-contract-terms
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contract-terms>
|
||||
`;
|
||||
|
||||
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`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="terms-container">
|
||||
<div class="section-card">
|
||||
<!-- Tabs Navigation -->
|
||||
<nav class="tabs-nav">
|
||||
${TERM_TABS.map(
|
||||
(tab) => html`
|
||||
<button
|
||||
class="tab-btn ${this.activeTab === tab.id ? 'active' : ''}"
|
||||
@click=${() => this.handleTabChange(tab.id)}
|
||||
>
|
||||
<dees-icon .iconFA=${tab.icon}></dees-icon>
|
||||
${tab.label}
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content">
|
||||
${this.activeTab === 'financial'
|
||||
? this.renderFinancialTerms()
|
||||
: this.activeTab === 'time'
|
||||
? this.renderTimeTerms()
|
||||
: this.renderObligations()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderFinancialTerms(): TemplateResult {
|
||||
const totalAmount = this.getTotalAmount();
|
||||
const paidAmount = this.getPaidAmount();
|
||||
const pendingAmount = totalAmount - paidAmount;
|
||||
|
||||
return html`
|
||||
<!-- Summary -->
|
||||
<div class="summary-card">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Total Value</span>
|
||||
<span class="summary-value currency">${this.formatCurrency(totalAmount, 'EUR')}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Paid</span>
|
||||
<span class="summary-value currency" style="color: #059669;">${this.formatCurrency(paidAmount, 'EUR')}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Pending</span>
|
||||
<span class="summary-value currency" style="color: #f59e0b;">${this.formatCurrency(pendingAmount, 'EUR')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Schedule -->
|
||||
<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<div>
|
||||
<div class="sub-section-title">Payment Schedule</div>
|
||||
<div class="sub-section-description">Scheduled payments and their status</div>
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-secondary" @click=${this.handleAddPayment}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Payment
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
${this.paymentSchedule.length > 0
|
||||
? html`
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Due Date</th>
|
||||
<th>Status</th>
|
||||
${!this.readonly ? html`<th></th>` : ''}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.paymentSchedule.map(
|
||||
(payment) => html`
|
||||
<tr>
|
||||
<td>${payment.description}</td>
|
||||
<td><span class="amount">${this.formatCurrency(payment.amount, payment.currency)}</span></td>
|
||||
<td>${this.formatDate(payment.dueDate)}</td>
|
||||
<td><span class="status-badge ${payment.status}">${payment.status}</span></td>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm">
|
||||
<dees-icon .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
</td>
|
||||
`
|
||||
: ''}
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:banknote'}></dees-icon>
|
||||
<h4>No Payment Schedule</h4>
|
||||
<p>Add payment terms to track financial obligations</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTimeTerms(): TemplateResult {
|
||||
const completedCount = this.milestones.filter((m) => m.status === 'completed').length;
|
||||
const totalCount = this.milestones.length;
|
||||
|
||||
return html`
|
||||
<!-- Summary -->
|
||||
<div class="summary-card">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Total Milestones</span>
|
||||
<span class="summary-value">${totalCount}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Completed</span>
|
||||
<span class="summary-value" style="color: #059669;">${completedCount}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Progress</span>
|
||||
<span class="summary-value">${totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestones -->
|
||||
<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<div>
|
||||
<div class="sub-section-title">Milestones</div>
|
||||
<div class="sub-section-description">Key project milestones and deadlines</div>
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-secondary" @click=${this.handleAddMilestone}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Milestone
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
${this.milestones.length > 0
|
||||
? html`
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Milestone</th>
|
||||
<th>Description</th>
|
||||
<th>Due Date</th>
|
||||
<th>Status</th>
|
||||
${!this.readonly ? html`<th></th>` : ''}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.milestones.map(
|
||||
(milestone) => html`
|
||||
<tr>
|
||||
<td><strong>${milestone.name}</strong></td>
|
||||
<td>${milestone.description || '—'}</td>
|
||||
<td>${this.formatDate(milestone.dueDate)}</td>
|
||||
<td><span class="status-badge ${milestone.status}">${milestone.status.replace('_', ' ')}</span></td>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm">
|
||||
<dees-icon .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
</td>
|
||||
`
|
||||
: ''}
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:calendar'}></dees-icon>
|
||||
<h4>No Milestones</h4>
|
||||
<p>Add milestones to track project progress</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderObligations(): TemplateResult {
|
||||
const completedCount = this.obligations.filter((o) => o.status === 'completed').length;
|
||||
|
||||
return html`
|
||||
<!-- Summary -->
|
||||
<div class="summary-card">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Total Obligations</span>
|
||||
<span class="summary-value">${this.obligations.length}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Completed</span>
|
||||
<span class="summary-value" style="color: #059669;">${completedCount}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Pending</span>
|
||||
<span class="summary-value" style="color: #f59e0b;">${this.obligations.length - completedCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info banner -->
|
||||
<div class="info-banner">
|
||||
<dees-icon .iconFA=${'lucide:info'}></dees-icon>
|
||||
<div class="info-banner-content">
|
||||
<div class="info-banner-title">Contractual Obligations</div>
|
||||
<div class="info-banner-text">
|
||||
Track responsibilities assigned to each party. Mark obligations as completed when fulfilled.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Obligations -->
|
||||
<div class="sub-section">
|
||||
<div class="sub-section-header">
|
||||
<div>
|
||||
<div class="sub-section-title">Party Obligations</div>
|
||||
<div class="sub-section-description">Responsibilities and deliverables by party</div>
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-secondary" @click=${this.handleAddObligation}>
|
||||
<dees-icon .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add Obligation
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
${this.obligations.length > 0
|
||||
? html`
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Obligation</th>
|
||||
<th>Responsible Party</th>
|
||||
<th>Deadline</th>
|
||||
<th>Status</th>
|
||||
${!this.readonly ? html`<th></th>` : ''}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.obligations.map(
|
||||
(obligation) => html`
|
||||
<tr>
|
||||
<td>${obligation.description}</td>
|
||||
<td>${this.getPartyName(obligation.responsibleParty)}</td>
|
||||
<td>${this.formatDate(obligation.deadline)}</td>
|
||||
<td><span class="status-badge ${obligation.status}">${obligation.status}</span></td>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm">
|
||||
<dees-icon .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
</td>
|
||||
`
|
||||
: ''}
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:check-square'}></dees-icon>
|
||||
<h4>No Obligations</h4>
|
||||
<p>Add obligations to track party responsibilities</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
domtools,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-contracteditor': ContractEditor;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-contracteditor')
|
||||
export class ContractEditor extends DeesElement {
|
||||
public static demo = () => html` <sdig-contracteditor
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contracteditor> `;
|
||||
|
||||
// INSTANCE
|
||||
public localStateInstance = new domtools.plugins.smartstate.Smartstate();
|
||||
public contractState =
|
||||
this.localStateInstance.getStatePart<plugins.sdInterfaces.IPortableContract>('contract');
|
||||
|
||||
@property({ type: Object })
|
||||
public contract: plugins.sdInterfaces.IPortableContract;
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html` <div class="mainbox"></div> `;
|
||||
}
|
||||
}
|
||||
8
ts_web/elements/sdig-contracteditor/index.ts
Normal file
8
ts_web/elements/sdig-contracteditor/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @file index.ts
|
||||
* @description Export barrel for sdig-contracteditor module
|
||||
*/
|
||||
|
||||
export * from './sdig-contracteditor.js';
|
||||
export * from './types.js';
|
||||
export * from './state.js';
|
||||
839
ts_web/elements/sdig-contracteditor/sdig-contracteditor.ts
Normal file
839
ts_web/elements/sdig-contracteditor/sdig-contracteditor.ts
Normal file
@@ -0,0 +1,839 @@
|
||||
/**
|
||||
* @file sdig-contracteditor.ts
|
||||
* @description Main contract editor orchestrator component
|
||||
*/
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { createEditorStore, type TEditorStore } from './state.js';
|
||||
import {
|
||||
type TEditorSection,
|
||||
type IEditorState,
|
||||
EDITOR_SECTIONS,
|
||||
type IContractChangeEventDetail,
|
||||
type ISectionChangeEventDetail,
|
||||
} from './types.js';
|
||||
|
||||
// Import sub-components
|
||||
import '../sdig-contract-header/sdig-contract-header.js';
|
||||
import '../sdig-contract-metadata/sdig-contract-metadata.js';
|
||||
import '../sdig-contract-parties/sdig-contract-parties.js';
|
||||
import '../sdig-contract-content/sdig-contract-content.js';
|
||||
import '../sdig-contract-terms/sdig-contract-terms.js';
|
||||
import '../sdig-contract-signatures/sdig-contract-signatures.js';
|
||||
import '../sdig-contract-attachments/sdig-contract-attachments.js';
|
||||
import '../sdig-contract-collaboration/sdig-contract-collaboration.js';
|
||||
import '../sdig-contract-audit/sdig-contract-audit.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-contracteditor': SdigContracteditor;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-contracteditor')
|
||||
export class SdigContracteditor extends DeesElement {
|
||||
// ============================================================================
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
|
||||
public static demo = () => html`
|
||||
<sdig-contracteditor
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contracteditor>
|
||||
`;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#09090b')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contract-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* shadcn-style badge */
|
||||
.contract-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
border: 1px solid transparent;
|
||||
background: ${cssManager.bdTheme('hsl(214 95% 93%)', 'hsl(214 95% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(214 95% 35%)', 'hsl(214 95% 70%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(214 95% 80%)', 'hsl(214 95% 25%)')};
|
||||
}
|
||||
|
||||
.contract-status.draft {
|
||||
background: ${cssManager.bdTheme('hsl(48 96% 89%)', 'hsl(48 96% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(25 95% 30%)', 'hsl(48 96% 70%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(48 96% 76%)', 'hsl(48 96% 25%)')};
|
||||
}
|
||||
|
||||
.contract-status.executed {
|
||||
background: ${cssManager.bdTheme('hsl(142 76% 90%)', 'hsl(142 76% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(142 76% 28%)', 'hsl(142 76% 65%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(142 76% 75%)', 'hsl(142 76% 25%)')};
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dirty-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.dirty-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.collaborators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: -8px;
|
||||
}
|
||||
|
||||
.collaborator-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.collaborator-avatar:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Navigation Tabs */
|
||||
.editor-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 24px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
||||
}
|
||||
|
||||
.nav-tab dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: ${cssManager.bdTheme('#ef4444', '#dc2626')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.editor-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.editor-sidebar {
|
||||
width: 320px;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Section placeholder */
|
||||
.section-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
text-align: center;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.section-placeholder dees-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.section-placeholder h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
.section-placeholder p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 24px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.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')};
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: ${cssManager.bdTheme('rgba(255,255,255,0.8)', 'rgba(0,0,0,0.8)')};
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Overview section layout */
|
||||
.overview-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// PROPERTIES
|
||||
// ============================================================================
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public accessor showSidebar: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
public accessor initialSection: TEditorSection = 'overview';
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
@state()
|
||||
private accessor editorState: IEditorState | null = null;
|
||||
|
||||
// ============================================================================
|
||||
// INSTANCE
|
||||
// ============================================================================
|
||||
|
||||
private store: TEditorStore | null = null;
|
||||
private unsubscribe: (() => void) | null = null;
|
||||
private storeReady: Promise<void>;
|
||||
private resolveStoreReady!: () => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.storeReady = new Promise((resolve) => {
|
||||
this.resolveStoreReady = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LIFECYCLE
|
||||
// ============================================================================
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.initStore();
|
||||
}
|
||||
|
||||
private async initStore() {
|
||||
this.store = await createEditorStore();
|
||||
this.unsubscribe = this.store.subscribe((state) => {
|
||||
this.editorState = state;
|
||||
});
|
||||
|
||||
// Set initial section
|
||||
this.store.setActiveSection(this.initialSection);
|
||||
this.resolveStoreReady();
|
||||
|
||||
// If contract was already set, apply it now
|
||||
if (this.contract) {
|
||||
this.store.setContract(this.contract);
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('contract') && this.contract) {
|
||||
await this.storeReady;
|
||||
this.store?.setContract(this.contract);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
private handleSectionChange(section: TEditorSection) {
|
||||
const previousSection = this.editorState?.activeSection || 'overview';
|
||||
this.store?.setActiveSection(section);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<ISectionChangeEventDetail>('section-change', {
|
||||
detail: { section, previousSection },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleSave() {
|
||||
if (!this.editorState?.contract) return;
|
||||
|
||||
this.store?.setSaving(true);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('contract-save', {
|
||||
detail: {
|
||||
contract: this.editorState.contract,
|
||||
isDraft: this.editorState.contract.lifecycle.currentStatus === 'draft',
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleDiscard() {
|
||||
this.store?.discardChanges();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('contract-discard', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleUndo() {
|
||||
this.store?.undo();
|
||||
}
|
||||
|
||||
private handleRedo() {
|
||||
this.store?.redo();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PUBLIC API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Update a field in the contract
|
||||
*/
|
||||
public updateField(path: string, value: unknown, description?: string) {
|
||||
this.store?.updateContract(path, value, description);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<IContractChangeEventDetail>('contract-change', {
|
||||
detail: { path, value, source: 'user' },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current contract state
|
||||
*/
|
||||
public getContract(): plugins.sdInterfaces.IPortableContract | null {
|
||||
return this.editorState?.contract || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark contract as saved externally
|
||||
*/
|
||||
public markSaved() {
|
||||
this.store?.markSaved();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER HELPERS
|
||||
// ============================================================================
|
||||
|
||||
private getStatusClass(status: string): string {
|
||||
if (status === 'draft' || status === 'internal_review') return 'draft';
|
||||
if (status === 'executed' || status === 'active') return 'executed';
|
||||
return '';
|
||||
}
|
||||
|
||||
private formatStatus(status: string): string {
|
||||
return status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
private handleFieldChange(e: CustomEvent<{ path: string; value: unknown }>) {
|
||||
const { path, value } = e.detail;
|
||||
this.updateField(path, value);
|
||||
}
|
||||
|
||||
private renderSectionContent(): TemplateResult {
|
||||
const section = this.editorState?.activeSection || 'overview';
|
||||
const contract = this.editorState?.contract;
|
||||
const sectionConfig = EDITOR_SECTIONS.find((s) => s.id === section);
|
||||
|
||||
// Render section based on active tab
|
||||
switch (section) {
|
||||
case 'overview':
|
||||
return this.renderOverviewSection();
|
||||
case 'parties':
|
||||
return this.renderPartiesSection();
|
||||
case 'content':
|
||||
return this.renderContentSection();
|
||||
case 'terms':
|
||||
return this.renderTermsSection();
|
||||
case 'signatures':
|
||||
return this.renderSignaturesSection();
|
||||
case 'attachments':
|
||||
return this.renderAttachmentsSection();
|
||||
case 'collaboration':
|
||||
return this.renderCollaborationSection();
|
||||
case 'audit':
|
||||
return this.renderAuditSection();
|
||||
default:
|
||||
return this.renderPlaceholder(sectionConfig, 'This section is being implemented...');
|
||||
}
|
||||
}
|
||||
|
||||
private renderOverviewSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="overview-section">
|
||||
<sdig-contract-header
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-header>
|
||||
|
||||
<sdig-contract-metadata
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-metadata>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPartiesSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sdig-contract-parties
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-parties>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderContentSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sdig-contract-content
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-content>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTermsSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sdig-contract-terms
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-terms>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSignaturesSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sdig-contract-signatures
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-signatures>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAttachmentsSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sdig-contract-attachments
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-attachments>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCollaborationSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sdig-contract-collaboration
|
||||
.contract=${contract}
|
||||
@field-change=${this.handleFieldChange}
|
||||
></sdig-contract-collaboration>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAuditSection(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
if (!contract) {
|
||||
return html`<div>No contract loaded</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<sdig-contract-audit
|
||||
.contract=${contract}
|
||||
></sdig-contract-audit>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPlaceholder(sectionConfig: typeof EDITOR_SECTIONS[0] | undefined, message: string): TemplateResult {
|
||||
return html`
|
||||
<div class="section-placeholder">
|
||||
<dees-icon .iconFA=${sectionConfig?.icon || 'lucide:file'}></dees-icon>
|
||||
<h3>${sectionConfig?.label || 'Section'}</h3>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
const contract = this.editorState?.contract;
|
||||
const activeSection = this.editorState?.activeSection || 'overview';
|
||||
const isDirty = this.editorState?.isDirty || false;
|
||||
const isSaving = this.editorState?.isSaving || false;
|
||||
const collaborators = this.editorState?.activeCollaborators || [];
|
||||
|
||||
return html`
|
||||
<div class="editor-container">
|
||||
<!-- Header -->
|
||||
<div class="editor-header">
|
||||
<div class="header-left">
|
||||
<h1 class="contract-title">${contract?.title || 'Untitled Contract'}</h1>
|
||||
${contract?.lifecycle?.currentStatus
|
||||
? html`
|
||||
<span class="contract-status ${this.getStatusClass(contract.lifecycle.currentStatus)}">
|
||||
${this.formatStatus(contract.lifecycle.currentStatus)}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
${isDirty
|
||||
? html`
|
||||
<div class="dirty-indicator">
|
||||
<span class="dirty-dot"></span>
|
||||
<span>Unsaved changes</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${collaborators.length > 0
|
||||
? html`
|
||||
<div class="collaborators">
|
||||
${collaborators.slice(0, 3).map(
|
||||
(c) => html`
|
||||
<div
|
||||
class="collaborator-avatar"
|
||||
style="background: ${c.color}"
|
||||
title="${c.displayName}"
|
||||
>
|
||||
${c.displayName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${collaborators.length > 3
|
||||
? html`
|
||||
<div class="collaborator-avatar" style="background: #6b7280">
|
||||
+${collaborators.length - 3}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<button class="btn btn-ghost" @click=${this.handleUndo} ?disabled=${!this.store?.canUndo()}>
|
||||
<dees-icon .iconFA=${'lucide:undo-2'}></dees-icon>
|
||||
</button>
|
||||
<button class="btn btn-ghost" @click=${this.handleRedo} ?disabled=${!this.store?.canRedo()}>
|
||||
<dees-icon .iconFA=${'lucide:redo-2'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="editor-nav">
|
||||
${EDITOR_SECTIONS.map(
|
||||
(section) => html`
|
||||
<button
|
||||
class="nav-tab ${activeSection === section.id ? 'active' : ''}"
|
||||
@click=${() => this.handleSectionChange(section.id)}
|
||||
?disabled=${section.disabled}
|
||||
>
|
||||
<dees-icon .iconFA=${section.icon}></dees-icon>
|
||||
<span>${section.label}</span>
|
||||
${section.badge
|
||||
? html`<span class="nav-badge">${section.badge}</span>`
|
||||
: ''}
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="editor-main">
|
||||
<div class="editor-content">
|
||||
${this.renderSectionContent()}
|
||||
</div>
|
||||
${this.showSidebar
|
||||
? html`
|
||||
<aside class="editor-sidebar">
|
||||
<!-- Sidebar content - collaboration panel -->
|
||||
</aside>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="editor-footer">
|
||||
<div class="footer-left">
|
||||
${contract?.updatedAt
|
||||
? html`<span>Last updated: ${new Date(contract.updatedAt).toLocaleString()}</span>`
|
||||
: ''}
|
||||
${contract?.versionHistory?.currentVersionId
|
||||
? html`<span>Version: ${contract.versionHistory.currentVersionId}</span>`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
${isDirty
|
||||
? html`
|
||||
<button class="btn btn-secondary" @click=${this.handleDiscard}>
|
||||
Discard
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click=${this.handleSave}
|
||||
?disabled=${!isDirty || isSaving}
|
||||
>
|
||||
${isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.editorState?.isLoading
|
||||
? html`
|
||||
<div class="loading-overlay">
|
||||
<dees-spinner></dees-spinner>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
407
ts_web/elements/sdig-contracteditor/state.ts
Normal file
407
ts_web/elements/sdig-contracteditor/state.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* @file state.ts
|
||||
* @description Smartstate store for contract editor
|
||||
*/
|
||||
|
||||
import { domtools } from '@design.estate/dees-element';
|
||||
import type * as sdInterfaces from '@signature.digital/tools/interfaces';
|
||||
import {
|
||||
type IEditorState,
|
||||
type TEditorSection,
|
||||
type TEditorMode,
|
||||
type IContractChange,
|
||||
type IValidationError,
|
||||
type IEditorUser,
|
||||
createInitialEditorState,
|
||||
} from './types.js';
|
||||
|
||||
// ============================================================================
|
||||
// STATE STORE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a new editor state store instance
|
||||
*/
|
||||
export async function createEditorStore() {
|
||||
const smartstate = new domtools.plugins.smartstate.Smartstate<{ editor: IEditorState }>();
|
||||
|
||||
// Initialize with default state (getStatePart is now async)
|
||||
const statePart = await smartstate.getStatePart<IEditorState>('editor', createInitialEditorState(), 'soft');
|
||||
|
||||
// Create actions for state modifications
|
||||
const setContractAction = statePart.createAction<{ contract: sdInterfaces.IPortableContract }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
contract: structuredClone(payload.contract),
|
||||
originalContract: structuredClone(payload.contract),
|
||||
isDirty: false,
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
})
|
||||
);
|
||||
|
||||
const updateContractAction = statePart.createAction<{ path: string; value: unknown; description?: string; userId?: string }>(
|
||||
async (statePartArg, payload) => {
|
||||
const state = statePartArg.getState();
|
||||
if (!state.contract) return state;
|
||||
|
||||
const previousValue = getNestedValue(state.contract, payload.path);
|
||||
|
||||
const change: IContractChange = {
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
path: payload.path,
|
||||
previousValue,
|
||||
newValue: payload.value,
|
||||
description: payload.description || `Updated ${payload.path}`,
|
||||
userId: payload.userId,
|
||||
};
|
||||
|
||||
const updatedContract = setNestedValue(
|
||||
structuredClone(state.contract),
|
||||
payload.path,
|
||||
payload.value
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
contract: updatedContract,
|
||||
isDirty: true,
|
||||
undoStack: [...state.undoStack, change],
|
||||
redoStack: [],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const setActiveSectionAction = statePart.createAction<{ section: TEditorSection }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
activeSection: payload.section,
|
||||
})
|
||||
);
|
||||
|
||||
const setEditorModeAction = statePart.createAction<{ mode: TEditorMode }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
editorMode: payload.mode,
|
||||
})
|
||||
);
|
||||
|
||||
const selectParagraphAction = statePart.createAction<{ paragraphId: string | null }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
selectedParagraphId: payload.paragraphId,
|
||||
})
|
||||
);
|
||||
|
||||
const selectPartyAction = statePart.createAction<{ partyId: string | null }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
selectedPartyId: payload.partyId,
|
||||
})
|
||||
);
|
||||
|
||||
const selectSignatureFieldAction = statePart.createAction<{ fieldId: string | null }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
selectedSignatureFieldId: payload.fieldId,
|
||||
})
|
||||
);
|
||||
|
||||
const undoAction = statePart.createAction<void>(
|
||||
async (statePartArg) => {
|
||||
const state = statePartArg.getState();
|
||||
if (state.undoStack.length === 0 || !state.contract) return state;
|
||||
|
||||
const change = state.undoStack[state.undoStack.length - 1];
|
||||
const updatedContract = setNestedValue(
|
||||
structuredClone(state.contract),
|
||||
change.path,
|
||||
change.previousValue
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
contract: updatedContract,
|
||||
undoStack: state.undoStack.slice(0, -1),
|
||||
redoStack: [...state.redoStack, change],
|
||||
isDirty: state.undoStack.length > 1,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const redoAction = statePart.createAction<void>(
|
||||
async (statePartArg) => {
|
||||
const state = statePartArg.getState();
|
||||
if (state.redoStack.length === 0 || !state.contract) return state;
|
||||
|
||||
const change = state.redoStack[state.redoStack.length - 1];
|
||||
const updatedContract = setNestedValue(
|
||||
structuredClone(state.contract),
|
||||
change.path,
|
||||
change.newValue
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
contract: updatedContract,
|
||||
undoStack: [...state.undoStack, change],
|
||||
redoStack: state.redoStack.slice(0, -1),
|
||||
isDirty: true,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const setLoadingAction = statePart.createAction<{ isLoading: boolean }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
isLoading: payload.isLoading,
|
||||
})
|
||||
);
|
||||
|
||||
const setSavingAction = statePart.createAction<{ isSaving: boolean }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
isSaving: payload.isSaving,
|
||||
})
|
||||
);
|
||||
|
||||
const markSavedAction = statePart.createAction<void>(
|
||||
async (statePartArg) => {
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
originalContract: state.contract ? structuredClone(state.contract) : null,
|
||||
isDirty: false,
|
||||
isSaving: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const discardChangesAction = statePart.createAction<void>(
|
||||
async (statePartArg) => {
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
contract: state.originalContract ? structuredClone(state.originalContract) : null,
|
||||
isDirty: false,
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const setValidationErrorsAction = statePart.createAction<{ errors: IValidationError[] }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
validationErrors: payload.errors,
|
||||
})
|
||||
);
|
||||
|
||||
const clearValidationErrorsAction = statePart.createAction<void>(
|
||||
async (statePartArg) => ({
|
||||
...statePartArg.getState(),
|
||||
validationErrors: [],
|
||||
})
|
||||
);
|
||||
|
||||
const setCurrentUserAction = statePart.createAction<{ user: IEditorUser }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
currentUser: payload.user,
|
||||
})
|
||||
);
|
||||
|
||||
const setActiveCollaboratorsAction = statePart.createAction<{ collaborators: sdInterfaces.IUserPresence[] }>(
|
||||
async (statePartArg, payload) => ({
|
||||
...statePartArg.getState(),
|
||||
activeCollaborators: payload.collaborators,
|
||||
})
|
||||
);
|
||||
|
||||
const addCollaboratorAction = statePart.createAction<{ collaborator: sdInterfaces.IUserPresence }>(
|
||||
async (statePartArg, payload) => {
|
||||
const state = statePartArg.getState();
|
||||
if (state.activeCollaborators.find(c => c.userId === payload.collaborator.userId)) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
activeCollaborators: [...state.activeCollaborators, payload.collaborator],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const removeCollaboratorAction = statePart.createAction<{ userId: string }>(
|
||||
async (statePartArg, payload) => {
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
activeCollaborators: state.activeCollaborators.filter(c => c.userId !== payload.userId),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const updateCollaboratorAction = statePart.createAction<{ userId: string; updates: Partial<sdInterfaces.IUserPresence> }>(
|
||||
async (statePartArg, payload) => {
|
||||
const state = statePartArg.getState();
|
||||
return {
|
||||
...state,
|
||||
activeCollaborators: state.activeCollaborators.map(c =>
|
||||
c.userId === payload.userId ? { ...c, ...payload.updates } : c
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
smartstate,
|
||||
statePart,
|
||||
|
||||
// Getters
|
||||
getContract: () => statePart.getState().contract,
|
||||
getActiveSection: () => statePart.getState().activeSection,
|
||||
isDirty: () => statePart.getState().isDirty,
|
||||
isLoading: () => statePart.getState().isLoading,
|
||||
isSaving: () => statePart.getState().isSaving,
|
||||
|
||||
// Contract operations
|
||||
setContract: (contract: sdInterfaces.IPortableContract) => setContractAction.trigger({ contract }),
|
||||
|
||||
updateContract: (path: string, value: unknown, description?: string) => {
|
||||
const state = statePart.getState();
|
||||
return updateContractAction.trigger({ path, value, description, userId: state.currentUser?.userId });
|
||||
},
|
||||
|
||||
// Navigation
|
||||
setActiveSection: (section: TEditorSection) => setActiveSectionAction.trigger({ section }),
|
||||
setEditorMode: (mode: TEditorMode) => setEditorModeAction.trigger({ mode }),
|
||||
|
||||
// Selection
|
||||
selectParagraph: (paragraphId: string | null) => selectParagraphAction.trigger({ paragraphId }),
|
||||
selectParty: (partyId: string | null) => selectPartyAction.trigger({ partyId }),
|
||||
selectSignatureField: (fieldId: string | null) => selectSignatureFieldAction.trigger({ fieldId }),
|
||||
|
||||
// Undo/Redo
|
||||
undo: () => undoAction.trigger(),
|
||||
redo: () => redoAction.trigger(),
|
||||
canUndo: () => statePart.getState().undoStack.length > 0,
|
||||
canRedo: () => statePart.getState().redoStack.length > 0,
|
||||
|
||||
// Loading/Saving state
|
||||
setLoading: (isLoading: boolean) => setLoadingAction.trigger({ isLoading }),
|
||||
setSaving: (isSaving: boolean) => setSavingAction.trigger({ isSaving }),
|
||||
markSaved: () => markSavedAction.trigger(),
|
||||
|
||||
// Discard changes
|
||||
discardChanges: () => discardChangesAction.trigger(),
|
||||
|
||||
// Validation
|
||||
setValidationErrors: (errors: IValidationError[]) => setValidationErrorsAction.trigger({ errors }),
|
||||
clearValidationErrors: () => clearValidationErrorsAction.trigger(),
|
||||
|
||||
// User/Collaboration
|
||||
setCurrentUser: (user: IEditorUser) => setCurrentUserAction.trigger({ user }),
|
||||
setActiveCollaborators: (collaborators: sdInterfaces.IUserPresence[]) => setActiveCollaboratorsAction.trigger({ collaborators }),
|
||||
addCollaborator: (collaborator: sdInterfaces.IUserPresence) => addCollaboratorAction.trigger({ collaborator }),
|
||||
removeCollaborator: (userId: string) => removeCollaboratorAction.trigger({ userId }),
|
||||
updateCollaborator: (userId: string, updates: Partial<sdInterfaces.IUserPresence>) => updateCollaboratorAction.trigger({ userId, updates }),
|
||||
|
||||
// Subscribe to state changes (using new API)
|
||||
subscribe: (callback: (state: IEditorState) => void) => {
|
||||
const subscription = statePart.select().subscribe(callback);
|
||||
return () => subscription.unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get nested value from object by path
|
||||
*/
|
||||
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
||||
const keys = path.split('.');
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current === null || current === undefined) return undefined;
|
||||
if (typeof current !== 'object') return undefined;
|
||||
|
||||
// Handle array index
|
||||
const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/);
|
||||
if (arrayMatch) {
|
||||
const [, arrayKey, index] = arrayMatch;
|
||||
current = (current as Record<string, unknown>)[arrayKey];
|
||||
if (Array.isArray(current)) {
|
||||
current = current[parseInt(index, 10)];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nested value in object by path (immutably)
|
||||
*/
|
||||
function setNestedValue<T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
path: string,
|
||||
value: unknown
|
||||
): T {
|
||||
const keys = path.split('.');
|
||||
const result = { ...obj } as Record<string, unknown>;
|
||||
let current = result;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
// Handle array index
|
||||
const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/);
|
||||
if (arrayMatch) {
|
||||
const [, arrayKey, index] = arrayMatch;
|
||||
const arr = [...((current[arrayKey] as unknown[]) || [])];
|
||||
if (i === keys.length - 2) {
|
||||
arr[parseInt(index, 10)] = value;
|
||||
current[arrayKey] = arr;
|
||||
return result as T;
|
||||
} else {
|
||||
arr[parseInt(index, 10)] = { ...(arr[parseInt(index, 10)] as Record<string, unknown> || {}) };
|
||||
current[arrayKey] = arr;
|
||||
current = arr[parseInt(index, 10)] as Record<string, unknown>;
|
||||
}
|
||||
} else {
|
||||
if (typeof current[key] !== 'object' || current[key] === null) {
|
||||
current[key] = {};
|
||||
} else {
|
||||
current[key] = { ...(current[key] as Record<string, unknown>) };
|
||||
}
|
||||
current = current[key] as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
const lastKey = keys[keys.length - 1];
|
||||
const arrayMatch = lastKey.match(/^(\w+)\[(\d+)\]$/);
|
||||
if (arrayMatch) {
|
||||
const [, arrayKey, index] = arrayMatch;
|
||||
const arr = [...((current[arrayKey] as unknown[]) || [])];
|
||||
arr[parseInt(index, 10)] = value;
|
||||
current[arrayKey] = arr;
|
||||
} else {
|
||||
current[lastKey] = value;
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for editor store
|
||||
*/
|
||||
export type TEditorStore = Awaited<ReturnType<typeof createEditorStore>>;
|
||||
228
ts_web/elements/sdig-contracteditor/types.ts
Normal file
228
ts_web/elements/sdig-contracteditor/types.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* @file types.ts
|
||||
* @description Editor-specific types and event interfaces
|
||||
*/
|
||||
|
||||
import type * as sdInterfaces from '@signature.digital/tools/interfaces';
|
||||
|
||||
// ============================================================================
|
||||
// EDITOR NAVIGATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Available editor sections/tabs
|
||||
*/
|
||||
export type TEditorSection =
|
||||
| 'overview'
|
||||
| 'parties'
|
||||
| 'content'
|
||||
| 'terms'
|
||||
| 'signatures'
|
||||
| 'attachments'
|
||||
| 'collaboration'
|
||||
| 'audit';
|
||||
|
||||
/**
|
||||
* Section configuration
|
||||
*/
|
||||
export interface IEditorSectionConfig {
|
||||
id: TEditorSection;
|
||||
label: string;
|
||||
icon: string;
|
||||
badge?: number | string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default section configurations
|
||||
*/
|
||||
export const EDITOR_SECTIONS: IEditorSectionConfig[] = [
|
||||
{ id: 'overview', label: 'Overview', icon: 'lucide:file-text' },
|
||||
{ id: 'parties', label: 'Parties & Roles', icon: 'lucide:users' },
|
||||
{ id: 'content', label: 'Content', icon: 'lucide:file-edit' },
|
||||
{ id: 'terms', label: 'Terms', icon: 'lucide:calculator' },
|
||||
{ id: 'signatures', label: 'Signatures', icon: 'lucide:pen-tool' },
|
||||
{ id: 'attachments', label: 'Attachments', icon: 'lucide:paperclip' },
|
||||
{ id: 'collaboration', label: 'Collaboration', icon: 'lucide:message-circle' },
|
||||
{ id: 'audit', label: 'Audit & History', icon: 'lucide:history' },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// EDITOR STATE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Current user in the editor
|
||||
*/
|
||||
export interface IEditorUser {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
email: string;
|
||||
avatarUrl?: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor mode
|
||||
*/
|
||||
export type TEditorMode = 'edit' | 'view' | 'review' | 'sign';
|
||||
|
||||
/**
|
||||
* Editor state interface
|
||||
*/
|
||||
export interface IEditorState {
|
||||
// Contract data
|
||||
contract: sdInterfaces.IPortableContract | null;
|
||||
originalContract: sdInterfaces.IPortableContract | null;
|
||||
|
||||
// UI state
|
||||
activeSection: TEditorSection;
|
||||
editorMode: TEditorMode;
|
||||
isDirty: boolean;
|
||||
isSaving: boolean;
|
||||
isLoading: boolean;
|
||||
|
||||
// Selection state
|
||||
selectedParagraphId: string | null;
|
||||
selectedPartyId: string | null;
|
||||
selectedSignatureFieldId: string | null;
|
||||
|
||||
// Collaboration
|
||||
currentUser: IEditorUser | null;
|
||||
activeCollaborators: sdInterfaces.IUserPresence[];
|
||||
|
||||
// Validation
|
||||
validationErrors: IValidationError[];
|
||||
|
||||
// History
|
||||
undoStack: IContractChange[];
|
||||
redoStack: IContractChange[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial editor state factory
|
||||
*/
|
||||
export function createInitialEditorState(): IEditorState {
|
||||
return {
|
||||
contract: null,
|
||||
originalContract: null,
|
||||
activeSection: 'overview',
|
||||
editorMode: 'edit',
|
||||
isDirty: false,
|
||||
isSaving: false,
|
||||
isLoading: false,
|
||||
selectedParagraphId: null,
|
||||
selectedPartyId: null,
|
||||
selectedSignatureFieldId: null,
|
||||
currentUser: null,
|
||||
activeCollaborators: [],
|
||||
validationErrors: [],
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CHANGE TRACKING
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Contract change for undo/redo
|
||||
*/
|
||||
export interface IContractChange {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
path: string;
|
||||
previousValue: unknown;
|
||||
newValue: unknown;
|
||||
description: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error
|
||||
*/
|
||||
export interface IValidationError {
|
||||
path: string;
|
||||
message: string;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
fieldLabel?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVENTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Contract change event detail
|
||||
*/
|
||||
export interface IContractChangeEventDetail {
|
||||
path: string;
|
||||
value: unknown;
|
||||
previousValue?: unknown;
|
||||
source?: 'user' | 'collaboration' | 'system';
|
||||
}
|
||||
|
||||
/**
|
||||
* Section change event detail
|
||||
*/
|
||||
export interface ISectionChangeEventDetail {
|
||||
section: TEditorSection;
|
||||
previousSection: TEditorSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save event detail
|
||||
*/
|
||||
export interface ISaveEventDetail {
|
||||
contract: sdInterfaces.IPortableContract;
|
||||
isDraft: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom event types
|
||||
*/
|
||||
export interface IEditorEvents {
|
||||
'contract-change': CustomEvent<IContractChangeEventDetail>;
|
||||
'section-change': CustomEvent<ISectionChangeEventDetail>;
|
||||
'contract-save': CustomEvent<ISaveEventDetail>;
|
||||
'contract-discard': CustomEvent<void>;
|
||||
'validation-error': CustomEvent<IValidationError[]>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Deep path type for nested object access
|
||||
*/
|
||||
export type TDeepPath<T, K extends keyof T = keyof T> = K extends string
|
||||
? T[K] extends Record<string, unknown>
|
||||
? `${K}` | `${K}.${TDeepPath<T[K]>}`
|
||||
: `${K}`
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Contract field path
|
||||
*/
|
||||
export type TContractPath = string; // Simplified for runtime use
|
||||
|
||||
/**
|
||||
* Field metadata for UI rendering
|
||||
*/
|
||||
export interface IFieldMetadata {
|
||||
path: TContractPath;
|
||||
label: string;
|
||||
description?: string;
|
||||
required: boolean;
|
||||
type: 'text' | 'textarea' | 'number' | 'date' | 'select' | 'multiselect' | 'checkbox' | 'custom';
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
validation?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
};
|
||||
}
|
||||
1
ts_web/elements/sdig-signbox/index.ts
Normal file
1
ts_web/elements/sdig-signbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-signbox.js';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as plugins from '../../plugins.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
1
ts_web/elements/sdig-signpad/index.ts
Normal file
1
ts_web/elements/sdig-signpad/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-signpad.js';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as plugins from '../../plugins.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
Reference in New Issue
Block a user