generated from coulomb/repo-seed
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>
1336 lines
43 KiB
HTML
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>
|