PageSourceSearch

https://www.woopets.ai/js/demo-share.js?v=2734a62c

js woopets.ai collected 2026-10-05 13:19:46 UTC 24,182 bytes, 454 lines download raw bytes

1/**
2 * demo-share.js — Partage & documents du Carnet de Vie (Lot 6).
3 *
4 * Sheet « Partager » (3 types de liens read-only : passeport / fiche d'urgence / dossier
5 * véto), création + copie/Web Share du lien, liste des liens actifs + révocation.
6 * Scan intelligent d'une photo de document (Vision) → proposition d'événement pré-rempli.
7 * Toast « Annuler » réutilisable (undo 6 s de la suppression de pièce jointe, B-6).
8 *
9 * Requires: WD.state, WD.dom, WD.api, WD.ENDPOINTS, WD.esc, WD.escAttr, WD.t, WD.te,
10 *           WD.showToast, WD.confirmDialog, WD.formatDate (demo-core.js),
11 *           WD.openHealthForm (runtime, demo-health.js)
12 * Exposes:  WD.openShareSheet, WD.scanPhotoDocument, WD.undoToast, WD.closeShareSheet
13 */
14(function (W) {
15    'use strict';
16
17    var state = W.state;
18    var api = W.api;
19    var EP = W.ENDPOINTS;
20    var esc = W.esc;
21    var t = W.t;
22    var te = W.te;
23
24    function haptic(style) { if (W.native && W.native.haptic) W.native.haptic(style); }
25
26    // Types de liens : icône + TTL par défaut + choix de durée (véto) + choix de période
27    // des événements (passeport & véto ont une timeline ; l'urgence n'en a pas).
28    var SHARE_TYPES = [
29        { type: 'passport', icon: '📄', ttl: 30, hasPeriod: true },
30        { type: 'emergency', icon: '🚨', ttl: 30, hasPeriod: false },
31        { type: 'vet', icon: '🩺', ttl: 30, ttlChoices: [1, 7, 30], hasPeriod: true }
32    ];
33    var TYPE_ICONS = { passport: '📄', emergency: '🚨', vet: '🩺' };
34    // Fenêtres d'événements proposées ([valeur jours, clé i18n]) ; '' = tout l'historique.
35    var PERIOD_CHOICES = [['', 'share_period_all'], ['30', 'share_period_30'], ['180', 'share_period_180'], ['365', 'share_period_365']];
36
37    // ------------------------------------------------------------------
38    // Bottom sheet autonome (indépendant de demo-carnet)
39    // ------------------------------------------------------------------
40
41    var sheet = { overlay: null, open: false, petId: null, pet: null, prevFocus: null };
42
43    function buildSheet() {
44        if (sheet.overlay) return;
45        sheet.overlay = document.createElement('div');
46        sheet.overlay.className = 'share-sheet-overlay';
47        sheet.overlay.innerHTML = '<div class="share-sheet" role="dialog" aria-modal="true">'
48            + '<div class="share-sheet-body" id="shareSheetBody"></div>'
49            + '</div>';
50        document.body.appendChild(sheet.overlay);
51        sheet.overlay.addEventListener('click', function (e) {
52            if (e.target === sheet.overlay) W.closeShareSheet();
53        });
54        // Délégation liée UNE fois (le div persiste entre les ouvertures).
55        sheet.overlay.querySelector('#shareSheetBody').addEventListener('click', onSheetClick);
56    }
57
58    function closeBtnHtml() {
59        return '<button type="button" class="share-sheet-close" data-share-close aria-label="' + W.escAttr(t('dialog_cancel')) + '">'
60            + '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>';
61    }
62
63    function renderSheet(html) {
64        buildSheet();
65        var body = document.getElementById('shareSheetBody');
66        body.innerHTML = html;
67        if (!sheet.open) sheet.prevFocus = document.activeElement;
68        sheet.open = true;
69        sheet.overlay.classList.add('active');
70        var first = body.querySelector('button, input, select');
71        if (first) { try { first.focus(); } catch (e) {} }
72    }
73
74    W.closeShareSheet = function () {
75        if (!sheet.overlay) return;
76        sheet.open = false;
77        sheet.overlay.classList.remove('active');
78        var prev = sheet.prevFocus;
79        sheet.prevFocus = null;
80        if (prev && prev.isConnected) { try { prev.focus(); } catch (e) {} }
81    };
82
83    W.isShareSheetOpen = function () { return !!sheet.open; };
84
85    // ------------------------------------------------------------------
86    // Écran d'accueil du partage : 3 types + liens actifs
87    // ------------------------------------------------------------------
88
89    W.openShareSheet = function (petId, pet) {
90        if (!state.user) { if (W.openLoginModal) W.openLoginModal(); return; }
91        if (!petId) return;
92        sheet.petId = petId;
93        sheet.pet = pet || (state.pets || []).find(function (p) { return p.id === petId; }) || null;
94        renderHome();
95        loadLinks(petId);
96    };
97
98    function renderHome() {
99        var name = (sheet.pet && sheet.pet.name) ? sheet.pet.name : '';
100        var parts = ['<div class="share-sheet-head"><span class="share-sheet-title">' + esc(t('share_sheet_title').replace('{name}', name)) + '</span>' + closeBtnHtml() + '</div>'];
101        parts.push('<p class="share-sheet-sub">' + esc(t('share_sheet_sub')) + '</p>');
102
103        parts.push('<div class="share-opts">');
104        SHARE_TYPES.forEach(function (st) {
105            parts.push('<div class="share-opt" data-share-type="' + st.type + '">'
106                + '<span class="share-opt-ico" aria-hidden="true">' + st.icon + '</span>'
107                + '<span class="share-opt-body">'
108                + '<span class="share-opt-title">' + esc(t('share_type_' + st.type)) + '</span>'
109                + '<span class="share-opt-desc">' + esc(t('share_type_' + st.type + '_desc')) + '</span>'
110                + '</span>'
111                + '<span class="share-opt-arrow" aria-hidden="true">›</span>'
112                + '</div>');
113        });
114        parts.push('</div>');
115
116        // Lot 7 : carte-bilan partageable (image canvas). Affichée UNIQUEMENT dans le
117        // shell (l'app) : le partage de fichier via navigator.share n'est pas fiable sur
118        // desktop web (« clic sans effet » remonté). Dans l'app, le partage natif marche.
119        if (W.openBilan && window.WOOPETS_IS_SHELL) {
120            parts.push('<div class="share-opts share-opts--bilan">');
121            parts.push('<div class="share-opt" data-share-bilan>'
122                + '<span class="share-opt-ico" aria-hidden="true">🎉</span>'
123                + '<span class="share-opt-body">'
124                + '<span class="share-opt-title">' + esc(W.t('bilan_share')) + '</span>'
125                + '<span class="share-opt-desc">' + esc(W.t('bilan_title').replace('{name}', name)) + '</span>'
126                + '</span>'
127                + '<span class="share-opt-arrow" aria-hidden="true">›</span>'
128                + '</div>');
129            parts.push('</div>');
130        }
131
132        // Zone des liens actifs (remplie en async par renderLinks).
133        parts.push('<div class="share-links" id="shareLinksList" hidden></div>');
134
135        renderSheet(parts.join(''));
136    }
137
138    // Écran de création : choix de la période des événements (passeport/véto) + de la durée
139    // du lien (véto). L'urgence n'a ni l'un ni l'autre → création directe.
140    function openCreate(type) {
141        var st = null;
142        SHARE_TYPES.forEach(function (s) { if (s.type === type) st = s; });
143        if (!st) return;
144
145        if (!st.hasPeriod && !st.ttlChoices) {
146            createLink(type, st.ttl, null);
147            return;
148        }
149
150        var parts = ['<div class="share-sheet-head">'
151            + '<button type="button" class="share-sheet-back" data-share-back aria-label="' + W.escAttr(t('dialog_cancel')) + '">
151<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg></button>'
152            + '<span class="share-sheet-title">' + st.icon + ' ' + esc(t('share_type_' + type)) + '</span>'
153            + closeBtnHtml() + '</div>'];
154
155        if (st.hasPeriod) {
156            parts.push('<p class="share-sheet-label">' + esc(t('share_period_prompt')) + '</p>');
157            parts.push('<div class="share-chip-group" data-group="period">');
158            PERIOD_CHOICES.forEach(function (p) {
159                parts.push('<button type="button" class="share-chip' + (p[0] === '' ? ' active' : '') + '" data-period="' + p[0] + '">' + esc(t(p[1])) + '</button>');
160            });
161            parts.push('</div>');
162        }
163
164        if (st.ttlChoices) {
165            parts.push('<p class="share-sheet-label">' + esc(t('share_ttl_prompt')) + '</p>');
166            parts.push('<div class="share-chip-group" data-group="ttl">');
167            st.ttlChoices.forEach(function (days) {
168                parts.push('<button type="button" class="share-chip' + (days === 30 ? ' active' : '') + '" data-ttl="' + days + '">' + esc(t('share_ttl_' + days)) + '</button>');
169            });
170            parts.push('</div>');
171        }
172
173        parts.push('<p class="share-sheet-note">' + esc(t('share_readonly_note')) + '</p>');
174        parts.push('<button type="button" class="share-create-btn" data-share-do-create="' + type + '">' + esc(t('share_create_link')) + '</button>');
175        renderSheet(parts.join(''));
176    }
177
178    // Lit les chips actives (période + durée) et lance la création.
179    function submitCreate(type) {
180        var body = document.getElementById('shareSheetBody');
181        if (!body) return;
182        var periodEl = body.querySelector('[data-group="period"] .share-chip.active');
183        var ttlEl = body.querySelector('[data-group="ttl"] .share-chip.active');
184        var period = periodEl ? periodEl.getAttribute('data-period') : '';
185        var ttl = ttlEl ? parseInt(ttlEl.getAttribute('data-ttl'), 10) : 30;
186        createLink(type, ttl, period ? parseInt(period, 10) : null);
187    }
188
189    function createLink(type, ttlDays, periodDays) {
190        if (sheet.creating) return;
191        sheet.creating = true;
192        api(EP.shareLinks(sheet.petId), { method: 'POST', body: { type: type, ttl_days: ttlDays, period_days: periodDays } })
193            .then(function (data) {
194                if (!data || !data.success) throw new Error((data && data.error) || te('server_error'));
195                haptic('success');
196                renderCreated(data);
197                loadLinks(sheet.petId);
198            })
199            .catch(function (err) { W.showToast(err.message, 'error'); })
200            .finally(function () { sheet.creating = false; });
201    }
202
203    // Écran « lien créé » : URL copiable + partage natif (le token n'est montré qu'ici).
204    function renderCreated(data) {
205        var url = data.url || '';
206        var parts = ['<div class="share-sheet-head"><span class="share-sheet-title">✅ ' + esc(t('share_created_title')) + '</span>' + closeBtnHtml() + '</div>'];
207        parts.push('<p class="share-sheet-sub">' + esc(t('share_created_sub').replace('{date}', data.expires_at && W.formatDate ? W.formatDate(String(data.expires_at).slice(0, 10)) : '')) + '</p>');
208        parts.push('<div class="share-url-row">'
209            + '<input type="text" class="share-url-input" id="shareUrlInput" readonly value="' + W.escAttr(url) + '" aria-label="' + W.escAttr(t('share_url_label')) + '">'
210            + '<button type="button" class="share-url-copy" data-share-copy="' + W.escAttr(url) + '">' + esc(t('share_copy')) + '</button>'
211            + '</div>');
212        if (navigator.share) {
213            parts.push('<button type="button" class="share-native-btn" data-share-native="' + W.escAttr(url) + '">' + esc(t('share_via')) + '</button>');
214        }
215        parts.push('<button type="button" class="share-done-btn" data-share-close>' + esc(t('share_done')) + '</button>');
216        renderSheet(parts.join(''));
217        var input = document.getElementById('shareUrlInput');
218        if (input) { try { input.focus(); input.select(); } catch (e) {} }
219    }
220
221    // ------------------------------------------------------------------
222    // Liens actifs (management + révocation)
223    // ------------------------------------------------------------------
224
225    function loadLinks(petId) {
226        api(EP.shareLinks(petId), { method: 'GET' }).then(function (data) {
227            if (!data || !data.success || sheet.petId !== petId) return;
228            renderLinks(data.links || []);
229        }).catch(function () {});
230    }
231
232    function renderLinks(links) {
233        var box = document.getElementById('shareLinksList');
234        if (!box) return;
235        if (!links.length) { box.hidden = true; box.innerHTML = ''; return; }
236
237        var parts = ['<div class="share-links-title">' + esc(t('share_active_links')) + '</div>'];
238        links.forEach(function (link) {
239            var expiry = link.expires_at && W.formatDate ? W.formatDate(String(link.expires_at).slice(0, 10)) : '';
240            parts.push('<div class="share-link-row">'
241                + '<span class="share-link-ico" aria-hidden="true">' + (TYPE_ICONS[link.type] || '🔗') + '</span>'
242                + '<span class="share-link-body">'
243                + '<span class="share-link-type">' + esc(t('share_type_' + link.type)) + '</span>'
244                + '<span class="share-link-meta">' + esc(t('share_expires_short').replace('{date}', expiry))
245                + ' · ' + esc(viewsLabel(link.view_count | 0)) + '</span>'
246                + '</span>'
247                + '<button type="button" class="share-link-revoke" data-share-revoke="' + (link.id | 0) + '">' + esc(t('share_revoke')) + '</button>'
248                + '</div>');
249        });
250        box.innerHTML = parts.join('');
251        box.hidden = false;
252    }
253
254    // Libellé « N vue(s) » avec la bonne règle de pluriel par langue : le français met
255    // 0 ET 1 au singulier (« 0 vue »), les autres langues seulement 1.
256    function viewsLabel(count) {
257        var locale = (window.CURRENT_LOCALE || 'fr').toLowerCase();
258        var singular = locale === 'fr' ? count <= 1 : count === 1;
259        return t(singular ? 'share_views_one' : 'share_views').replace('{count}', String(count));
260    }
261
262    function revokeLink(id) {
263        W.confirmDialog(t('share_revoke_confirm'), { confirmLabel: t('share_revoke'), danger: true }).then(function (ok) {
264            if (!ok) return;
265            api(EP.shareRevoke(id), { method: 'POST', body: {} })
266                .then(function (data) {
267                    if (!data || !data.success) throw new Error((data && data.error) || te('server_error'));
268                    W.showToast(t('share_revoked'), 'success');
269                    loadLinks(sheet.petId);
270                })
271                .catch(function (err) { W.showToast(err.message, 'error'); });
272        });
273    }
274
275    function copyToClipboard(url) {
276        if (navigator.clipboard && navigator.clipboard.writeText) {
277            navigator.clipboard.writeText(url).then(function () { W.showToast(t('share_copied'), 'success'); })
278                .catch(function () { fallbackCopy(url); });
279        } else {
280            fallbackCopy(url);
281        }
282    }
283
284    function fallbackCopy(url) {
285        var input = document.getElementById('shareUrlInput');
286        if (input) { try { input.focus(); input.select(); document.execCommand('copy'); W.showToast(t('share_copied'), 'success'); return; } catch (e) {} }
287        W.showToast(url, 'info');
288    }
289
290    function onSheetClick(e) {
291        if (e.target.closest('[data-share-close]')) { W.closeShareSheet(); return; }
292        if (e.target.closest('[data-share-back]')) { renderHome(); loadLinks(sheet.petId); return; }
293        var opt = e.target.closest('[data-share-type]');
294        if (opt) { haptic('light'); openCreate(opt.getAttribute('data-share-type')); return; }
295        // Lot 7 : carte-bilan (image canvas) — ferme la sheet et lance le partage.
296        if (e.target.closest('[data-share-bilan]')) {
297            var pid = sheet.petId;
298            W.closeShareSheet();
299            if (W.openBilan && pid) W.openBilan(pid);
300            return;
301        }
302        // Toggle d'une chip (période ou durée) : une seule active dans son groupe.
303        var chip = e.target.closest('.share-chip');
304        if (chip) {
305            var group = chip.closest('.share-chip-group');
306            if (group) { group.querySelectorAll('.share-chip').forEach(function (c) { c.classList.remove('active'); }); }
307            chip.classList.add('active');
308            return;
309        }
310        var doCreate = e.target.closest('[data-share-do-create]');
311        if (doCreate) { submitCreate(doCreate.getAttribute('data-share-do-create')); return; }
312        var copy = e.target.closest('[data-share-copy]');
313        if (copy) { copyToClipboard(copy.getAttribute('data-share-copy')); return; }
314        var nat = e.target.closest('[data-share-native]');
315        if (nat) {
316            var url = nat.getAttribute('data-share-native');
317            if (navigator.share) { navigator.share({ title: t('share_native_title'), url: url }).catch(function () {}); }
318            return;
319        }
320        var rev = e.target.closest('[data-share-revoke]');
321        if (rev) { revokeLink(parseInt(rev.getAttribute('data-share-revoke'), 10)); return; }
322    }
323
324    // ------------------------------------------------------------------
325    // Scan intelligent de document (Vision) → proposition d'événement
326    // ------------------------------------------------------------------
327
328    W.scanPhotoDocument = function (photoId, petId) {
329        if (!photoId || sheet.scanning) return;
330        sheet.scanning = true;
331        sheet.petId = petId || sheet.petId;
332        // Sheet de progression pendant l'appel Vision (5-15 s).
333        renderSheet('<div class="share-sheet-head"><span class="share-sheet-title">✨ ' + esc(t('scan_title')) + '</span>' + closeBtnHtml() + '</div>'
334            + '<div class="share-scan-loading"><span class="share-scan-spinner" aria-hidden="true"></span>' + esc(t('scan_loading')) + '</div>');
335
336        api(EP.docScan(photoId), { method: 'POST', body: {} }, 60000)
337            .then(function (data) {
338                if (!data || !data.success || !data.scan) throw new Error((data && data.error) || te('server_error'));
339                renderScanResult(data.scan);
340            })
341            .catch(function (err) {
342                renderScanFailed(err.message);
343            })
344            .finally(function () { sheet.scanning = false; });
345    };
346
347    function renderScanResult(scan) {
348        var confidencePct = Math.round((scan.confidence || 0) * 100);
349        var products = (scan.product_names || []).slice(0, 5);
350        var title = scan.title || t('scan_doc_' + (scan.doc_type || 'other'));
351
352        var parts = ['<div class="share-sheet-head"><span class="share-sheet-title">✨ ' + esc(t('scan_result_title')) + '</span>' + closeBtnHtml() + '</div>'];
353        parts.push('<div class="share-scan-card">');
354        parts.push('<div class="share-scan-type"><span class="share-scan-badge">' + esc(t('scan_doc_' + (scan.doc_type || 'other'))) + '</span>'
355            + '<span class="share-scan-conf" title="' + W.escAttr(t('scan_confidence')) + '">' + confidencePct + '%</span></div>');
356        parts.push('<div class="share-scan-field"><span class="share-scan-k">' + esc(t('scan_field_title')) + '</span><span class="share-scan-v">' + esc(title) + '</span></div>');
357        if (scan.event_date) {
358            parts.push('<div class="share-scan-field"><span class="share-scan-k">' + esc(t('scan_field_date')) + '</span><span class="share-scan-v">' + esc(W.formatDate ? W.formatDate(scan.event_date) : scan.event_date) + '</span></div>');
359        }
360        if (products.length) {
361            parts.push('<div class="share-scan-field"><span class="share-scan-k">' + esc(t('scan_field_products')) + '</span><span class="share-scan-v">' + esc(products.join(', ')) + '</span></div>');
362        }
363        parts.push('</div>');
364        parts.push('<p class="share-scan-disclaimer">' + esc(t('scan_disclaimer')) + '</p>');
365        parts.push('<button type="button" class="share-scan-create" id="shareScanCreate">' + esc(t('scan_create_event')) + '</button>');
366        parts.push('<button type="button" class="share-scan-archive" data-share-close>' + esc(t('scan_just_archive')) + '</button>');
367        renderSheet(parts.join(''));
368
369        // Stocker le scan pour la création (évite de le ré-encoder en data-attr).
370        sheet.lastScan = scan;
371        sheet.lastScanTitle = title;
372        var btn = document.getElementById('shareScanCreate');
373        if (btn) {
374            btn.addEventListener('click', function () { createEventFromScan(); });
375        }
376    }
377
378    function renderScanFailed(message) {
379        renderSheet('<div class="share-sheet-head"><span class="share-sheet-title">' + esc(t('scan_title')) + '</span>' + closeBtnHtml() + '</div>'
380            + '<div class="share-scan-failed"><p>' + esc(t('scan_failed')) + '</p>'
381            + '<button type="button" class="share-scan-archive" data-share-close>' + esc(t('scan_just_archive')) + '</button></div>');
382    }
383
384    // Ouvre le formulaire carnet pré-rempli avec les métadonnées extraites (l'utilisateur
385    // valide/corrige puis enregistre — le scan reste une SUGGESTION, jamais une écriture auto).
386    function createEventFromScan() {
387        var scan = sheet.lastScan;
388        if (!scan || !W.openHealthForm) { W.closeShareSheet(); return; }
389        W.closeShareSheet();
390        W.openHealthForm(null);
391        // Remplissage après ouverture du formulaire (les champs existent alors).
392        setTimeout(function () {
393            var cat = document.getElementById('healthFormCategory');
394            if (cat && scan.suggested_category) {
395                cat.value = scan.suggested_category;
396                try { cat.dispatchEvent(new Event('change')); } catch (e) {}
397            }
398            var title = document.getElementById('healthFormTitle');
399            if (title && sheet.lastScanTitle) title.value = sheet.lastScanTitle;
400            var date = document.getElementById('healthFormDate');
401            if (date && scan.event_date) date.value = scan.event_date;
402            var desc = document.getElementById('healthFormDesc');
403            if (desc && (scan.product_names || []).length) {
404                desc.value = (scan.product_names || []).join(', ');
405            }
406        }, 60);
407    }
408
409    // ------------------------------------------------------------------
410    // Toast « Annuler » réutilisable (undo 6 s — suppression de pièce jointe B-6)
411    // ------------------------------------------------------------------
412
413    // onCommit() est appelé à l'expiration SANS annulation ; onUndo() au clic « Annuler ».
414    // Un seul toast undo à la fois (le précédent commit immédiatement).
415    var activeUndo = null;
416
417    W.undoToast = function (message, onCommit, onUndo, seconds) {
418        commitPendingUndo();
419        var secs = seconds || 6;
420        var el = document.createElement('div');
421        el.className = 'undo-toast';
422        el.innerHTML = '<span class="undo-toast-msg"></span>'
423            + '<button type="button" class="undo-toast-btn">' + esc(t('action_undo')) + '</button>';
424        el.querySelector('.undo-toast-msg').textContent = message;
425        (W.dom && W.dom.toastContainer ? W.dom.toastContainer : document.body).appendChild(el);
426        requestAnimationFrame(function () { el.classList.add('show'); });
427
428        var committed = false;
429        var timer = setTimeout(function () { finish(false); }, secs * 1000);
430
431        function finish(undone) {
432            if (committed) return;
433            committed = true;
434            clearTimeout(timer);
435            if (activeUndo === handle) activeUndo = null;
436            el.classList.remove('show');
437            setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
438            try { if (undone) { if (onUndo) onUndo(); } else if (onCommit) { onCommit(); } } catch (e) {}
439        }
440
441        el.querySelector('.undo-toast-btn').addEventListener('click', function () { haptic('light'); finish(true); });
442        var handle = { commit: function () { finish(false); } };
443        activeUndo = handle;
444        return handle;
445    };
446
447    function commitPendingUndo() {
448        if (activeUndo) { try { activeUndo.commit(); } catch (e) {} activeUndo = null; }
449    }
450
451    // Commit tout undo en attente si l'utilisateur quitte la page (la suppression part).
452    window.addEventListener('beforeunload', commitPendingUndo);
453
454})(window.WD);

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.