PageSourceSearch

https://prisma-optlyx.web.app/js/global-timer.js?v=2549f451

js prisma-optlyx.web.app collected 2026-10-03 12:33:43 UTC 5,095 bytes, 119 lines download raw bytes

1// Timer timesheet globale — avviabile dall'header "+ Nuovo" senza che lo
2// scadenziario-app sia montato. Replica la logica di
3// scadenziario-app/src/lib/timesheetActions.ts::startTimerEntry usando SOLO le
4// globali (window.db / window.auth / window.firebase / window.DbHelper), così la
5// scrittura su studios/{sid}/time_entries funziona da qualsiasi pagina.
6// ZERO Cloud Function.
7(function () {
8  'use strict';
9
10  function db() { return window.db || null; }
11  function uid() { return (window.auth && window.auth.currentUser && window.auth.currentUser.uid) || null; }
12  function studioId() {
13    try {
14      if (window.DbHelper && typeof window.DbHelper.getStudioId === 'function') {
15        const s = window.DbHelper.getStudioId();
16        if (s) return s;
17      }
18    } catch (_) {}
19    return uid();
20  }
21  function toDateStr(d) {
22    return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
23  }
24  async function resolveRate(sid, u) {
25    try {
26      const m = await db().collection('studios').doc(sid).collection('team').doc(u).get();
27      return Number(m.data() && m.data().hourlyRate) || 0;
28    } catch (_) { return 0; }
29  }
30
31  // Avvia un timer (voce status:'running'). Idempotente: se ce n'è già uno in
32  // corso per l'utente, ritorna { already:true } senza crearne un altro.
33  async function start(opts) {
34    opts = opts || {};
35    const sid = studioId(), u = uid();
36    if (!db() || !sid || !u) return { error: 'no-session' };
37    const col = db().collection('studios').doc(sid).collection('time_entries');
38    const existing = await col.where('userId', '==', u).where('status', '==', 'running').limit(1).get();
39    if (!existing.empty) return { already: true, id: existing.docs[0].id };
40
41    const now = new Date();
42    const rate = await resolveRate(sid, u);
43    let userName = (window.auth && window.auth.currentUser && window.auth.currentUser.displayName) || '';
44    try {
45      const m = await db().collection('studios').doc(sid).collection('team').doc(u).get();
46      const d = m.data();
47      if (d && (d.name || d.displayName)) userName = d.name || d.displayName;
48    } catch (_) {}
49
50    const serverTs = (window.firebase && window.firebase.firestore && window.firebase.firestore.FieldValue
51      && window.firebase.firestore.FieldValue.serverTimestamp)
52      ? window.firebase.firestore.FieldValue.serverTimestamp()
53      : new Date();
54
55    const ref = await col.add({
56      userId: u,
57      userName: userName,
58      clientId: opts.clientId || null,
59      clientName: opts.clientName || null,
60      deadlineId: opts.deadlineId || null,
61      deadlineTitle: opts.deadlineTitle || null,
62      dateStr: toDateStr(now),
63      note: opts.note || '',
64      billable: opts.billable !== false,
65      rate: rate,
66      minutes: 0,
67      amount: 0,
68      status: 'running',
69      startedAt: now.getTime(),
70      createdAt: serverTs,
71    });
72    return { id: ref.id };
73  }
74
75  // Handler per il menu "+ Nuovo": avvia il timer e porta alla vista Tempi dello
76  // scadenziario (il TimerPill lo mostrerà live). Deep-link via flag + evento.
77  async function startFromMenu() {
78    let res;
79    try {
80      res = await start();
81    } catch (e) {
82      console.error('[GlobalTimer] start failed', e);
83      if (window.showToast) window.showToast('Impossibile avviare il timer', 'error');
84      return;
85    }
86    if (res && res.error === 'no-session') {
87      if (window.showToast) window.showToast('Sessione non pronta, riprova', 'error');
88      return;
89    }
90    if (window.showToast) {
91      window.showToast(res && res.already ? 'Timer già in corso' : 'Timer avviato', 'success');
92    }
93    // Misura d'uso: solo un timer nuovo davvero scritto (non «già in corso»).
94    if (res && res.id && !res.already) {
95      try { if (window.NavUsage) window.NavUsage.azione('scadenze', 'timer_avviato'); } catch (_) {}
96      // Un timer partito è un tempo_registrato cominciato: finisce quando si ferma.
97      try { if (window.NavUsage && window.NavUsage.inizio) window.NavUsage.inizio('scadenze', 'tempo_registrato'); } catch (_) {}
98    }
99    // Deep-link alla vista Tempi RIUSANDO il bridge esistente dello scadenziario
100    // (`__scadenziarioPendingView` consumata al cold mount + evento
101    // `scadenziario:navigate` per l'app già montata). Niente meccanism
101i paralleli.
102    // IMPORTANTE: router.load è async — un suo reject NON deve sfuggire come
103    // unhandledrejection (mostrerebbe "Si è verificato un errore di sistema").
104    try { window.__scadenziarioPendingView = 'timesheet'; } catch (_) {}
105    try {
106      if (window.router && typeof window.router.load === 'function') {
107        var p = window.router.load('flows');
108        if (p && typeof p.then === 'function') p.then(function () {}, function () {});
109      }
110    } catch (_) {}
111    setTimeout(function () {
112      try {
113        window.dispatchEvent(new CustomEvent('scadenziario:navigate', { detail: { view: 'timesheet' } }));
114      } catch (_) {}
115    }, 500);
116  }
117
118  window.PrismaGlobalTimer = { start: start, startFromMenu: startFromMenu };
119})();

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.