/**
* @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:FileEdit', color: '#f59e0b' },
{ id: 'review', label: 'Review', icon: 'lucide:Eye', color: '#3b82f6' },
{ id: 'pending', label: 'Pending Signatures', icon: 'lucide:PenTool', color: '#8b5cf6' },
{ id: 'signed', label: 'Signed', icon: 'lucide:CheckCircle', color: '#10b981' },
{ id: 'executed', label: 'Executed', icon: 'lucide:ShieldCheck', color: '#059669' },
];
// Event type configuration
const EVENT_TYPES = {
created: { icon: 'lucide:PlusCircle', color: '#10b981', label: 'Created' },
updated: { icon: 'lucide:Pencil', color: '#3b82f6', label: 'Updated' },
status_change: { icon: 'lucide:ArrowRightCircle', color: '#8b5cf6', label: 'Status Changed' },
signature: { icon: 'lucide:PenTool', color: '#10b981', label: 'Signature' },
comment: { icon: 'lucide:MessageCircle', color: '#f59e0b', label: 'Comment' },
attachment: { icon: 'lucide:Paperclip', color: '#6366f1', label: 'Attachment' },
viewed: { icon: 'lucide:Eye', color: '#6b7280', label: 'Viewed' },
shared: { icon: 'lucide:Share2', color: '#ec4899', label: 'Shared' },
};
@customElement('sdig-contract-audit')
export class SdigContractAudit extends DeesElement {
// ============================================================================
// STATIC
// ============================================================================
public static demo = () => html`
`;
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`
No contract loaded
`;
}
const currentStatusIndex = this.getCurrentStatusIndex();
const filteredEvents = this.getFilteredEvents();
const stats = this.getEventStats();
return html`
Contract Lifecycle
${STATUS_WORKFLOW.map((status, index) => html`
${index < STATUS_WORKFLOW.length - 1
? html`
`
: ''}
`)}
${stats.total}
Total Events
${stats.signatures}
Signatures
${stats.comments}
Comments
(this.searchQuery = (e.target as HTMLInputElement).value)}
/>
${filteredEvents.length > 0
? html`
${filteredEvents.map((event) => this.renderTimelineItem(event))}
`
: html`
No Events Found
No activity matches your current filters
`}
`;
}
private renderTimelineItem(event: IAuditEvent): TemplateResult {
const config = this.getEventConfig(event.type);
return html`
${event.userName.charAt(0)}
${event.userName}
${event.description}
${event.details
? html`
${event.details.field
? html`
Field:
${event.details.field}
`
: ''}
${event.details.oldValue && event.details.newValue
? html`
${event.details.oldValue}
→
${event.details.newValue}
`
: ''}
${event.details.attachmentName
? html`
File:
${event.details.attachmentName}
`
: ''}
`
: ''}
`;
}
}