PageSourceSearch

https://biomapi.com/static/v2.2.6/js/historyManager.js

js biomapi.com collected 2026-09-24 14:39:20 UTC 8,664 bytes, 278 lines download raw bytes

1// History Manager - LocalStorage management for BiomPIN access history
2import './biompin-sdk.js';
3
4const BiomPinSDK = window.BiomPinSDK;
5
6// Initialize the stateful history store using BiomPinSDK
7const sdkHistory = BiomPinSDK.history.create({
8    storageKey: 'biompin_history',
9    maxEntries: 250
10});
11
12const INITIAL_VISIBLE_COUNT = 5; // Show first 5 items by default
13
14// Track if history is expanded
15let isHistoryExpanded = false;
16
17// Current database ID (persists across deploys, changes only on DB reset)
18let currentDbId = null;
19
20/**
21 * Set the current database ID
22 * @param {string} id - Database UUID from server
23 */
24export function setDbId(id) {
25    currentDbId = id;
26}
27
28/**
29 * Get the current database ID
30 * @returns {string|null}
31 */
32export function getDbId() {
33    return currentDbId;
34}
35
36/**
37 * Purge history entries from a previous database instance.
38 * Entries without a db_id field are treated as stale.
39 * @param {string} dbId - Current database ID
40 * @returns {number} Number of entries removed
41 */
42export function purgeStaleEntries(dbId) {
43    const before = sdkHistory.list().length;
44    const kept = sdkHistory.pruneDbIdMismatch(dbId);
45    return before - kept.length;
46}
47
48/**
49 * Add or update history from a response, after local identifiers are restored.
50 * @param {Object} response - Biometry API response
51 * @param {Object} [options] - Caller-supplied PIN and database context
52 * @param {string} [options.pin] - Retrieved PIN, defaults to the response PIN
53 * @param {string} [options.dbId] - Database ID, defaults to the current database
54 */
55export function addHistoryEntry(response, { pin = response.biompin?.pin, dbId } = {}) {
56    sdkHistory.add({
57        pin,
58        patientName: response.data?.patient?.name,
59        patientId: response.data?.patient?.id,
60        expiresAt: response.biompin?.expires_at,
61        dbId: dbId || currentDbId
62    });
63}
64
65/**
66 * Get all valid (non-expired) history entries
67 * @returns {Array} Array of valid history items
68 */
69export function getValidHistory() {
70    return sdkHistory.pruneExpired();
71}
72
73/**
74 * Delete a single history entry by PIN
75 * @param {string} pin - BiomPIN to remove
76 * @returns {number} Remaining entry count
77 */
78export function deleteHistoryEntry(pin) {
79    const remaining = sdkHistory.clearOne(pin);
80    return remaining.length;
81}
82
83/**
84 * Clear all history
85 */
86export function clearHistory() {
87    sdkHistory.clearAll();
88}
89
90/**
91 * Format relative time for expiry display with color coding
92 * @param {string} isoTimestamp - ISO timestamp
93 * @returns {Object} { text: "12h", colorClass: "time-warning" }
94 */
95function formatRelativeTime(isoTimestamp) {
96    if (!isoTimestamp) {
97        return { text: 'Unknown', colorClass: 'time-unknown' };
98    }
99
100    const expiry = new Date(isoTimestamp);
101    const now = new Date();
102    const diffMs = expiry - now;
103
104    if (diffMs < 0) {
105        return { text: 'Expired', colorClass: 'time-expired' };
106    }
107
108    const diffHours = Math.floor(diffMs / 3600000);
109
110    let colorClass;
111    if (diffHours >= 72) {
112        colorClass = 'time-safe';      // 3+ days: green
113    } else if (diffHours >= 24) {
114        colorClass = 'time-warning';   // 1-3 days: yellow
115    } else {
116        colorClass = 'time-urgent';    // <24 hours: orange
117    }
118
119    let text;
120    if (diffHours >= 24) {
121        const diffDays = Math.floor(diffHours / 24);
122        text = `${diffDays}d`;
123    } else {
124        text = `${diffHours}h`;
125    }
126
127    return { text, colorClass };
128}
129
130/**
131 * Render history list to DOM
132 * @param {Function} onItemClick - Callback when history item is clicked (receives pin)
133 * @param {string} [filterText] - Optional filter string (matches patient name, patient id, or pin)
134 */
135export function renderHistory(onItemClick, filterText = '') {
136    const container = document.getElementById('historyList');
137    if (!container) return;
138
139    const allHistory = getValidHistory();
140
141    // Apply filter
142    const history = filterText
143        ? allHistory.filter(item => {
144            const q = filterText.toLowerCase();
145            return (item.patient_name || '').toLowerCase().includes(q) ||
146                   (item.patient_id || '').toLowerCase().includes(q) ||
147                   (item.pin || '').toLowerCase().includes(q);
148        })
149        : allHistory;
150
151    const clearBtn = document.getElementById('clearHistoryBtn');
152    const searchBar = document.getElementById('historySearchBar');
153    const hasAny = allHistory.length > 0;
154
155    // Show/hide search bar based on whether history has any entries
156    if (searchBar) searchBar.style.display = hasAny ? '' : 'none';
157
158    // Empty state
159    if (history.length === 0) {
160        container.innerHTML = `
161            <div class="history-empty">
162                <i class="fas fa-clock-rotate-left"></i>
163                <p class="mb-0">
163${hasAny ? 'No matching entries' : 'No BiomPIN history'}</p>
164            </div>
165        `;
166        if (clearBtn) clearBtn.style.display = hasAny ? 'block' : 'none';
167        return;
168    }
169
170    // Show clear button when there is history
171    if (clearBtn) clearBtn.style.display = 'block';
172
173    // Determine how many items to show
174    const shouldShowLoadMore = !isHistoryExpanded && history.length > INITIAL_VISIBLE_COUNT;
175    const itemsToShow = shouldShowLoadMore ? history.slice(0, INITIAL_VISIBLE_COUNT) : history;
176
177    // Build history items HTML
178    let html = '';
179    itemsToShow.forEach(item => {
180        const timeInfo = formatRelativeTime(item.expires_at);
181
182        // Build patient info line - only show available fields
183        const patientParts = [];
184        if (item.patient_name) patientParts.push(item.patient_name);
185        if (item.patient_id) patientParts.push(item.patient_id);
186        const patientInfo = patientParts.length > 0 ? patientParts.join(' • ') : '';
187
188        html += `
189            <div class="history-item" data-pin="${item.pin}">
190                <button class="history-item-delete" data-pin="${item.pin}" title="Delete entry" aria-label="Delete ${item.pin}">
191                    <i class="fas fa-trash-can"></i>
192                </button>
193                <div class="history-item-pin">${item.pin}</div>
194                <div class="history-item-info">
195                    <div class="history-item-patient-info">
196                        ${patientInfo}
197                    </div>
198                    <div class="history-item-time ${timeInfo.colorClass}">
199                        <i class="fas fa-clock"></i>
200                        <span>${timeInfo.text}</span>
201                    </div>
202                </div>
203            </div>
204        `;
205    });
206
207    // Add "Load more" card if needed
208    if (shouldShowLoadMore) {
209        const remainingCount = history.length - INITIAL_VISIBLE_COUNT;
210        html += `
211            <div class="history-load-more" id="historyLoadMore">
212                <i class="fas fa-chevron-down me-2"></i>
213                <span>Load more from history (${remainingCount} older)</span>
214            </div>
215        `;
216    }
217
218    container.innerHTML = html;
219
220    // Attach delete button handlers
221    container.querySelectorAll('.history-item-delete').forEach(btn => {
222        btn.addEventListener('click', (event) => {
223            event.stopPropagation();
224            const pin = btn.getAttribute('data-pin');
225            if (pin) {
226                deleteHistoryEntry(pin);
227                renderHistory(onItemClick, filterText);
228            }
229        });
230    });
231
232    // Attach click handlers to history items
233    container.querySelectorAll('.history-item').forEach(item => {
234        item.addEventListener('click', () => {
235            const pin = item.getAttribute('data-pin');
236            if (pin && onItemClick) {
237                onItemClick(pin);
238            }
239        });
240    });
241
242    // Attach click handler to "Load more" card
243    const loadMoreCard = document.getElementById('historyLoadMore');
244    if (loadMoreCard) {
245        loadMoreCard.addEventListener('click', () => {
246            isHistoryExpanded = true;
247            renderHistory(onItemClick, filterText);
248        });
249    }
250}
251
252/**
253 * Initialize history UI (attach clear button handler)
254 * @param {Function} onHistoryCleared - Callback when history is cleared
255 */
256export function initHistoryUI(onHistoryCleared) {
257    const clearBtn = document.getElementById('clearHistoryBtn');
258    if (!clearBtn) return;
259
260    clearBtn.addEventListener('click', () => {
261        clearHistory();
262        if (onHistoryCleared) {
263            onHistoryCleared();
264        }
265    });
266}
267
268export default {
269    addHistoryEntry,
270    getValidHistory,
271    clearHistory,
272    deleteHistoryEntry,
273    renderHistory,
274    initHistoryUI,
275    setDbId,
276    getDbId,
277    purgeStaleEntries
278};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.