/** * TEMS Contract Negotiations List Component * * Displays contract negotiations retrieved from the EDC connector. * Shows negotiation status, agreement details, and allows viewing contracts. * Uses sib-core's DataspaceConnectorStore for all operations. * * @customElement solid-tems-negotiations-list */ import "@src/initializer"; import * as utils from "@helpers"; import { localized, msg } from "@lit/localize"; import { OrbitDSPComponent } from "@startinblox/solid-tems-shared"; import { css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js"; type NegotiationState = | "REQUESTING" | "REQUESTED" | "OFFERING" | "OFFERED" | "ACCEPTING" | "ACCEPTED" | "AGREEING" | "AGREED" | "VERIFYING" | "VERIFIED" | "FINALIZING" | "FINALIZED" | "TERMINATING" | "TERMINATED"; interface ContractNegotiation { "@id": string; "@type"?: string; state: NegotiationState; counterPartyId?: string; counterPartyAddress?: string; contractAgreementId?: string; assetId?: string; errorDetail?: string; createdAt?: number; type?: string; } interface ContractAgreement { "@type": "ContractAgreement"; "@id": string; assetId: string; policy?: any; contractSigningDate?: number; consumerId?: string; providerId?: string; } @customElement("solid-tems-negotiations-list") @localized() export class TemsNegotiationsList extends OrbitDSPComponent { constructor() { super(); utils.setupCacheInvalidation(this, { keywords: ["negotiations", "contracts"], }); } static styles = css` :host { display: block; } .management-container { padding: 1rem; } .toolbar { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; } .toolbar tems-search-bar { flex: 1; min-width: 200px; } .toolbar-actions { display: flex; gap: 0.5rem; align-items: center; } .filter-select { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem; } .negotiations-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 1rem; } .negotiation-card { background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); cursor: pointer; transition: box-shadow 0.2s, border-color 0.2s; } .negotiation-card:hover { border-color: #e91e63; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .negotiation-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem; } .negotiation-id { font-size: 0.75rem; color: #666; word-break: break-all; flex: 1; } .state-badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; margin-left: 0.5rem; white-space: nowrap; } .state-requesting, .state-requested, .state-offering, .state-offered { background-color: #fff3e0; color: #ef6c00; } .state-accepting, .state-accepted, .state-agreeing, .state-agreed { background-color: #e3f2fd; color: #1565c0; } .state-verifying, .state-verified, .state-finalizing { background-color: #f3e5f5; color: #7b1fa2; } .state-finalized { background-color: #e8f5e9; color: #2e7d32; } .state-terminating, .state-terminated { background-color: #ffebee; color: #c62828; } .negotiation-info { font-size: 0.875rem; color: #666; margin-bottom: 0.5rem; } .info-row { display: flex; gap: 0.5rem; margin-bottom: 0.25rem; } .info-label { font-weight: 500; min-width: 100px; color: #888; } .info-value { word-break: break-all; } .agreement-badge { display: inline-block; padding: 0.25rem 0.5rem; background: #e8f5e9; color: #2e7d32; border-radius: 4px; font-size: 0.75rem; margin-top: 0.5rem; } .error-detail { margin-top: 0.5rem; padding: 0.5rem; background: #ffebee; color: #c62828; border-radius: 4px; font-size: 0.75rem; } .created-date { font-size: 0.7rem; color: #999; margin-top: 0.5rem; } .empty-state { text-align: center; padding: 3rem; color: #666; } .empty-state h3 { margin: 0.5rem 0; } .empty-state p { margin: 0; } .empty-icon { display: block; font-size: 3rem; line-height: 1; margin-bottom: 1rem; } .loading { text-align: center; padding: 2rem; color: #666; } .message { padding: 1rem; border-radius: 4px; margin-bottom: 1rem; } .message-error { background-color: #ffebee; color: #c62828; border: 1px solid #ef9a9a; } .message-success { background-color: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; } .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; border-radius: 8px; max-width: 700px; width: 90%; max-height: 90vh; overflow-y: auto; } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid #e0e0e0; } .modal-header h3 { margin: 0; } .modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #666; } .modal-body { padding: 1.5rem; } .detail-section { display: flex; flex-direction: column; gap: 1rem; } .detail-row { display: flex; flex-direction: column; gap: 0.25rem; } .detail-row strong { color: #666; font-size: 0.875rem; } .detail-value { font-family: monospace; background: #f5f5f5; padding: 0.5rem; border-radius: 4px; word-break: break-all; font-size: 0.85rem; } .agreement-section { margin-top: 1rem; padding: 1rem; background: #f5f5f5; border-radius: 8px; } .agreement-section h4 { margin: 0 0 1rem 0; color: #2e7d32; } .policy-preview { background: white; padding: 0.75rem; border-radius: 4px; font-size: 0.75rem; font-family: monospace; max-height: 200px; overflow-y: auto; white-space: pre-wrap; } .form-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #e0e0e0; } .stats-bar { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; } .stat-item { padding: 0.5rem 1rem; background: #f5f5f5; border-radius: 4px; font-size: 0.875rem; } .stat-value { font-weight: 600; margin-left: 0.25rem; } `; @property({ attribute: "header", type: String }) header = "Contract Negotiations"; @property({ attribute: "auto-refresh", type: Number }) autoRefreshInterval = 0; // 0 = disabled, otherwise seconds @state() private negotiations: ContractNegotiation[] = []; @state() private isLoading = false; @state() private viewingNegotiation: ContractNegotiation | null = null; @state() private viewingAgreement: ContractAgreement | null = null; @state() private searchTerm = ""; @state() private stateFilter: NegotiationState | "ALL" = "ALL"; @state() private errorMessage = ""; @state() private successMessage = ""; private refreshTimer?: number; override async _afterAttach() { await super._afterAttach(); if (this.storeService) { this.loadNegotiations(); this.setupAutoRefresh(); } } override disconnectedCallback() { super.disconnectedCallback(); if (this.refreshTimer) { clearInterval(this.refreshTimer); } } private setupAutoRefresh() { if (this.autoRefreshInterval > 0) { this.refreshTimer = window.setInterval(() => { this.loadNegotiations(); }, this.autoRefreshInterval * 1000); } } private async loadNegotiations() { if (!this.storeService) { this.showError("Store not initialized. Check connector configuration."); return; } this.isLoading = true; this.clearMessages(); try { const negotiations = await this.storeService.getAllContractNegotiations(); this.negotiations = Array.isArray(negotiations) ? negotiations : []; } catch (error: any) { console.error("Failed to load contract negotiations:", error); this.showError(`Failed to load contract negotiations: ${error.message}`); } finally { this.isLoading = false; } } private async loadAgreement(negotiationId: string) { if (!this.storeService) return; try { const agreement = await this.storeService.getContractAgreement(negotiationId); this.viewingAgreement = agreement; } catch (error: any) { console.error("Failed to load contract agreement:", error); this.showError(`Failed to load agreement: ${error.message}`); } } private showError(message: string) { this.errorMessage = message; this.successMessage = ""; setTimeout(() => { this.errorMessage = ""; }, 5000); } // private showSuccess(message: string) { // this.successMessage = message; // this.errorMessage = ""; // setTimeout(() => { // this.successMessage = ""; // }, 3000); // } private clearMessages() { this.errorMessage = ""; this.successMessage = ""; } private _handleSearch(e: CustomEvent) { this.searchTerm = e.detail || ""; } private getStateBadgeClass(state: NegotiationState): string { return `state-${state.toLowerCase()}`; } private get filteredNegotiations() { let filtered = [...this.negotiations]; // Filter by state if (this.stateFilter !== "ALL") { filtered = filtered.filter((n) => n.state === this.stateFilter); } // Filter by search term if (this.searchTerm) { const term = this.searchTerm.toLowerCase(); filtered = filtered.filter((negotiation) => { const id = negotiation["@id"]?.toLowerCase() || ""; const counterParty = negotiation.counterPartyId?.toLowerCase() || ""; const agreementId = negotiation.contractAgreementId?.toLowerCase() || ""; return ( id.includes(term) || counterParty.includes(term) || agreementId.includes(term) ); }); } // Sort by created date descending (newest first) filtered.sort((a, b) => { const aTime = a.createdAt || 0; const bTime = b.createdAt || 0; return bTime - aTime; }); return filtered; } private get stateStats() { const stats: Record = { FINALIZED: 0, TERMINATED: 0, IN_PROGRESS: 0, }; this.negotiations.forEach((n) => { if (n.state === "FINALIZED") { stats.FINALIZED++; } else if (n.state === "TERMINATED") { stats.TERMINATED++; } else { stats.IN_PROGRESS++; } }); return stats; } render() { const stats = this.stateStats; return html`
`} ?disabled=${this.isLoading} @clicked=${() => this.loadNegotiations()} >
${this.renderMessages()}
${msg("Total")}:${this.negotiations.length}
${msg("Finalized")}:${stats.FINALIZED}
${msg("In Progress")}:${stats.IN_PROGRESS}
${msg("Terminated")}:${stats.TERMINATED}
${this.viewingNegotiation ? this.renderDetailView() : nothing} ${this.isLoading ? html`
${msg("Loading negotiations...")}
` : this.renderNegotiationsList()}
`; } private renderMessages() { return html` ${this.errorMessage ? html`
${this.errorMessage}
` : nothing} ${this.successMessage ? html`
${this.successMessage}
` : nothing} `; } private renderNegotiationsList() { const negotiations = this.filteredNegotiations; if (negotiations.length === 0) { return html`
📋

${msg("No negotiations found")}

${msg("Contract negotiations will appear here when initiated.")}

`; } return html`
${repeat( negotiations, (negotiation) => negotiation["@id"], (negotiation) => this.renderNegotiationCard(negotiation), )}
`; } private renderNegotiationCard(negotiation: ContractNegotiation) { return html`
{ this.viewingNegotiation = negotiation; this.viewingAgreement = null; if ( negotiation.state === "FINALIZED" && negotiation.contractAgreementId ) { this.loadAgreement(negotiation["@id"]); } }} >
ID: ${negotiation["@id"]}
${negotiation.state}
${negotiation.assetId ? html`
${msg("Asset")}: ${negotiation.assetId}
` : nothing} ${negotiation.counterPartyId ? html`
${msg("Counter Party")}: ${negotiation.counterPartyId}
` : nothing} ${negotiation.type ? html`
${msg("Type")}: ${negotiation.type}
` : nothing}
${negotiation.contractAgreementId ? html`
${msg("Agreement")}: ${negotiation.contractAgreementId.substring(0, 20)}...
` : nothing} ${negotiation.errorDetail ? html`
${msg("Error")}: ${negotiation.errorDetail}
` : nothing} ${negotiation.createdAt ? html`
${msg("Created")}: ${new Date(negotiation.createdAt).toLocaleString()}
` : nothing}
`; } private renderDetailView() { if (!this.viewingNegotiation) return nothing; const negotiation = this.viewingNegotiation; return html` `; } private renderAgreementDetails() { if (!this.viewingAgreement) return nothing; const agreement = this.viewingAgreement; return html`

${msg("Contract Agreement")}

${msg("Agreement ID")}: ${agreement["@id"]}
${msg("Asset ID")}: ${agreement.assetId}
${agreement.consumerId ? html`
${msg("Consumer ID")}: ${agreement.consumerId}
` : nothing} ${agreement.providerId ? html`
${msg("Provider ID")}: ${agreement.providerId}
` : nothing} ${agreement.contractSigningDate ? html`
${msg("Signing Date")}: ${new Date( agreement.contractSigningDate, ).toLocaleString()}
` : nothing} ${agreement.policy ? html`
${msg("Policy")}:
${JSON.stringify(agreement.policy, null, 2)}
` : nothing}
`; } }