Files
binect-js/explorer/index.html
tegwick b9aebb42f1 Add Binect SDK implementation, Explorer, and test suite
SDK (@binect/js):
- BinectClient with domain sub-clients (documents, sendings, accounts,
  attachments, invoices)
- HTTP Basic Auth, native fetch only (no runtime dependencies)
- TypeScript types matching Binect API vocabulary
- Status predicates and polling helpers in helpers.ts
- Structured error handling (BinectApiError, BinectAuthError)

Explorer:
- Standalone browser-based API explorer (explorer/index.html)
- Interactive testing without code

Tests:
- Unit tests for client, types, errors, helpers, http
- E2E tests for upload/delete and send/cancel workflows

Also includes:
- Architecture Decision Records (ADRs)
- Example DIN 5008 letter PDFs for testing
- API specification research notes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 23:10:34 +01:00

1336 lines
43 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Binect Explorer</title>
<style>
:root {
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--danger-color: #dc2626;
--danger-hover: #b91c1c;
--success-color: #16a34a;
--warning-color: #ca8a04;
--bg-color: #f8fafc;
--card-bg: #ffffff;
--text-color: #1e293b;
--text-muted: #64748b;
--border-color: #e2e8f0;
--code-bg: #f1f5f9;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
padding: 16px 20px;
margin-bottom: 20px;
}
header h1 {
font-size: 1.5rem;
color: var(--primary-color);
}
header p {
color: var(--text-muted);
font-size: 0.875rem;
}
.grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
}
@media (max-width: 900px) {
.grid {
grid-template-columns: 1fr;
}
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.card h2 {
font-size: 1rem;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
font-weight: 500;
margin-bottom: 4px;
font-size: 0.875rem;
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="file"],
select,
textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.875rem;
font-family: inherit;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
}
.btn-primary:disabled {
background-color: var(--text-muted);
cursor: not-allowed;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: var(--danger-hover);
}
.btn-secondary {
background-color: var(--border-color);
color: var(--text-color);
}
.btn-secondary:hover {
background-color: #cbd5e1;
}
.btn-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.status-1 { background-color: #fef3c7; color: #92400e; }
.status-2 { background-color: #d1fae5; color: #065f46; }
.status-3 { background-color: #dbeafe; color: #1e40af; }
.status-4 { background-color: #e0e7ff; color: #3730a3; }
.status-5 { background-color: #d1fae5; color: #065f46; }
.status-6 { background-color: #f3f4f6; color: #374151; }
.status-7 { background-color: #fee2e2; color: #991b1b; }
.alert {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.875rem;
}
.alert-error {
background-color: #fee2e2;
color: #991b1b;
border: 1px solid #fecaca;
}
.alert-success {
background-color: #d1fae5;
color: #065f46;
border: 1px solid #a7f3d0;
}
.alert-warning {
background-color: #fef3c7;
color: #92400e;
border: 1px solid #fde68a;
}
.connected-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
}
.connected-indicator .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--text-muted);
}
.connected-indicator.connected .dot {
background-color: var(--success-color);
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 16px;
}
.tab {
padding: 8px 16px;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-muted);
cursor: pointer;
font-size: 0.875rem;
}
.tab:hover {
color: var(--text-color);
}
.tab.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.document-list {
max-height: 400px;
overflow-y: auto;
}
.document-item {
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-bottom: 8px;
}
.document-item:hover {
background-color: var(--bg-color);
}
.document-item h4 {
font-size: 0.875rem;
margin-bottom: 4px;
}
.document-item p {
font-size: 0.75rem;
color: var(--text-muted);
}
.json-view {
background-color: var(--code-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 12px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.75rem;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background-color: var(--card-bg);
border-radius: 8px;
padding: 24px;
max-width: 500px;
width: 90%;
}
.modal h3 {
margin-bottom: 16px;
}
.modal .btn-group {
margin-top: 20px;
justify-content: flex-end;
}
.hidden {
display: none !important;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
width: auto;
}
.info-text {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 4px;
}
.account-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
.account-info-item {
text-align: center;
padding: 16px;
background-color: var(--bg-color);
border-radius: 4px;
}
.account-info-item .value {
font-size: 1.5rem;
font-weight: 600;
color: var(--primary-color);
}
.account-info-item .label {
font-size: 0.75rem;
color: var(--text-muted);
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>Binect Explorer</h1>
<p>Interactive tool for learning and testing the Binect API</p>
</div>
</header>
<main class="container">
<div class="grid">
<!-- Sidebar -->
<aside>
<!-- Credentials Card -->
<div class="card">
<h2>API Credentials</h2>
<div class="connected-indicator" id="connection-status">
<span class="dot"></span>
<span>Not connected</span>
</div>
<form id="credentials-form">
<div class="form-group">
<label for="username">Username (Email)</label>
<input type="email" id="username" placeholder="your@email.com" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" required>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="remember-credentials">
<label for="remember-credentials">Remember credentials (local storage)</label>
</div>
<div class="btn-group">
<button type="submit" class="btn-primary" id="connect-btn">Connect</button>
<button type="button" class="btn-secondary" id="disconnect-btn" disabled>Disconnect</button>
</div>
</form>
</div>
<!-- Account Info Card -->
<div class="card hidden" id="account-card">
<h2>Account</h2>
<div id="account-info" class="account-info">
<div class="account-info-item">
<div class="value" id="account-credit">-</div>
<div class="label">Credit Balance</div>
</div>
<div class="account-info-item">
<div class="value" id="account-debitor">-</div>
<div class="label">Debitor Number</div>
</div>
</div>
</div>
<!-- Profiles Card -->
<div class="card">
<h2>Use Case Profiles</h2>
<div class="form-group">
<label for="profile-select">Load Profile</label>
<select id="profile-select">
<option value="">-- Select Profile --</option>
</select>
</div>
<div class="btn-group">
<button class="btn-secondary" id="save-profile-btn">Save Current</button>
<button class="btn-secondary" id="export-profiles-btn">Export</button>
<button class="btn-secondary" id="import-profiles-btn">Import</button>
</div>
<input type="file" id="import-file" accept=".json" class="hidden">
</div>
</aside>
<!-- Main Content -->
<section>
<div class="card">
<div class="tabs">
<button class="tab active" data-tab="documents">Documents</button>
<button class="tab" data-tab="upload">Upload</button>
<button class="tab" data-tab="sendings">Sendings</button>
<button class="tab" data-tab="attachments">Attachments</button>
<button class="tab" data-tab="response">Raw Response</button>
</div>
<!-- Documents Tab -->
<div class="tab-content active" id="tab-documents">
<div class="btn-group" style="margin-bottom: 16px;">
<button class="btn-primary" id="load-documents-btn" disabled>Load Shippable</button>
<button class="btn-secondary" id="load-errors-btn" disabled>Load Errors</button>
<button class="btn-secondary" id="refresh-documents-btn" disabled>Refresh</button>
</div>
<div id="documents-alert"></div>
<div class="document-list" id="documents-list">
<p class="info-text">Connect and load documents to see them here.</p>
</div>
</div>
<!-- Upload Tab -->
<div class="tab-content" id="tab-upload">
<form id="upload-form">
<div class="form-group">
<label for="pdf-file">PDF File</label>
<input type="file" id="pdf-file" accept=".pdf" required>
<p class="info-text">Maximum file size: 12 MB</p>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="upload-color">
<label for="upload-color">Color printing</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="upload-duplex" checked>
<label for="upload-duplex">Duplex (double-sided)</label>
</div>
<div class="form-group">
<label for="upload-envelope">Envelope Type</label>
<select id="upload-envelope">
<option value="DINLANG">DIN Lang (Standard)</option>
<option value="C4">C4 (Large)</option>
</select>
</div>
<div class="form-group">
<label for="upload-franking">Franking Type</label>
<select id="upload-franking">
<option value="STANDARD_FRANKING">Standard Franking</option>
<option value="DV_FRANKING">DV Franking</option>
<option value="UNSPECIFIED">Unspecified</option>
</select>
</div>
<div id="upload-alert"></div>
<div class="btn-group">
<button type="submit" class="btn-primary" id="upload-btn" disabled>Upload Document</button>
</div>
</form>
</div>
<!-- Sendings Tab -->
<div class="tab-content" id="tab-sendings">
<div class="btn-group" style="margin-bottom: 16px;">
<button class="btn-primary" id="load-sendings-btn" disabled>Load Sendings</button>
</div>
<div id="sendings-alert"></div>
<div class="document-list" id="sendings-list">
<p class="info-text">Connect and load sendings to see them here.</p>
</div>
</div>
<!-- Attachments Tab -->
<div class="tab-content" id="tab-attachments">
<div class="btn-group" style="margin-bottom: 16px;">
<button class="btn-primary" id="load-attachments-btn" disabled>Load Attachments</button>
</div>
<div id="attachments-alert"></div>
<div class="document-list" id="attachments-list">
<p class="info-text">Connect and load attachments to see them here.</p>
</div>
</div>
<!-- Raw Response Tab -->
<div class="tab-content" id="tab-response">
<p class="info-text">Last API response will be shown here.</p>
<div class="json-view" id="response-view">
// No response yet
</div>
</div>
</div>
<!-- Document Detail Card -->
<div class="card hidden" id="document-detail-card">
<h2>Document Details</h2>
<div id="document-detail"></div>
<div class="btn-group" style="margin-top: 16px;">
<button class="btn-primary" id="send-document-btn">Send Document</button>
<button class="btn-secondary" id="preview-document-btn">Preview PDF</button>
<button class="btn-danger" id="delete-document-btn">Delete</button>
</div>
</div>
</section>
</div>
</main>
<!-- Confirmation Modal -->
<div class="modal-overlay hidden" id="confirm-modal">
<div class="modal">
<h3 id="confirm-title">Confirm Action</h3>
<p id="confirm-message">Are you sure you want to proceed?</p>
<div class="alert alert-warning" id="confirm-warning">
This action will send physical mail and incur charges.
</div>
<div class="btn-group">
<button class="btn-secondary" id="confirm-cancel">Cancel</button>
<button class="btn-danger" id="confirm-ok">Confirm</button>
</div>
</div>
</div>
<script type="module">
// =========================================================================
// Binect Explorer Application
// =========================================================================
// Import SDK from built distribution
// In production, this would be: import { BinectClient, ... } from '@binect/js';
// For local development, we inline a minimal client
// -------------------------------------------------------------------------
// Minimal inline client for standalone use
// -------------------------------------------------------------------------
const DEFAULT_BASE_URL = 'https://app.binect.de/binectapi/v1';
function encodeBasicAuth(username, password) {
return btoa(`${username}:${password}`);
}
class BinectApiError extends Error {
constructor(message, status, endpoint, method, response = null) {
super(message);
this.name = 'BinectApiError';
this.status = status;
this.endpoint = endpoint;
this.method = method;
this.response = response;
}
}
class HttpClient {
constructor(config) {
this.baseUrl = config.baseUrl;
this.authHeader = `Basic ${encodeBasicAuth(config.username, config.password)}`;
}
async request(options) {
const url = this.buildUrl(options.path, options.query);
const headers = {
Authorization: this.authHeader,
Accept: 'application/json',
};
const init = { method: options.method, headers };
if (options.body !== undefined) {
headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(options.body);
}
const response = await fetch(url, init);
if (!response.ok) {
let errorResponse = null;
try {
const text = await response.text();
if (text) errorResponse = JSON.parse(text);
} catch {}
const message = errorResponse?.message ?? errorResponse?.error ?? `HTTP ${response.status} error`;
throw new BinectApiError(message, response.status, options.path, options.method, errorResponse);
}
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return undefined;
}
const text = await response.text();
return text ? JSON.parse(text) : undefined;
}
async requestRaw(options) {
const url = this.buildUrl(options.path, options.query);
const headers = { Authorization: this.authHeader };
const response = await fetch(url, { method: options.method, headers });
if (!response.ok) {
throw new BinectApiError(`HTTP ${response.status}`, response.status, options.path, options.method);
}
return response;
}
buildUrl(path, query) {
const url = new URL(path, this.baseUrl);
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
}
return url.toString();
}
}
// Simple API wrapper
class BinectClient {
constructor(config) {
this.http = new HttpClient({
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
username: config.username,
password: config.password,
});
}
// Documents
async uploadDocument(options) {
return this.http.request({ method: 'POST', path: '/documents', body: options });
}
async listDocuments(pagination) {
return this.http.request({ method: 'GET', path: '/documents', query: pagination });
}
async listDocumentErrors(pagination) {
return this.http.request({ method: 'GET', path: '/documents/errors', query: pagination });
}
async getDocument(id) {
return this.http.request({ method: 'GET', path: `/documents/${encodeURIComponent(id)}` });
}
async deleteDocument(id) {
return this.http.request({ method: 'DELETE', path: `/documents/${encodeURIComponent(id)}` });
}
async getDocumentPdf(id) {
return this.http.requestRaw({ method: 'GET', path: `/documents/${encodeURIComponent(id)}/pdf` });
}
// Sendings
async listSendings(pagination) {
return this.http.request({ method: 'GET', path: '/sendings', query: pagination });
}
async sendDocument(id) {
return this.http.request({ method: 'POST', path: `/sendings/${encodeURIComponent(id)}` });
}
async cancelSending(id) {
return this.http.request({ method: 'PUT', path: `/sendings/${encodeURIComponent(id)}` });
}
// Attachments
async listAttachments(pagination) {
return this.http.request({ method: 'GET', path: '/attachments', query: pagination });
}
// Account
async getAccount() {
return this.http.request({ method: 'GET', path: '/accounts' });
}
}
// -------------------------------------------------------------------------
// Status helpers
// -------------------------------------------------------------------------
const STATUS_LABELS = {
1: 'In Preparation',
2: 'Shippable',
3: 'Production Queue',
4: 'Printing',
5: 'Sent',
6: 'Canceled',
7: 'Erroneous',
};
function getStatusLabel(status) {
return STATUS_LABELS[status] ?? 'Unknown';
}
// -------------------------------------------------------------------------
// Application State
// -------------------------------------------------------------------------
let client = null;
let lastResponse = null;
let selectedDocumentId = null;
let profiles = {};
// -------------------------------------------------------------------------
// DOM Elements
// -------------------------------------------------------------------------
const elements = {
// Credentials
credentialsForm: document.getElementById('credentials-form'),
usernameInput: document.getElementById('username'),
passwordInput: document.getElementById('password'),
rememberCheckbox: document.getElementById('remember-credentials'),
connectBtn: document.getElementById('connect-btn'),
disconnectBtn: document.getElementById('disconnect-btn'),
connectionStatus: document.getElementById('connection-status'),
// Account
accountCard: document.getElementById('account-card'),
accountCredit: document.getElementById('account-credit'),
accountDebitor: document.getElementById('account-debitor'),
// Tabs
tabs: document.querySelectorAll('.tab'),
tabContents: document.querySelectorAll('.tab-content'),
// Documents
loadDocumentsBtn: document.getElementById('load-documents-btn'),
loadErrorsBtn: document.getElementById('load-errors-btn'),
refreshDocumentsBtn: document.getElementById('refresh-documents-btn'),
documentsList: document.getElementById('documents-list'),
documentsAlert: document.getElementById('documents-alert'),
// Upload
uploadForm: document.getElementById('upload-form'),
pdfFileInput: document.getElementById('pdf-file'),
uploadColorCheckbox: document.getElementById('upload-color'),
uploadDuplexCheckbox: document.getElementById('upload-duplex'),
uploadEnvelopeSelect: document.getElementById('upload-envelope'),
uploadFrankingSelect: document.getElementById('upload-franking'),
uploadBtn: document.getElementById('upload-btn'),
uploadAlert: document.getElementById('upload-alert'),
// Sendings
loadSendingsBtn: document.getElementById('load-sendings-btn'),
sendingsList: document.getElementById('sendings-list'),
sendingsAlert: document.getElementById('sendings-alert'),
// Attachments
loadAttachmentsBtn: document.getElementById('load-attachments-btn'),
attachmentsList: document.getElementById('attachments-list'),
attachmentsAlert: document.getElementById('attachments-alert'),
// Response
responseView: document.getElementById('response-view'),
// Document Detail
documentDetailCard: document.getElementById('document-detail-card'),
documentDetail: document.getElementById('document-detail'),
sendDocumentBtn: document.getElementById('send-document-btn'),
previewDocumentBtn: document.getElementById('preview-document-btn'),
deleteDocumentBtn: document.getElementById('delete-document-btn'),
// Modal
confirmModal: document.getElementById('confirm-modal'),
confirmTitle: document.getElementById('confirm-title'),
confirmMessage: document.getElementById('confirm-message'),
confirmWarning: document.getElementById('confirm-warning'),
confirmCancelBtn: document.getElementById('confirm-cancel'),
confirmOkBtn: document.getElementById('confirm-ok'),
// Profiles
profileSelect: document.getElementById('profile-select'),
saveProfileBtn: document.getElementById('save-profile-btn'),
exportProfilesBtn: document.getElementById('export-profiles-btn'),
importProfilesBtn: document.getElementById('import-profiles-btn'),
importFileInput: document.getElementById('import-file'),
};
// -------------------------------------------------------------------------
// Utility Functions
// -------------------------------------------------------------------------
function showAlert(container, message, type = 'error') {
container.innerHTML = `<div class="alert alert-${type}">${escapeHtml(message)}</div>`;
}
function clearAlert(container) {
container.innerHTML = '';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatJson(obj) {
return JSON.stringify(obj, null, 2);
}
function updateResponse(data) {
lastResponse = data;
elements.responseView.textContent = formatJson(data);
}
function setConnected(connected) {
if (connected) {
elements.connectionStatus.classList.add('connected');
elements.connectionStatus.querySelector('span:last-child').textContent = 'Connected';
elements.connectBtn.disabled = true;
elements.disconnectBtn.disabled = false;
elements.accountCard.classList.remove('hidden');
// Enable buttons
elements.loadDocumentsBtn.disabled = false;
elements.loadErrorsBtn.disabled = false;
elements.refreshDocumentsBtn.disabled = false;
elements.uploadBtn.disabled = false;
elements.loadSendingsBtn.disabled = false;
elements.loadAttachmentsBtn.disabled = false;
} else {
elements.connectionStatus.classList.remove('connected');
elements.connectionStatus.querySelector('span:last-child').textContent = 'Not connected';
elements.connectBtn.disabled = false;
elements.disconnectBtn.disabled = true;
elements.accountCard.classList.add('hidden');
// Disable buttons
elements.loadDocumentsBtn.disabled = true;
elements.loadErrorsBtn.disabled = true;
elements.refreshDocumentsBtn.disabled = true;
elements.uploadBtn.disabled = true;
elements.loadSendingsBtn.disabled = true;
elements.loadAttachmentsBtn.disabled = true;
}
}
async function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
const base64 = result.split(',')[1];
resolve(base64);
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
function showConfirmModal(title, message, showWarning = false) {
elements.confirmTitle.textContent = title;
elements.confirmMessage.textContent = message;
elements.confirmWarning.classList.toggle('hidden', !showWarning);
elements.confirmModal.classList.remove('hidden');
return new Promise((resolve) => {
const handleOk = () => {
cleanup();
resolve(true);
};
const handleCancel = () => {
cleanup();
resolve(false);
};
const cleanup = () => {
elements.confirmModal.classList.add('hidden');
elements.confirmOkBtn.removeEventListener('click', handleOk);
elements.confirmCancelBtn.removeEventListener('click', handleCancel);
};
elements.confirmOkBtn.addEventListener('click', handleOk);
elements.confirmCancelBtn.addEventListener('click', handleCancel);
});
}
// -------------------------------------------------------------------------
// Render Functions
// -------------------------------------------------------------------------
function renderDocumentsList(documents) {
if (!documents || documents.length === 0) {
elements.documentsList.innerHTML = '<p class="info-text">No documents found.</p>';
return;
}
elements.documentsList.innerHTML = documents.map(doc => `
<div class="document-item" data-id="${escapeHtml(doc.documentId)}">
<h4>
${escapeHtml(doc.documentId)}
<span class="status-badge status-${doc.status}">${getStatusLabel(doc.status)}</span>
</h4>
<p>${doc.pageCount} page(s) • ${doc.color ? 'Color' : 'B&W'}${doc.duplex ? 'Duplex' : 'Simplex'}${doc.envelope}</p>
${doc.price ? `<p>Price: €${doc.price.toFixed(2)}</p>` : ''}
</div>
`).join('');
// Add click handlers
elements.documentsList.querySelectorAll('.document-item').forEach(item => {
item.addEventListener('click', () => selectDocument(item.dataset.id));
});
}
function renderSendingsList(sendings) {
if (!sendings || sendings.length === 0) {
elements.sendingsList.innerHTML = '<p class="info-text">No sendings found.</p>';
return;
}
elements.sendingsList.innerHTML = sendings.map(sending => `
<div class="document-item">
<h4>
${escapeHtml(sending.documentId)}
<span class="status-badge status-${sending.status}">${getStatusLabel(sending.status)}</span>
</h4>
${sending.price ? `<p>Price: €${sending.price.toFixed(2)}</p>` : ''}
${sending.trackingId ? `<p>Tracking: ${escapeHtml(sending.trackingId)}</p>` : ''}
</div>
`).join('');
}
function renderAttachmentsList(attachments) {
if (!attachments || attachments.length === 0) {
elements.attachmentsList.innerHTML = '<p class="info-text">No attachments found.</p>';
return;
}
elements.attachmentsList.innerHTML = attachments.map(att => `
<div class="document-item">
<h4>${escapeHtml(att.name || att.attachmentId)}</h4>
<p>${att.pageCount} page(s) • Created: ${new Date(att.createdAt).toLocaleDateString()}</p>
</div>
`).join('');
}
async function selectDocument(documentId) {
selectedDocumentId = documentId;
try {
const doc = await client.getDocument(documentId);
updateResponse(doc);
elements.documentDetail.innerHTML = `
<p><strong>ID:</strong> ${escapeHtml(doc.documentId)}</p>
<p><strong>Status:</strong> <span class="status-badge status-${doc.status}">${getStatusLabel(doc.status)}</span></p>
<p><strong>Pages:</strong> ${doc.pageCount}</p>
<p><strong>Color:</strong> ${doc.color ? 'Yes' : 'No'}</p>
<p><strong>Duplex:</strong> ${doc.duplex ? 'Yes' : 'No'}</p>
<p><strong>Envelope:</strong> ${doc.envelope}</p>
${doc.price ? `<p><strong>Price:</strong> €${doc.price.toFixed(2)}</p>` : ''}
${doc.address ? `<p><strong>Address:</strong> ${escapeHtml(JSON.stringify(doc.address))}</p>` : ''}
${doc.validationMessages?.length ? `
<div style="margin-top: 12px;">
<strong>Validation Messages:</strong>
${doc.validationMessages.map(m => `
<div class="alert alert-${m.type === 'ERROR' ? 'error' : m.type === 'WARNING' ? 'warning' : 'success'}" style="margin-top: 8px;">
[${m.code}] ${escapeHtml(m.message)}
</div>
`).join('')}
</div>
` : ''}
`;
elements.documentDetailCard.classList.remove('hidden');
// Update button states based on status
const canSend = doc.status === 2; // SHIPPABLE
const canDelete = doc.status <= 2 || doc.status === 7; // Not yet sent
elements.sendDocumentBtn.disabled = !canSend;
elements.deleteDocumentBtn.disabled = !canDelete;
} catch (error) {
showAlert(elements.documentsAlert, error.message);
}
}
// -------------------------------------------------------------------------
// Event Handlers
// -------------------------------------------------------------------------
// Tab switching
elements.tabs.forEach(tab => {
tab.addEventListener('click', () => {
elements.tabs.forEach(t => t.classList.remove('active'));
elements.tabContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`tab-${tab.dataset.tab}`).classList.add('active');
});
});
// Connect
elements.credentialsForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = elements.usernameInput.value;
const password = elements.passwordInput.value;
try {
client = new BinectClient({ username, password });
// Test connection by fetching account info
const accountInfo = await client.getAccount();
updateResponse(accountInfo);
elements.accountCredit.textContent = `${accountInfo.credit?.toFixed(2) ?? '0.00'}`;
elements.accountDebitor.textContent = accountInfo.debitornumber ?? '-';
setConnected(true);
// Save credentials if requested
if (elements.rememberCheckbox.checked) {
localStorage.setItem('binect-credentials', JSON.stringify({ username, password }));
}
} catch (error) {
showAlert(elements.documentsAlert, `Connection failed: ${error.message}`);
client = null;
}
});
// Disconnect
elements.disconnectBtn.addEventListener('click', () => {
client = null;
selectedDocumentId = null;
setConnected(false);
elements.documentsList.innerHTML = '<p class="info-text">Connect and load documents to see them here.</p>';
elements.documentDetailCard.classList.add('hidden');
localStorage.removeItem('binect-credentials');
});
// Load documents
elements.loadDocumentsBtn.addEventListener('click', async () => {
clearAlert(elements.documentsAlert);
try {
const result = await client.listDocuments({ limit: 50 });
updateResponse(result);
renderDocumentsList(result.items ?? result);
} catch (error) {
showAlert(elements.documentsAlert, error.message);
}
});
// Load document errors
elements.loadErrorsBtn.addEventListener('click', async () => {
clearAlert(elements.documentsAlert);
try {
const result = await client.listDocumentErrors({ limit: 50 });
updateResponse(result);
renderDocumentsList(result.items ?? result);
} catch (error) {
showAlert(elements.documentsAlert, error.message);
}
});
// Refresh documents
elements.refreshDocumentsBtn.addEventListener('click', () => {
elements.loadDocumentsBtn.click();
});
// Upload document
elements.uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
clearAlert(elements.uploadAlert);
const file = elements.pdfFileInput.files[0];
if (!file) {
showAlert(elements.uploadAlert, 'Please select a PDF file');
return;
}
if (file.size > 12 * 1024 * 1024) {
showAlert(elements.uploadAlert, 'File too large. Maximum size is 12 MB.');
return;
}
try {
const content = await fileToBase64(file);
const options = {
content,
color: elements.uploadColorCheckbox.checked,
duplex: elements.uploadDuplexCheckbox.checked,
envelope: elements.uploadEnvelopeSelect.value,
franking: elements.uploadFrankingSelect.value,
};
const result = await client.uploadDocument(options);
updateResponse(result);
showAlert(elements.uploadAlert, `Document uploaded: ${result.documentId}`, 'success');
// Clear file input
elements.pdfFileInput.value = '';
// Switch to documents tab and refresh
document.querySelector('[data-tab="documents"]').click();
elements.loadDocumentsBtn.click();
} catch (error) {
showAlert(elements.uploadAlert, error.message);
}
});
// Load sendings
elements.loadSendingsBtn.addEventListener('click', async () => {
clearAlert(elements.sendingsAlert);
try {
const result = await client.listSendings({ limit: 50 });
updateResponse(result);
renderSendingsList(result.items ?? result);
} catch (error) {
showAlert(elements.sendingsAlert, error.message);
}
});
// Load attachments
elements.loadAttachmentsBtn.addEventListener('click', async () => {
clearAlert(elements.attachmentsAlert);
try {
const result = await client.listAttachments({ limit: 50 });
updateResponse(result);
renderAttachmentsList(result.items ?? result);
} catch (error) {
showAlert(elements.attachmentsAlert, error.message);
}
});
// Send document
elements.sendDocumentBtn.addEventListener('click', async () => {
if (!selectedDocumentId) return;
const confirmed = await showConfirmModal(
'Send Document',
`Are you sure you want to send document ${selectedDocumentId}? This will dispatch physical mail.`,
true
);
if (confirmed) {
try {
const result = await client.sendDocument(selectedDocumentId);
updateResponse(result);
showAlert(elements.documentsAlert, 'Document sent successfully!', 'success');
await selectDocument(selectedDocumentId);
} catch (error) {
showAlert(elements.documentsAlert, error.message);
}
}
});
// Preview document PDF
elements.previewDocumentBtn.addEventListener('click', async () => {
if (!selectedDocumentId) return;
try {
const response = await client.getDocumentPdf(selectedDocumentId);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
} catch (error) {
showAlert(elements.documentsAlert, error.message);
}
});
// Delete document
elements.deleteDocumentBtn.addEventListener('click', async () => {
if (!selectedDocumentId) return;
const confirmed = await showConfirmModal(
'Delete Document',
`Are you sure you want to delete document ${selectedDocumentId}? This cannot be undone.`,
false
);
if (confirmed) {
try {
await client.deleteDocument(selectedDocumentId);
showAlert(elements.documentsAlert, 'Document deleted.', 'success');
elements.documentDetailCard.classList.add('hidden');
selectedDocumentId = null;
elements.loadDocumentsBtn.click();
} catch (error) {
showAlert(elements.documentsAlert, error.message);
}
}
});
// -------------------------------------------------------------------------
// Profiles Management
// -------------------------------------------------------------------------
function loadProfiles() {
try {
const saved = localStorage.getItem('binect-profiles');
if (saved) {
profiles = JSON.parse(saved);
updateProfileSelect();
}
} catch {}
}
function saveProfiles() {
localStorage.setItem('binect-profiles', JSON.stringify(profiles));
updateProfileSelect();
}
function updateProfileSelect() {
elements.profileSelect.innerHTML = '<option value="">-- Select Profile --</option>' +
Object.keys(profiles).map(name =>
`<option value="${escapeHtml(name)}">${escapeHtml(name)}</option>`
).join('');
}
function getCurrentProfile() {
return {
color: elements.uploadColorCheckbox.checked,
duplex: elements.uploadDuplexCheckbox.checked,
envelope: elements.uploadEnvelopeSelect.value,
franking: elements.uploadFrankingSelect.value,
};
}
function applyProfile(profile) {
if (!profile) return;
elements.uploadColorCheckbox.checked = profile.color ?? false;
elements.uploadDuplexCheckbox.checked = profile.duplex ?? true;
elements.uploadEnvelopeSelect.value = profile.envelope ?? 'DINLANG';
elements.uploadFrankingSelect.value = profile.franking ?? 'STANDARD_FRANKING';
}
elements.profileSelect.addEventListener('change', () => {
const name = elements.profileSelect.value;
if (name && profiles[name]) {
applyProfile(profiles[name]);
}
});
elements.saveProfileBtn.addEventListener('click', () => {
const name = prompt('Enter profile name:');
if (name) {
profiles[name] = getCurrentProfile();
saveProfiles();
alert('Profile saved!');
}
});
elements.exportProfilesBtn.addEventListener('click', () => {
const data = JSON.stringify(profiles, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'binect-profiles.json';
a.click();
URL.revokeObjectURL(url);
});
elements.importProfilesBtn.addEventListener('click', () => {
elements.importFileInput.click();
});
elements.importFileInput.addEventListener('change', async () => {
const file = elements.importFileInput.files[0];
if (!file) return;
try {
const text = await file.text();
const imported = JSON.parse(text);
profiles = { ...profiles, ...imported };
saveProfiles();
alert('Profiles imported!');
} catch (error) {
alert('Failed to import profiles: ' + error.message);
}
elements.importFileInput.value = '';
});
// -------------------------------------------------------------------------
// Initialization
// -------------------------------------------------------------------------
function init() {
loadProfiles();
// Load saved credentials
try {
const saved = localStorage.getItem('binect-credentials');
if (saved) {
const { username, password } = JSON.parse(saved);
elements.usernameInput.value = username;
elements.passwordInput.value = password;
elements.rememberCheckbox.checked = true;
}
} catch {}
}
init();
</script>
</body>
</html>