/** ====== CRM SALES – Backend (Apps Script) ====== **/ const APP_TITLE = 'CRM SALES'; const SESSION_TTL_SEC = 10 * 3600; // 10h const SESSION_PREFIX = 'sess:'; /** ===== SUPABASE CONFIG ===== **/ const SUPA_URL = PropertiesService.getScriptProperties().getProperty('SUPA_URL') || ''; const SUPA_KEY = PropertiesService.getScriptProperties().getProperty('SUPA_KEY') || ''; // service_role sau anon dacă RLS off const SUPA_TABLE = 'bd_clienti_activi'; const SUPA_TABLE_PIERDUTI = 'bd_clienti_pierduti'; // cheie pentru contorul global de ID-uri în ScriptProperties const ID_COUNTER_KEY = 'next:id_unic'; const SUPA_STORAGE_URL = SUPA_URL + '/storage/v1'; const SUPA_BUCKET = 'documente_clienti'; // ===== SUPABASE – PROCESARI ===== const SUPA_PROC_HDR = 'bd_procesari_hdr'; const SUPA_PROC_BANCA = 'bd_procesari_banca'; const SUPA_LOGS_TABLE = 'bd_logs_actiuni'; const SUPA_PROGRAMARI = 'bd_programari'; // ===== CPS – tabele Supabase ===== const SUPA_CPS_AFC = 'bd_cps_afc'; const SUPA_CPS_RCCF = 'bd_cps_rccf'; const SUPA_CPS_TSMC = 'bd_cps_tsmc'; /** ====== TO DO – BACKEND SUPABASE ====== **/ const SUPA_TODO = 'bd_to_do'; // ===== CPS – bucket Supabase Storage (privat) ===== const SUPA_BUCKET_CPS = 'contracte_cps'; // ===== Training – metadata & bucket ===== const SUPA_TRAINING_TABLE = 'bd_materiale_training'; const SUPA_BUCKET_TRAINING = 'training'; const SUPA_ELIGIBILI = 'bd_eligibili'; const SUPA_TABLE_NEELIGIBILI = 'bd_neeligibili'; const SUPA_SOLICITARI_BC = 'bd_solicitari_bc'; /** ===== MANYCHAT – CONFIG & HELPERS (CRM SALES) ===== **/ const MANYCHAT_API_PROP_AGENT = 'MANYCHAT_API_TOKEN'; // cheie în ScriptProperties const MANYCHAT_BASE_URL_AGENT = 'https://api.manychat.com'; const MANYCHAT_TAG_AGENT_ALOC = 'AlocareAgent'; // Custom User Field pentru numele agentului – vezi în ManyChat: {{cuf_14023121}} const MANYCHAT_AGENT_FIELD_ID = 14023121; const SUPA_MESAJ_CLIENTI = 'bd_mesaje_clienti'; // NOU: CUF-uri pentru mesajul de ALOCARE AGENT const MANYCHAT_CUF_ZIUA_MAXIMA_ID = 14042345; // {{cuf_14042345}} const MANYCHAT_CUF_ZIUA_SAPTAMANA_ID = 14042340; // {{cuf_14042340}} const MANYCHAT_CUF_DATA_CONTACT_ID = 14044861; // {{cuf_14044861}} const BC_RR_KEY='bc_rr_last_asistent'; const SUPA_LISTA_CREDITORI_SS='bd_lista_creditori_ss'; const SUPA_BC_SCOR='bd_interogari_bc_scorerise'; const SUPA_ADRESE_EMAIL='bd_adrese_email'; // ===== BC STANDARD – WORKSPACE POOL (fisier extern) ===== const BC_EXT_SS_ID='1AxdhPbAD6qkYEVQqvCgGnV-ijxph9PfZM4BBIjdqPgE'; const BC_WS=['Extrageri BC 1','Extrageri BC 2','Extrageri BC 3','Extrageri BC 4','Extrageri BC 5']; const BC_WS_KEY='bcws:',BC_WS_TTL=8*60*1000; // TTL slot (ms) const BC_SRC_R=2999,BC_SRC_C=51,BC_PASTE_R=2; // A1:AY2999 -> paste de la A2 const BC_HDR_R=3002,BC_RES_R=3003; // headere + rezultat const BC_AUX_RANGE='BA4004:BE7000',BC_AUX_PASTE_R=3347,BC_AUX_PASTE_C=10; // J3347 const BC_TBL='bd_interogari_bc'; const SUPA_FUNCTII_SUPABASE='bd_functii_supabase'; const SUPA_FLUXURI_INTERNE='bd_fluxuri_interne'; const SUPA_TABELE_SUPABASE='bd_tabele_supabase'; function supaHeaders_(wantCount){ const h = { 'apikey': SUPA_KEY, 'Authorization': 'Bearer ' + SUPA_KEY, 'Content-Type': 'application/json' }; if (wantCount) h['Prefer'] = 'count=exact'; return h; } function supaGet_(table, qs, rangeStart, rangeEnd, wantCount){ const url=SUPA_URL+'/rest/v1/'+table+(qs?('?'+qs):''); const opt={method:'get',headers:supaHeaders_(!!wantCount),muteHttpExceptions:true}; if(rangeStart!=null){opt.headers['Range-Unit']='items';opt.headers['Range']=rangeStart+'-'+rangeEnd;} const res=UrlFetchApp.fetch(url,opt); if(res.getResponseCode()>=300) throw new Error('Supabase GET: '+res.getContentText()); return JSON.parse(res.getContentText()||'[]'); } function supaSelectAll_(table, qs, pageSize){ const out=[], step=Math.min(1000, Math.max(1, Number(pageSize||1000))); let from=0; for(;;){ const batch=supaGet_(table, qs, from, from+step-1, false) || []; out.push.apply(out, batch); if(batch.length= 300) throw new Error('Supabase UPSERT: ' + res.getContentText()); const txt = res.getContentText(); return txt ? JSON.parse(txt) : []; } function supaOneById_(id, select, table){ const t=table||SUPA_TABLE; const qs='id_unic=eq.'+encodeURIComponent(String(id))+'&select='+encodeURIComponent(select||'*')+'&limit=1'; const a=supaGet_(t, qs); return (a&&a[0])||null; } /** Patch parțial în bd_clienti_activi, după id_unic (Supabase only). */ function clientiSupabase_patch_(idUnic, patch){ idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); patch = patch || {}; patch.id_unic = idUnic; supaUpsert_(SUPA_TABLE, [patch], 'id_unic'); return { ok:true }; } /** ========== HELPERS PENTRU MAPARE SUPABASE ========== **/ function _toIsoDateFromRo_(v) { if (!v) return ''; const s = String(v).trim(); if (!s) return ''; let m = s.match(/^(\d{1,2})[./](\d{1,2})[./](\d{4})$/); if (m) { const d = ('0' + m[1]).slice(-2), mo = ('0' + m[2]).slice(-2), y = m[3]; return `${y}-${mo}-${d}`; } if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s; const d = new Date(s); return isNaN(d) ? '' : Utilities.formatDate(d, Session.getScriptTimeZone(), 'yyyy-MM-dd'); } function _mapEditorToDb_(idUnic, payload) { const out = { id_unic: String(idUnic) }; if (payload && payload.dg) { const p = payload.dg; if (p.agent) out.agent = String(p.agent).trim(); if (p.dataClient) out.data_client = _toIsoDateFromRo_(p.dataClient) || null; if (p.canal) out.canal = String(p.canal).trim(); if (p.campanie) out.campanie_recomandator = String(p.campanie).trim(); if (p.adsetTelefon) out.adset_tel_recomandator = String(p.adsetTelefon).trim(); if (p.ad) out.ad = String(p.ad).trim(); if (p.info) out.info_suplimentare = String(p.info).trim(); } if (payload && payload.ip) { const p = payload.ip; const set = (src, dst) => { if (p[src] !== undefined) out[dst] = p[src] == null ? null : String(p[src]).trim(); }; set('prenume', 'prenume'); set('nume', 'nume_familie'); set('numeComplet', 'nume_complet'); set('cnp', 'cnp'); set('telefon', 'telefon'); set('telefon2', 'telefon_2'); set('email', 'e_mail'); set('emailCreat', 'e_mail_creat'); set('judetCIMunca', 'judet_ci_munca'); set('judetCI', 'judet'); set('localitate', 'localitate'); set('strada', 'strada'); set('nr', 'nr_strada'); set('bloc', 'bloc'); set('sc', 'scara'); set('etaj', 'etaj'); set('ap', 'apart'); set('regiunea', 'regiunea'); } if (payload && payload.if) { const p = payload.if; const set = (src, dst) => { if (p[src] !== undefined) out[dst] = p[src] == null ? null : String(p[src]).trim(); }; set('tipVenit1', 'tip_venit_1'); if (p.valoareVenit1) out.valoare_venit_1 = String(p.valoareVenit1).trim(); set('angajator1', 'angajator_1'); if (p.dataAngajarii1) out.data_angajarii_1 = _toIsoDateFromRo_(p.dataAngajarii1) || null; set('vechime1', 'vechime_1'); set('intrerupereUltimulAn1', 'intrerupere_1'); set('verificareVenit1', 'verificare_venit_1'); set('popriri1', 'popriri_pe_venit_1'); set('tipAngajator1', 'tip_angajator_1'); if (p.tipVenit2) out.tip_venit_2 = String(p.tipVenit2).trim(); if (p.valoareVenit2) out.valoare_venit_2 = String(p.valoareVenit2).trim(); set('angajator2', 'angajator_2'); if (p.dataAngajarii2) out.data_angajarii_2 = _toIsoDateFromRo_(p.dataAngajarii2) || null; set('vechime2', 'vechime_2'); set('intrerupereUltimulAn2', 'intrerupere_2'); set('verificareVenit2', 'verificare_venit_2'); set('popriri2', 'popriri_pe_venit_2'); set('tipAngajator2', 'tip_angajator_2'); } if (payload && payload.gest) { const p = payload.gest; if (p.status) out.status = String(p.status).trim(); if (p.dua) out.dua = _toIsoDateFromRo_(p.dua) || null; if (p.statusSecundar) out.status_secundar = String(p.statusSecundar).trim(); if (p.dva) out.dva = _toIsoDateFromRo_(p.dva) || null; if (p.dvaMaxim) out.dva_maxim = _toIsoDateFromRo_(p.dvaMaxim) || null; if (p.raportBC) out.raport_bc = String(p.raportBC).trim(); } return out; } function _isoDateToInput_(d) { if (!d) return ''; try { return Utilities.formatDate(new Date(d), Session.getScriptTimeZone(), 'yyyy-MM-dd'); } catch (e) { return ''; } } function _dispDate_(isoOrStr, txt) { try { if (isoOrStr) { const d = isoOrStr instanceof Date ? isoOrStr : new Date(String(isoOrStr)); if (!isNaN(d)) return Utilities.formatDate(d, Session.getScriptTimeZone(), 'dd.MM.yyyy'); } } catch (_) {} const s = String(txt || isoOrStr || '').trim(); if (!s) return ''; let m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); if (m) return ('0' + m[1]).slice(-2) + '.' + ('0' + m[2]).slice(-2) + '.' + m[3]; m = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (m) return ('0' + m[1]).slice(-2) + '.' + ('0' + m[2]).slice(-2) + '.' + m[3]; m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (m) return m[3] + '.' + m[2] + '.' + m[1]; try { const d2 = new Date(s); if (!isNaN(d2)) return Utilities.formatDate(d2, Session.getScriptTimeZone(), 'dd.MM.yyyy'); } catch (_) {} return s.replace(/\//g, '.'); } /** ====== MAPE CELULE (foile individuale ale agenților) ====== **/ const MAP = { bcGen: { rezultat: 'A12', situatie: 'B12', istoric: 'C12', fico: 'D12', codebitor: 'E12', sumeRestante: 'F12', stergStd: 'G12', stergSpec: 'H12', dataRaport: 'I12' }, bcAlte: { conturiBcTotal: 'J12', conturiBcActive: 'K12', conturiBcInchise: 'L12', conturiIfnTotal: 'M12', conturiIfnActive: 'N12', conturiIfnInchise: 'O12', conturiBanciTotal: 'P12', conturiBanciActive: 'Q12', conturiBanciInchise: 'R12', interogariTotal: 'S12', interogariBanci: 'T12', interogariIfn: 'U12' }, bcMatrice: { intCurente: 'V11', int3: 'W11', int6: 'X11', int12: 'Y11', int24: 'Z11', intIstoric: 'AA11', mentiuni: 'AB11' } }; /** ====== RENDER & AUTH ====== **/ function renderClientEditorSkeleton_(e) { return HtmlService.createHtmlOutputFromFile('ClientEditorSkeleton') .setTitle('CRM SALES – Editor Skeleton') .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); } function doGet(e) { const page = (e && e.parameter && e.parameter.page) || ''; if (page === 'editorTest') return renderClientEditorSkeleton_(e); if (page === 'client') return renderClientEditor_(e); const tpl = HtmlService.createTemplateFromFile('Index'); return tpl.evaluate().setTitle(APP_TITLE) .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); } function include(name) { return HtmlService.createHtmlOutputFromFile(name).getContent(); } function getAppInfo() { return { name: APP_TITLE, version: '0.5', timeZone: Session.getScriptTimeZone() }; } function login(username, password){ username=String(username||'').trim(); password=String(password||'').trim(); if(!username||!password) return{ok:false,error:'Completați user și parolă.'}; const userKey=username.toUpperCase(); let rows; try{ const qs='USER=eq.'+encodeURIComponent(userKey)+ '&select='+encodeURIComponent('USER,PAROLA,"TIP USER","TIP ANGAJAT",session_rev')+ '&limit=1'; rows=supaGet_(SUPA_USERS_TABLE,qs); }catch(e){ return{ok:false,error:'Eroare la conexiunea cu baza de date (login). Încearcă din nou.'}; } if(!rows||!rows.length) return{ok:false,error:'User sau parolă greșită.'}; const r=rows[0]; if(String(r.PAROLA||'').trim()!==password) return{ok:false,error:'User sau parolă greșită.'}; const role=String(r['TIP USER']||'Utilizator').trim(); const tipAngajat=String(r['TIP ANGAJAT']||'').trim(); const token=Utilities.getUuid(); const sess={ user:userKey, role, tipAngajat, ts:Date.now(), session_rev:Number(r.session_rev||0)||0 }; _sessionSave_(token,sess,SESSION_TTL_SEC); return{ok:true,token,user:userKey,role,tipAngajat}; } function _sessionSave_(token, payload, ttlSec) { const data = Object.assign({}, payload, { exp: Date.now() + ttlSec * 1000 }); const k = SESSION_PREFIX + token; CacheService.getScriptCache().put(k, JSON.stringify(data), Math.min(ttlSec, 21600)); PropertiesService.getScriptProperties().setProperty(k, JSON.stringify(data)); } function _sessionDelete_(token) { const k = SESSION_PREFIX + token; try { CacheService.getScriptCache().remove(k); } catch (_) { } try { PropertiesService.getScriptProperties().deleteProperty(k); } catch (_) { } } function _sessionLoad_(token) { const k = SESSION_PREFIX + token; let raw = CacheService.getScriptCache().get(k); if (!raw) raw = PropertiesService.getScriptProperties().getProperty(k); if (!raw) throw new Error('Sesiune expirată sau invalidă.'); const obj = JSON.parse(raw); if (!obj.exp || Date.now() > obj.exp) { _sessionDelete_(token); throw new Error('Sesiune expirată.'); } return obj; } function logout(token) { if (token) _sessionDelete_(token); return { ok: true }; } function _getSession(token){ if(!token) throw new Error('Lipsă token.'); const sess=_sessionLoad_(token); const u=String((sess&&sess.user)||'').trim(); if(!u) throw new Error('Sesiune invalidă.'); const cur=_userRev_get_(u); const mine=Number(sess.session_rev||0); if(mine!==cur) throw new Error('Sesiune revocată. Te rugăm relogare.'); return sess; } /** ====== LISTARE CLIENTI ACTUALI ====== **/ function listClientiActivi(token) { const sess = _getSession(token); const isUtil = String(sess.role || '').toLowerCase() === 'utilizator'; const me = _s(sess.user).trim(); const select = 'id_unic,agent,data_client,nume_complet,data_nasterii,e_mail_creat,judet_ci_munca,valoare_venit,status,dua,status_secundar,dva,dva_maxim,delay,raport_bc,status_bc,potential_refin,tipologie_client,observatii_aratate,cnp,telefon,e_mail'; const filters = []; if (isUtil) { filters.push('agent=eq.' + encodeURIComponent(me)); filters.push('status=neq.RESPINS'); } const qs = 'select=' + encodeURIComponent(select) + (filters.length ? '&' + filters.join('&') : ''); const data = supaSelectAll_(SUPA_TABLE, qs, 1000); function toMsAsc(v) { if (v instanceof Date) return v.getTime(); const s = String(v || '').trim(); const m = /^(\d{1,2})[.\-\/](\d{1,2})[.\-\/](\d{4})$/.exec(s); if (m) return new Date(+m[3], +m[2] - 1, +m[1]).getTime(); const d = new Date(s); return isNaN(d) ? Number.POSITIVE_INFINITY : d.getTime(); } data.sort((a, b) => toMsAsc(a.dva) - toMsAsc(b.dva)); const rows = data.map(r => { const dcTxt = _dispDate_(r.data_client, r.data_client); const duaTxt = _dispDate_(r.dua, r.dua); const dvaTxt = _dispDate_(r.dva, r.dva); const dvmTxt = _dispDate_(r.dva_maxim, r.dva_maxim); const age = _ageFromBirthDate(r.data_nasterii); return [ _s(r.nume_complet), _s(age), _s(r.e_mail_creat), _s(r.judet_ci_munca), _s(r.valoare_venit), _s(r.status), duaTxt, _s(r.status_secundar), dvaTxt, dvmTxt, _s(r.delay), _s(r.raport_bc), _s(r.status_bc), _s(r.potential_refin), _s(r.tipologie_client), _s(r.observatii_aratate), _s(r.cnp), _s(r.telefon), _s(r.id_unic), _s(r.agent), _s(r.e_mail), dcTxt ]; }); return { ok: true, rows, user: sess.user, role: sess.role }; } function getEditorUrl(token, idUnic) { const sess = _getSession(token); const base = ScriptApp.getService().getUrl(); if (!base) throw new Error('Nu pot obține URL-ul Web App.'); idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); return `${base}?page=client&id=${encodeURIComponent(idUnic)}&row=2&tok=${encodeURIComponent(token)}`; } /** ====== RENDER editor client (rapid: folosește row din URL; NU încarcă BC la load) ====== **/ function renderClientEditor_(e){ const tok = (e && e.parameter && e.parameter.tok || '').trim(); const id = (e && e.parameter && e.parameter.id || '').trim(); const isNew = String((e && e.parameter && e.parameter.new) || '') === '1'; const warnTel = String((e && e.parameter && e.parameter.warnTel) || '') === '1'; if (!tok || !id){ return HtmlService.createHtmlOutput('
Parametri lipsă/invalidi (id/tok).
'); } const sess = _safeGetSession_(tok); // vezi helperul de mai jos if (!sess) return HtmlService.createHtmlOutput('
Sesiune invalidă sau expirată.
'); // citim din Supabase tot ce ne trebuie pt. editor const select = 'id_unic,agent,nume_complet,prenume,nume_familie,cnp,data_nasterii,' + 'telefon,telefon_2,e_mail,e_mail_creat,' + 'data_client,judet_ci_munca, regiunea, judet, localitate, strada, nr_strada, bloc, scara, etaj, apart,' + 'tip_venit_1, valoare_vernit_1:valoare_venit_1, angajator_1, data_angajarii_1, vechime_1, intrerupere_1, verificare_venit_1, popriri_pe_venit_1, tip_angajator_1,' + 'tip_venit_2, valoare_venit_2, angajator_2, data_angajarii_2, vechime_2, intrerupere_2, verificare_venit_2, popriri_pe_venit_2, tip_angajator_2,' + 'status, dua, status_secundar, dva, dva_maxim, raport_bc, istoric, observatii, observatii_aratate,' + 'canal, campanie_recomandator, adset_tel_recomandator, ad, info_suplimentare'; const src=String((e&&e.parameter&&e.parameter.src)||'').trim().toLowerCase(); const tbl=(src==='cp')?SUPA_TABLE_PIERDUTI:SUPA_TABLE; const rec=supaOneById_(id, select, tbl); if (!rec) { return HtmlService.createHtmlOutput('
ID UNIC inexistent.
'); } // permisiuni: Utilizator doar pe propriii clienți if (String(sess.role||'').toLowerCase() === 'utilizator'){ if (_s(rec.agent).trim() && _s(rec.agent).trim() !== _s(sess.user).trim()){ return HtmlService.createHtmlOutput('
Nu aveți drepturi pentru acest client.
'); } } const t = HtmlService.createTemplateFromFile('ClientEditor'); t.appTitle = APP_TITLE; t.user = sess.user; t.role = sess.role; t.id = id; t.token = tok; const headerName = _s(rec.nume_complet); const headerCnp = _s(rec.cnp); const tel = _s(rec.telefon); t.headerTitle = (headerName || 'Client') + (headerCnp ? ' — CNP ' + headerCnp : '') + (tel ? ' — TEL ' + tel : ''); // === DATE GENERALE t.agentValue = _s(rec.agent); t.dataClientValue = _toInputDate(_s(rec.data_client).replace(/\//g,'.')); t.canalValue = _s(rec.canal); t.campanieRecomandatorValue = _s(rec.campanie_recomandator); t.adsetTelefonRecomandatorValue = _s(rec.adset_tel_recomandator); t.adValue = _s(rec.ad); t.infoSuplimentareValue = _s(rec.info_suplimentare); // === INFORMAȚII PERSONALE t.prenumeValue = _s(rec.prenume); t.numeValue = _s(rec.nume_familie); t.numeCompletValue = _s(rec.nume_complet); t.cnpSurseValue = _s(rec.cnp); t.varstaValue = _ageFromBirthDate(rec.data_nasterii); t.telefonSurseValue = _s(rec.telefon); t.telefon2Value = _s(rec.telefon_2); t.emailValue = _s(rec.e_mail); t.emailCreatValue = _s(rec.e_mail_creat); t.judetCIMuncaValue = _s(rec.judet_ci_munca); t.regiuneaValue = _s(rec.regiunea); t.judetCIValue = _s(rec.judet); t.localitateValue = _s(rec.localitate); t.stradaValue = _s(rec.strada); t.nrStradaValue = _s(rec.nr_strada); t.blocValue = _s(rec.bloc); t.scaraValue = _s(rec.scara); t.etajValue = _s(rec.etaj); t.apartamentValue = _s(rec.apart); // === FINANCIAR – VENIT 1 / 2 t.tipVenit1Value = _s(rec.tip_venit_1); t.valoareVenit1Value = _s(rec.valoare_vernit_1 || rec.valoare_venit_1); t.angajator1Value = _s(rec.angajator_1); t.dataAngajarii1Value = _toInputDate(_s(rec.data_angajarii_1).replace(/\//g,'.')); t.vechime1Value = _s(rec.vechime_1); t.intrerupereUltimulAn1Value = _s(rec.intrerupere_1); t.verificareVenit1Value = _s(rec.verificare_venit_1); t.popriri1Value = _s(rec.popriri_pe_venit_1); t.tipAngajator1Value = _s(rec.tip_angajator_1); t.tipVenit2Value = _s(rec.tip_venit_2); t.valoareVenit2Value = _s(rec.valoare_venit_2); t.angajator2Value = _s(rec.angajator_2); t.dataAngajarii2Value = _toInputDate(_s(rec.data_angajarii_2).replace(/\//g,'.')); t.vechime2Value = _s(rec.vechime_2); t.intrerupereUltimulAn2Value = _s(rec.intrerupere_2); t.verificareVenit2Value = _s(rec.verificare_venit_2); t.popriri2Value = _s(rec.popriri_pe_venit_2); t.tipAngajator2Value = _s(rec.tip_angajator_2); // === BC – nu le mai citim din MDC; rămân goale și se încarcă din foaia user-ului prin editor_incarcaBC() t.dataRaportBCValue = ''; t.conturiBcTotalValue = ''; t.conturiBcActiveValue = ''; t.conturiBcInchiseValue = ''; t.ficoValue = ''; t.sumeRestanteCurenteValue = ''; t.stergereStandardValue = ''; t.stergereSpecialeValue = ''; t.rezultatInterogareBCValue = ''; t.situatieBcClientValue = ''; t.istoricBCValue = ''; t.codebitorAlteCrediteValue = ''; t.intarzieriCurenteValue = ''; t.intarzieriUlt3Value = ''; t.intarzieriUlt6Value = ''; // dacă există și în HTML; dacă nu, poți să o rămână t.intarzieriUlt12Value = ''; t.intarzieriUlt24Value = ''; t.intarzieriIstoricValue = ''; // (sau renunță dacă nu e folosită) t.mentiuniBCValue = ''; t.conturiIFNTotalValue = ''; t.conturiIFNActiveValue = ''; t.conturiIFNInchiseValue = ''; t.conturiBanciTotalValue = ''; // 👈 ADĂUGĂ ASTA t.conturiBanciActiveValue = ''; // 👈 ADĂUGĂ ASTA t.conturiBanciInchiseValue = ''; // 👈 ȘI ASTA t.interogariTotalValue = ''; t.interogariBanciValue = ''; t.interogariIFNuriValue = ''; t.crediteActiveRows = []; t.codebitorData = []; t.intarzieriCreditoriRows = []; t.interogariRows = []; // === GESTIONARE t.statusValue = _s(rec.status); t.dataUltimeiActivitatiValue = _toInputDate(_s(rec.dua).replace(/\//g,'.')); t.statusSecundarValue = _s(rec.status_secundar); t.dataViitoareiActivitatiValue = _toInputDate(_s(rec.dva).replace(/\//g,'.')); t.dvaMaximaValue = _toInputDate(_s(rec.dva_maxim).replace(/\//g,'.')); t.raportBCValue = _s(rec.raport_bc); t.istoricValue = _s(rec.istoric); t.observatiiValue = _s(rec.observatii); t.row = 2; // editorul tău folosește F1 pt. BC/Contracte t.isNew = isNew; t.warnTel = warnTel; t.showWarnTel = (isNew && warnTel); return t.evaluate() .setTitle(t.headerTitle) .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); } /** helper: _getSession cu mesaj curat */ function _safeGetSession_(token){ try { return _getSession(token); } catch(_){ return null; } } function _ageFromBirthDate(d){ if (!d) return ''; const dt = _toDate(d); // avem deja helperul _toDate mai jos în fișier if (!dt) return ''; const today = new Date(); let age = today.getFullYear() - dt.getFullYear(); const m = today.getMonth() - dt.getMonth(); if (m < 0 || (m === 0 && today.getDate() < dt.getDate())) { age--; } if (age < 0 || age > 120) return ''; return String(age); } /** ====== UI – opțiuni campanii din "Surse Tehnice" ====== **/ function ui_getCampaniiTehnice(){ const cache = CacheService.getScriptCache(); const CK = 'opt:campanii_tech:v2'; // 💡 bump la v2 ca să nu folosească cache-ul vechi try { const hit = cache.get(CK); if (hit) return { ok:true, campanii: JSON.parse(hit) }; } catch(_){} const ss = SpreadsheetApp.getActive(); // 1) Surse Tehnice!A601:A700 let fromSurse = []; try{ const sh = ss.getSheetByName('Surse Tehnice'); if (sh){ fromSurse = sh.getRange(601, 1, 100, 1) // A601:A700 .getValues() .map(r => _s(r[0]).trim()) .filter(_nz); } }catch(_){} // 2) Toate valorile existente în MDC, col. "CAMPANIE / RECOMANDATOR" let fromMdc = []; try{ const shM = _sheet(CLIENTI_SHEET); const H = getHeaderMap_(); const c = _col(H,'CAMPANIE / RECOMANDATOR'); if (c >= 0){ const start = 4, last = shM.getLastRow(); if (last >= start){ fromMdc = shM.getRange(start, c+1, last-start+1, 1) .getDisplayValues() .map(r => _s(r[0]).trim()) .filter(_nz); } } }catch(_){} // 3) Unire + dedup + sort const uniq = Array.from(new Set([].concat(fromSurse, fromMdc))) .filter(_nz) .sort((a,b)=> a.localeCompare(b,'ro')); try { cache.put(CK, JSON.stringify(uniq), 6*60*60); } catch(_){} return { ok:true, campanii: uniq }; } function editor_saveBC(idUnic, payload){ const shUser = _resolveUserSheetForId(String(idUnic || '').trim()); if (!shUser) return { ok:false, msg:'Nu am găsit foaia utilizatorului pentru acest client (ID UNIC).' }; const w = []; if (_has(payload,'rezultatInterogareBC')) w.push([MAP.bcGen.rezultat, payload.rezultatInterogareBC]); if (_has(payload,'situatieBcClient')) w.push([MAP.bcGen.situatie, payload.situatieBcClient]); if (_has(payload,'istoricBC')) w.push([MAP.bcGen.istoric, payload.istoricBC]); if (_has(payload,'codebitorAlteCredite')) w.push([MAP.bcGen.codebitor, payload.codebitorAlteCredite]); if (_has(payload,'sumeRestanteCurente')) w.push([MAP.bcGen.sumeRestante, _numOr(payload.sumeRestanteCurente)]); if (_has(payload,'stergereStandard')) w.push([MAP.bcGen.stergStd, _numOr(payload.stergereStandard)]); if (_has(payload,'stergereSpeciale')) w.push([MAP.bcGen.stergSpec, _numOr(payload.stergereSpeciale)]); _writePairs(shUser, w); return { ok:true, msg:'Salvat (Informații BC).' }; } // Alte acțiuni demo (parametrizate pe ID) function editor_createEmail(idUnic){ return { ok:true, msg:'TODO: creare email pentru ID ' + String(idUnic||'') }; } /** Încărcare LA CERERE a blocurilor BC din foaia userului (A16:M40 etc.). */ function editor_incarcaBC(token, idUnic){ const sess = _getSession(token); idUnic = String(idUnic||'').trim(); const shUser = SpreadsheetApp.getActive().getSheetByName(sess.user); if (!shUser) return { ok:false, msg:'Nu există foaia utilizatorului.' }; // 1) Context corect în foaia userului (D1 = ID UNIC) try{ const idD1 = String(shUser.getRange('D1').getValue()||'').trim(); if (idD1 !== idUnic){ shUser.getRange('D1').setValue(idUnic); } }catch(_){} // —— CREDITE ACTIVE ——————————————————————————————— const rawCA = shUser.getRange('A16:M40').getValues() .filter(r => _s(r[0]).trim() !== ''); const crediteActiveRows = rawCA.map(r => ([ _s(r[0]), _s(r[1]), _s(r[2]), _s(r[3]), _s(r[4]), _s(r[5]), _s(r[6]), _s(r[7]), _s(r[8]), '', // CREDIT CU CODEB. (select DA/NU în UI) _s(r[10]), // SE REFIN. (K) '', // EDITARE CONT (select) _s(r[12]) // CREF (M) ])); // —— CODEBITOR ———————————————————————————————— const codebitorData = shUser.getRange('A44:F48').getValues() .filter(r => _s(r[0]).trim() !== '') .map(r => ({ creditorCodebitor: _s(r[0]), intarzieriCurenteCodebitor: _s(r[1]), tipCreditCodebitor: _s(r[2]), rataCodebitor: _s(r[3]), soldCodebitor: _s(r[4]), codebitorPentruCodebitor: _s(r[5]), })); // —— ÎNTÂRZIERI ———————————————————————————————— const intarzieriCreditoriRows = shUser.getRange('G44:N53').getValues() .filter(r => _s(r[0]).trim() !== '') .map(r => r.map(_s)); // —— INTEROGĂRI ———————————————————————————————— const interogariRows = shUser.getRange('O44:X53').getValues() .filter(r => _s(r[0]).trim() !== '') .map(r => r.map(_s)); return { ok:true, data:{ crediteActiveRows, codebitorData, intarzieriCreditoriRows, interogariRows } }; } function editor_genIntermediar(idUnic){ return { ok:true, text:'Rezultat intermediar – demo', id:idUnic }; } function editor_genFinal(idUnic){ return { ok:true, final:'Rezultat final – demo', coment:'Comentarii – demo', id:idUnic }; } /** ====== Helpers generice ====== **/ function _sheet(name){ const sh = SpreadsheetApp.getActive().getSheetByName(name); if (!sh) throw new Error(`Sheet-ul "${name}" nu există.`); return sh; } function _s(v){ if (v==null) return ''; if (Object.prototype.toString.call(v)==='[object Date]') return Utilities.formatDate(v, Session.getScriptTimeZone(), 'dd.MM.yyyy'); return String(v); } function _nz(v){ return v!=null && v!==''; } function _has(obj, key){ return obj && Object.prototype.hasOwnProperty.call(obj, key) && obj[key]!==undefined; } function _numOr(v){ if (v == null) return ''; const s = String(v).replace(/\s+/g,'').replace(',','.'); if (s === '') return ''; // ← NU mai returna 0 const n = Number(s); return isFinite(n) ? n : v; } function _toInputDate(v){ if (v==null || v==='') return ''; if (Object.prototype.toString.call(v)==='[object Date]') return Utilities.formatDate(v, Session.getScriptTimeZone(), 'yyyy-MM-dd'); const s = String(v).trim(); const m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); if (m) return `${m[3]}-${('0'+m[2]).slice(-2)}-${('0'+m[1]).slice(-2)}`; const d = new Date(s); return isNaN(d) ? '' : Utilities.formatDate(d, Session.getScriptTimeZone(), 'yyyy-MM-dd'); } function _fmtDateHuman(v){ if (v==null || v==='') return ''; if (Object.prototype.toString.call(v)==='[object Date]') return Utilities.formatDate(v, Session.getScriptTimeZone(), 'dd.MM.yyyy'); return String(v); } function _toDate(input){ if (!input) return ''; if (input instanceof Date) return input; const s = String(input).trim(); if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { const [y,m,d] = s.split('-').map(Number); return new Date(y, m-1, d); } const d = new Date(s); return isNaN(d) ? '' : d; } function _readByMap(sh, mapAll){ const keys = Object.keys(mapAll); if (!keys.length) return {}; const addrs = keys.map(k => mapAll[k]); const ranges = sh.getRangeList(addrs).getRanges(); const out = {}; for (let i=0;ip[0])).getRanges(); pairs.forEach((p,idx)=> rl[idx].setValue(p[1])); } /** ====== Foaia utilizatorului pentru salvare: după ID UNIC în D1 ====== **/ function _resolveUserSheetForId(idUnic){ if (!idUnic) return null; const ss = SpreadsheetApp.getActive(); try { const shU = _sheet(USERS_SHEET); const users = shU.getRange(2,1,Math.max(0, shU.getLastRow()-1),1) .getValues().map(r=>String(r[0]||'').trim()).filter(Boolean); for (const u of users){ const sh = ss.getSheetByName(u); if (!sh) continue; const d1 = String(sh.getRange('D1').getValue() || '').trim(); if (d1 === idUnic) return sh; // sheet-ul care lucrează cu acel ID UNIC } } catch(e){ /* ignore */ } return null; } /** ====== UI – opțiuni AGENTI din Supabase (bd_useri_si_parole) ====== **/ function ui_getAgenti(){ const cache = CacheService.getScriptCache(); const CK = 'opt:agenti:v3'; // bump versiune cache; v2 putea avea listă goală // 1) Cache try { const hit = cache.get(CK); if (hit) return { ok:true, agenti: JSON.parse(hit) }; } catch (_) {} let vals = []; try { // luăm user + STATUS HR, ordonate alfabetic const qs = 'select=' + encodeURIComponent('USER,"TIP ANGAJAT","STATUS HR"') + '&order=' + encodeURIComponent('USER.asc'); const rows = supaGet_(SUPA_USERS_TABLE, qs) || []; vals = rows .filter(r => String(r['STATUS HR'] || '').trim().toUpperCase() === 'ACTIV') .map(r => _s(r.USER).trim()) .filter(_nz) .sort((a,b) => a.localeCompare(b,'ro')); } catch(e){ Logger.log('ui_getAgenti – eroare Supabase: ' + e); vals = []; } // 2) Cache-uim doar dacă avem ceva (ca să nu blocăm cu un array gol) if (vals.length){ try { cache.put(CK, JSON.stringify(vals), 6*60*60); } catch(_){} } return { ok:true, agenti: vals }; } /** ====== Marketing ====== **/ function marketing_listRows(token){ const sess = _getSession(token); const isUtil = (sess.role || '').toLowerCase() === 'utilizator'; const me = _s(sess.user).trim(); // luăm doar coloanele necesare pentru tab-ul ANIVERSĂRI const select = 'id_unic,agent,nume_complet,prenume,data_nasterii,' + 'telefon,telefon_2,e_mail,' + 'aniversare_credit,onomastica_1,onomastica_2,cnp'; let qs = 'select=' + encodeURIComponent(select); if (isUtil){ qs += '&agent=eq.' + encodeURIComponent(me); } // tragem TOȚI clienții din view-ul v_clienti_aniversari (already filtrat pe „azi”) const data = supaSelectAll_('v_clienti_aniversari', qs, 1000); const rows = (data || []).map(r => ({ agent: _s(r.agent), numeClient: _s(r.nume_complet), prenume: _s(r.prenume), varsta: _ageFromBirthDate(r.data_nasterii), telefon: _s(r.telefon), telefon2: _s(r.telefon_2), email: _s(r.e_mail), // pentru tabelul de la ANIVERSĂRI: // în vechiul MDC, "ANIVERSARE" era deja doar data "de azi" → aici o putem formata din data_nasterii aniversare: (function(){ if (!r.data_nasterii) return ''; const dob = _toDate(r.data_nasterii); if (!dob) return ''; const tz = Session.getScriptTimeZone(); // afișăm ziua/luna din anul curent (sau 28/29 feb, logic identică cu view-ul) const today = new Date(); let y = today.getFullYear(); let m = dob.getMonth()+1; let d = dob.getDate(); // mică grijă la 29 feb – dacă anul nu e bisect, arată 28.02 (ca în view) if (m === 2 && d === 29){ const isLeapYear = (y % 400 === 0) || (y % 4 === 0 && y % 100 !== 0); if (!isLeapYear) d = 28; } const bdThisYear = new Date(y, m-1, d); return Utilities.formatDate(bdThisYear, tz, 'dd/MM/yyyy'); })(), aniversareCredit: _s(r.aniversare_credit), onomastica: _s(r.onomastica_1) || _s(r.onomastica_2), cnp: _s(r.cnp), id: _s(r.id_unic) })); return { ok:true, rows, user:sess.user, role:sess.role }; } /** ====== PROGRAMĂRI (BD Programari) ====== **/ function _normPhone(s){ return String(s||'').replace(/\D+/g,''); } function _toIsoDay(v){ const tz = Session.getScriptTimeZone(); if (v == null || v === '') return ''; if (Object.prototype.toString.call(v) === '[object Date]') return Utilities.formatDate(v, tz, 'yyyy-MM-dd'); const s = String(v).trim(); const m = s.match(/^(\d{1,2})[.\-\/](\d{1,2})[.\-\/](\d{4})$/); if (m){ const d = new Date(+m[3], +m[2]-1, +m[1]); return Utilities.formatDate(d, tz, 'yyyy-MM-dd'); } const d = new Date(s); return isNaN(d) ? '' : Utilities.formatDate(d, tz, 'yyyy-MM-dd'); } function _timeToHHmm(v){ if (v == null || v === '') return ''; if (Object.prototype.toString.call(v) === '[object Date]') return Utilities.formatDate(v, Session.getScriptTimeZone(), 'HH:mm'); const s = String(v).trim(); let m = s.match(/^(\d{1,2})[:.](\d{2})(?::\d{2})?$/); if (m){ const hh = ('0'+m[1]).slice(-2); const mm = ('0'+m[2]).slice(-2); return `${hh}:${mm}`; } const n = Number(s); if (!isNaN(n) && n > 0 && n < 1){ const mins = Math.round(n * 24 * 60); const hh = String(Math.floor(mins/60)).padStart(2,'0'); const mm = String(mins % 60).padStart(2,'0'); return `${hh}:${mm}`; } return s; } function _weekdayROUpper(d){ const n = ['DUMINICA','LUNI','MARTI','MIERCURI','JOI','VINERI','SAMBATA']; return n[d.getDay()]; } /** Creează o programare în Supabase (bd_programari) pentru ID UNIC dat. */ function programari_create(token, idUnic, payload){ const sess = _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) return { ok:false, msg:'ID UNIC lipsă.' }; // luăm numele + telefonul din bd_clienti_activi const recCli = supaOneById_(idUnic, 'id_unic,nume_complet,telefon'); if (!recCli){ return { ok:false, msg:'ID UNIC inexistent în bd_clienti_activi.' }; } const dataIso = String(payload && payload.data || '').trim(); // yyyy-MM-dd, vine direct din input type="date" const ora = _timeToHHmm(payload && payload.ora); const loc = _s(payload && payload.locatie); const scop = _s(payload && payload.scop); if (!dataIso || !ora || !loc || !scop){ return { ok:false, msg:'Completează data, ora, locația și scopul.' }; } const telDigits = String(recCli.telefon || '').replace(/\D+/g,''); const row = { id_unic_client: Number(idUnic), agent: String(sess.user || '').trim(), data_programare: dataIso, // păstrăm ISO ca text ora_programare: ora, telefon_client: telDigits || null, nume_client: _s(recCli.nume_complet), locatia: loc, scop_programare: scop, status: null, // neconfirmată motiv: null, comentariu_agent: null, comentariu_manager: null, data_trimitere_sms: null, mesaj_mvt0: null }; const inserted = supaInsertOne_(SUPA_PROGRAMARI, row); // pentru UI returnăm exact ce aștepta și înainte return { ok:true, msg:'Programarea a fost înregistrată!', data: _fmtDateHuman(_fromYMD_(dataIso)), ora: ora, telefon: telDigits, nume: _s(recCli.nume_complet) }; } /** Info consilier din "Useri si Parole" */ function programari_getAdvisor(token){ const sess = _getSession(token); const userCode = String(sess.user || '').trim(); if (!userCode) return { ok:true, nume:'', adresa:'', telefon:'', email:'' }; try{ const qs = 'USER=eq.' + encodeURIComponent(userCode) + '&select=' + encodeURIComponent('"NUME COMPLET USER",ADRESA,TELEFON,"E-MAIL"') + '&limit=1'; const rows = supaGet_(SUPA_USERS_TABLE, qs); if (!rows || !rows.length){ return { ok:true, nume:'', adresa:'', telefon:'', email:'' }; } const r = rows[0]; let tel = String(r.TELEFON || '').trim(); const digits = tel.replace(/\D+/g,''); if (/^07\d{8}$/.test(digits)){ tel = digits.replace(/^(\d{4})(\d{3})(\d{3})$/, '$1.$2.$3'); } return { ok: true, nume: String(r['NUME COMPLET USER'] || '').trim(), adresa: String(r.ADRESA || '').trim(), telefon: tel, email: String(r['E-MAIL'] || '').trim() }; }catch(_){ return { ok:true, nume:'', adresa:'', telefon:'', email:'' }; } } /** Listă programări (5 zile lucrătoare) din Supabase. */ function programari_list(token, startIso, days){ const sess = _getSession(token); const tz = Session.getScriptTimeZone(); const count = Math.max(1, Number(days || 5)); const showAll = (String(sess.role||'').toLowerCase() !== 'utilizator'); const me = _s(sess.user).trim(); // construim fereastra de 5 zile lucrătoare ca înainte let d = startIso ? new Date(startIso + 'T00:00:00') : new Date(); const daysOut = []; while (daysOut.length < count){ const dow = d.getDay(); if (dow !== 0 && dow !== 6){ daysOut.push({ iso: Utilities.formatDate(d, tz, 'yyyy-MM-dd'), label: _weekdayROUpper(d) + ' - ' + Utilities.formatDate(d, tz, 'dd/MM/yyyy') }); } d = new Date(d.getFullYear(), d.getMonth(), d.getDate()+1); } // sloturi oră (le păstrăm identic) const times = []; for (let h=9; h<=17; h++){ times.push((('0'+h).slice(-2))+':00'); if (h<17) times.push((('0'+h).slice(-2))+':30'); if (h===17) times.push('17:30'); } while (times[times.length-1] === '18:00') times.pop(); const events = {}; daysOut.forEach(x => { events[x.iso] = {}; }); // citim programările din Supabase if (daysOut.length){ const fromIso = daysOut[0].iso; const toIso = daysOut[daysOut.length-1].iso; const conds = [ 'data_programare=gte.' + encodeURIComponent(fromIso), 'data_programare=lte.' + encodeURIComponent(toIso) ]; if (!showAll){ conds.push('agent=eq.' + encodeURIComponent(me)); } const select = 'id,id_unic_client,agent,data_programare,ora_programare,telefon_client,nume_client,locatia,scop_programare,status'; const qs = 'select=' + encodeURIComponent(select) + '&' + conds.join('&') + '&order=data_programare.asc&order=ora_programare.asc'; const rows = supaGet_(SUPA_PROGRAMARI, qs) || []; const onlyIso = new Set(daysOut.map(x=>x.iso)); rows.forEach(r => { const iso = String(r.data_programare || '').trim(); const time = _timeToHHmm(r.ora_programare); if (!iso || !onlyIso.has(iso) || !time) return; const agent = _s(r.agent); const tel = _s(r.telefon_client); const nume = _s(r.nume_client); const scop = _s(r.scop_programare); const status= _s(r.status); const base = [nume, tel, scop, status].filter(Boolean).join(' / '); const label = showAll ? (agent + ' — ' + base) : base; if (!events[iso][time]) events[iso][time] = []; events[iso][time].push(label); }); } return { ok:true, days:daysOut, times:times, events:events }; } /** CONFIRMĂRI – programări cu STATUS NULL și DATA < azi (din Supabase). */ function programari_listConfirm(token){ const sess = _getSession(token); const role = String(sess.role||'').toLowerCase(); const showAll = (role !== 'utilizator'); const me = _s(sess.user).trim(); const tz = Session.getScriptTimeZone(); const todayIso = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd'); const conds = [ 'data_programare=lt.' + encodeURIComponent(todayIso), 'status=is.null' ]; if (!showAll){ conds.push('agent=eq.' + encodeURIComponent(me)); } const select = 'id,agent,data_programare,ora_programare,telefon_client,nume_client,locatia,scop_programare,status,motiv,comentariu_agent'; const qs = 'select=' + encodeURIComponent(select) + '&' + conds.join('&') + '&order=data_programare.asc&order=ora_programare.asc'; const rows = supaGet_(SUPA_PROGRAMARI, qs) || []; const out = []; rows.forEach(r => { const iso = String(r.data_programare || '').trim(); const dataDisp = iso ? _fmtDateHuman(_fromYMD_(iso)) : ''; out.push({ row: r.id, // << acum e ID-ul programării agent: _s(r.agent), data: dataDisp, ora: _timeToHHmm(r.ora_programare), tel: _s(r.telefon_client), nume: _s(r.nume_client), loc: _s(r.locatia), scop: _s(r.scop_programare), status:_s(r.status), motiv: _s(r.motiv), comentA: _s(r.comentariu_agent) }); }); return { ok:true, rows: out }; } /** NEREALIZATE – STATUS ∈ {NEREALIZATA, ANULATA} cu interval și filtre din Supabase. */ function programari_listNerealizate(token, fromIso, toIso, agentFilter, statusFilter){ const sess = _getSession(token); const role = String(sess.role||'').toLowerCase(); const showAll = (role !== 'utilizator'); const me = _s(sess.user).trim().toUpperCase(); const from = String(fromIso || '').trim(); // yyyy-MM-dd const to = String(toIso || '').trim(); const wantedStatus = String(statusFilter || '').trim().toUpperCase(); const wantedAgent = showAll ? String(agentFilter || '').trim().toUpperCase() : me; const conds = [ 'status=in.(NEREALIZATA,ANULATA)' ]; if (from) conds.push('data_programare=gte.' + encodeURIComponent(from)); if (to) conds.push('data_programare=lte.' + encodeURIComponent(to)); if (wantedAgent) conds.push('agent=eq.' + encodeURIComponent(wantedAgent)); const select = 'id,agent,data_programare,ora_programare,telefon_client,nume_client,locatia,scop_programare,status,motiv,comentariu_agent,comentariu_manager'; let qs = 'select=' + encodeURIComponent(select) + '&' + conds.join('&') + '&order=data_programare.asc&order=ora_programare.asc'; const rows = supaGet_(SUPA_PROGRAMARI, qs) || []; const out = []; rows.forEach(r => { const status = String(r.status || '').toUpperCase(); if (wantedStatus && status !== wantedStatus) return; const iso = String(r.data_programare || '').trim(); const dataDisp = iso ? _fmtDateHuman(_fromYMD_(iso)) : ''; out.push({ row: r.id, // ID programare agent: _s(r.agent), data: dataDisp, ora: _timeToHHmm(r.ora_programare), tel: _s(r.telefon_client), nume: _s(r.nume_client), loc: _s(r.locatia), scop: _s(r.scop_programare), status: status, motiv: _s(r.motiv), comentAgent: _s(r.comentariu_agent), comentManager: _s(r.comentariu_manager) }); }); return { ok:true, rows: out }; } /** ====== Asistent Vanzari AI – filtrare (ID UNIC, nu row) ====== **/ function asistentAI_listRows(token, fromIso, toIso, agentFilter, statusFilter){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase().trim(); const meCode = _s(sess.user).trim().toUpperCase(); const showAll = role !== 'utilizator'; const from = String(fromIso || '').trim(); // yyyy-MM-dd const to = String(toIso || '').trim(); // yyyy-MM-dd const wantedStatus = String(statusFilter || '').trim().toUpperCase(); const wantedAgent = showAll ? String(agentFilter || '').trim().toUpperCase() : meCode; // vrem numai NEINTERESAT + CL. REFUZA COLABORAREA const allowed = new Set(['NEINTERESAT','CL. REFUZA COLABORAREA']); const sh = _sheet(CLIENTI_SHEET); const startRow = 4; const lastRow = sh.getLastRow(); if (lastRow < startRow) return { ok:true, rows:[], user:sess.user, role:sess.role }; const H = getHeaderMap_(); const width = sh.getLastColumn(); const data = sh.getRange(startRow, 1, lastRow - startRow + 1, width).getValues(); const out = []; data.forEach(r => { const agent = _s(_val(r,H,'AGENT')).trim().toUpperCase(); if (!agent) return; if (!showAll && agent !== meCode) return; if (wantedAgent && agent !== wantedAgent) return; const statusSec = _s(_val(r,H,'STATUS SECUNDAR')).trim().toUpperCase(); if (!allowed.has(statusSec)) return; if (wantedStatus && statusSec !== wantedStatus) return; const duaRaw = _val(r,H,'DUA'); // filtrăm strict pe DUA const iso = _toIsoDay(duaRaw); const inRange = (!from || (iso && iso >= from)) && (!to || (iso && iso <= to)); if (!inRange) return; const analiza = _val(r,H,'ANALIZA AI'); // fallback, dacă foaia ta încă nu are coloana nouă const idUnic = _s(_val(r,H,'ID UNIC')).trim(); out.push({ id: idUnic, agent, client: _s(_val(r,H,'NUME COMPLET')), status: statusSec, dataUltAct: _fmtDateHuman(duaRaw), // în tabel apare ca “DUA” recomandariAI: _s(analiza) // din “ANALIZA AI” }); }); return { ok:true, rows:out, user:sess.user, role:sess.role }; } /** Găsește sheetul de editare pentru utilizatorul curent (după userKey) */ function getEditorSheet_(userKey){ const ss = SpreadsheetApp.getActiveSpreadsheet(); if (userKey){ let sh = ss.getSheetByName(String(userKey)); if (sh) return sh; const maybe = String(userKey).split('@')[0].toUpperCase(); sh = ss.getSheetByName(maybe); if (sh) return sh; } throw new Error('Sheet de editare inexistent pentru ' + (userKey || 'utilizatorul curent')); } /** ====== MARKETING: Contact consilier din "Useri si Parole" ====== **/ /** ====== MARKETING: Contact consilier din "Useri si Parole" ====== **/ function marketing_getUserContact(token){ const sess = _getSession(token); const cache = CacheService.getScriptCache(); const CK = 'opt:usercontact:' + _s(sess.user); try{ const hit = cache.get(CK); if (hit){ const obj = JSON.parse(hit); // 🔹 acum trimitem și adresa, dacă există în cache return { ok: true, nume: obj.nume || '', telefon: obj.telefon || '', adresa: obj.adresa || '' }; } }catch(_){} const userCode = String(sess.user || '').trim(); let nume = '', tel = '', adr = ''; // 🔹 adr = adresa consilierului try{ const qs = 'USER=eq.' + encodeURIComponent(userCode) + // 🔹 includem și ADRESA în select '&select=' + encodeURIComponent('"NUME COMPLET USER",TELEFON,ADRESA') + '&limit=1'; const rows = supaGet_(SUPA_USERS_TABLE, qs); if (rows && rows.length){ const r = rows[0]; nume = String(r['NUME COMPLET USER'] || '').trim(); tel = String(r.TELEFON || '').trim(); adr = String(r.ADRESA || '').trim(); // 🔹 luăm adresa din tabel } }catch(_){} // 🔹 salvăm și adresa în cache try{ cache.put( CK, JSON.stringify({ nume: nume, telefon: tel, adresa: adr }), 6 * 60 * 60 ); }catch(_){} // 🔹 o trimitem și în răspuns return { ok: true, nume: nume, telefon: tel, adresa: adr }; } /** DATE GENERALE – salvează în bd_clienti_activi (Supabase), nu mai scrie în MDC. */ function editor_saveDateGeneraleMDC(token, idUnic, data){ _getSession(token); // doar validare sesiune idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); data = data || {}; var patch = { agent: data.agent || null, data_client: data.dataClient || null, canal: data.canal || null, campanie_recomandator: data.campanie || null, adset_tel_recomandator: data.adsetTelefon || null, ad: data.ad || null, info_suplimentare: data.info || null }; return clientiSupabase_patch_(idUnic, patch); } /** INFORMAȚII PERSONALE – scriere doar în Supabase. */ function editor_saveDatePersonaleMDC(token, idUnic, data){ _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); data = data || {}; var patch = { prenume: data.prenume || null, nume_familie: data.nume || null, // ✅ SCHIMBAT: nume_familie nume_complet: data.numeComplet || null, cnp: data.cnp || null, telefon: data.telefon || null, telefon_2: data.telefon2 || null, e_mail: data.email || null, e_mail_creat: data.emailCreat || null, judet_ci_munca: data.judetCIMunca || null, judet: data.judetCI || null, localitate: data.localitate || null, strada: data.strada || null, nr_strada: data.nr || null, bloc: data.bloc || null, scara: data.sc || null, etaj: data.etaj || null, apart: data.ap || null }; return clientiSupabase_patch_(idUnic, patch); } /** INFORMAȚII FINANCIARE – scriere doar în Supabase. */ function editor_saveFinanciarMDC(token, idUnic, data){ _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); data = data || {}; var patch = { // VENIT 1 tip_venit_1: data.tipVenit1 || null, valoare_venit_1: data.valoareVenit1 || null, angajator_1: data.angajator1 || null, data_angajarii_1: data.dataAngajarii1 || null, vechime_1: data.vechime1 || null, intrerupere_1: data.intrerupereUltimulAn1 || null, verificare_venit_1: data.verificareVenit1|| null, popriri_pe_venit_1: data.popriri1 || null, tip_angajator_1: data.tipAngajator1 || null, // VENIT 2 tip_venit_2: data.tipVenit2 || null, valoare_venit_2: data.valoareVenit2 || null, angajator_2: data.angajator2 || null, data_angajarii_2: data.dataAngajarii2 || null, vechime_2: data.vechime2 || null, intrerupere_2: data.intrerupereUltimulAn2 || null, verificare_venit_2: data.verificareVenit2|| null, popriri_pe_venit_2: data.popriri2 || null, tip_angajator_2: data.tipAngajator2 || null }; return clientiSupabase_patch_(idUnic, patch); } /** ====== SAVE in MDC – GESTIONARE (campuri + ISTORIC + OBSERVATII) ====== **/ function editor_saveGestionareMDC(token, idUnic, payload){ const sess = _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); const sh = _sheet(CLIENTI_SHEET); const H = getHeaderMap_(); const row = findMdcRowById_(idUnic); if (row < 2) throw new Error('ID UNIC inexistent în MDC.'); // Utilizator poate edita doar propriii clienți const role = String(sess.role || '').toLowerCase(); const valuesRow = sh.getRange(row, 1, 1, sh.getLastColumn()).getValues()[0] || []; const agentOfRow = _s(_val(valuesRow, H, 'AGENT')).trim(); if (role === 'utilizator' && agentOfRow && agentOfRow !== _s(sess.user).trim()){ throw new Error('Nu aveți drepturi pentru acest client.'); } // helper generic de scriere pe antet const setAt = (header, value, transform) => { const c = _col(H, header); if (c < 0) return false; const v = transform ? transform(value) : value; sh.getRange(row, c+1).setValue(v == null ? '' : v); return true; }; // ========= 1) Scriem câmpurile declarative ========= setAt('STATUS', payload && payload.status); setAt('DUA', payload && payload.dua, _toDate); setAt('STATUS SECUNDAR', payload && payload.statusSecundar); setAt('DVA', payload && payload.dva, _toDate); setAt('DVA MAXIM', payload && payload.dvaMaxim, _toDate); setAt('RAPORT BC', payload && payload.raportBC); // ========= 2) ISTORIC + OBSERVAȚII (append cu dată & user) ========= const tz = Session.getScriptTimeZone(); const azi = Utilities.formatDate(new Date(), tz, 'dd/MM/yyyy'); const user = _s(sess.user); // valori curente const curIstoric = _s(_val(valuesRow, H, 'ISTORIC')); const colObsKey = _col(H,'OBSERVATII') >= 0 ? 'OBSERVATII' : (_col(H,'OBSERVATII ARATATE') >= 0 ? 'OBSERVATII ARATATE' : null); if (!colObsKey) throw new Error('Nu găsesc coloana „OBSERVATII” în MDC.'); const curObs = _s(_val(valuesRow, H, colObsKey)); // linii noi const st = _s(payload && payload.status).trim(); const s2 = _s(payload && payload.statusSecundar).trim(); const lineIstoric = (st || s2) ? `${azi} - ${user} - ${[st, s2].filter(Boolean).join(' / ')}` : ''; const obsNou = _s(payload && payload.observatiiNoi).trim(); const lineObs = obsNou ? `${azi} - ${user} - ${obsNou}` : ''; // compuneri let newIstoric = curIstoric, newObs = curObs; if (lineIstoric){ newIstoric = curIstoric ? (curIstoric + '\n' + lineIstoric) : lineIstoric; setAt('ISTORIC', newIstoric); } if (lineObs){ newObs = curObs ? (curObs + '\n' + lineObs) : lineObs; setAt(colObsKey, newObs); } // Încălzește indexul/cashe-ul try { index_upsert(idUnic, row); } catch (_) {} return { ok:true, istoric:newIstoric, observatii:newObs }; } /** ====== HR – PREZENȚĂ (STATUS ZILNIC) ====== **/ function hr_getPrezentaStatus(token){ const sess = _getSession(token); const userCode = String(sess.user || '').trim(); if (!userCode) return { ok:true, status:'INACTIV' }; try{ const qs = 'USER=eq.' + encodeURIComponent(userCode) + '&select=' + encodeURIComponent('"STATUS ZILNIC"') + '&limit=1'; const rows = supaGet_(SUPA_USERS_TABLE, qs); if (!rows || !rows.length) return { ok:true, status:'INACTIV' }; const v = String(rows[0]['STATUS ZILNIC'] || '').trim().toUpperCase(); return { ok:true, status: (v === 'ACTIV' ? 'ACTIV' : 'INACTIV') }; }catch(_){ return { ok:true, status:'INACTIV' }; } } function hr_setPrezentaStatus(token, status){ const sess = _getSession(token); let v = String(status || '').trim().toUpperCase(); if (v !== 'ACTIV' && v !== 'INACTIV') v = 'INACTIV'; const userCode = String(sess.user || '').trim(); if (!userCode) return { ok:false, msg:'User lipsă.' }; try{ supaUpsert_(SUPA_USERS_TABLE, [{ USER: userCode, 'STATUS ZILNIC': v }], 'USER'); return { ok:true, saved:v }; }catch(e){ return { ok:false, msg: e && e.message ? e.message : String(e) }; } } // helper intern: caută în Supabase după CNP și întoarce câmpurile necesare pentru UI "Client Nou" function _supaFindByCnp_(table, cnpDigits){ const qs = 'cnp=eq.' + encodeURIComponent(cnpDigits) + '&select=' + encodeURIComponent([ 'agent', 'data_client', 'nume_complet', 'status', 'dua', 'status_secundar', 'dva', 'observatii_aratate', 'telefon', 'e_mail_creat' ].join(',')) + '&limit=1'; const arr = supaGet_(table, qs); if (!arr || !arr.length) return null; const r = arr[0]; return { agent: _s(r.agent), dataClient: _fmtDateHuman(r.data_client), // convertim la dd.MM.yyyy numeComplet: _s(r.nume_complet), status: _s(r.status), dua: _fmtDateHuman(r.dua), statusSecundar:_s(r.status_secundar), dva: _fmtDateHuman(r.dva), observatii: _s(r.observatii_aratate), telefon: _s(r.telefon), emailCreat: _s(r.e_mail_creat) }; } /** CLIENT NOU – căutare după CNP în Supabase (ACTIVI / PIERDUȚI) */ function clientnou_lookup(token, cnpRaw){ _getSession(token); const cnp = String(cnpRaw||'').replace(/\D+/g,''); if (!cnp) return { ok:false, error:'CNP lipsă.' }; // 1) clienți activi (echivalent MDC) const act = _supaFindByCnp_(SUPA_TABLE, cnp); if (act){ return { ok:true, found:true, source:'MDC', data: act }; // păstrăm string-ul "MDC" pentru compatibilitate UI } // 2) clienți pierduți (echivalent BD AAA) const lost = _supaFindByCnp_(SUPA_TABLE_PIERDUTI, cnp); if (lost){ return { ok:true, found:true, source:'AAA', data: lost }; // păstrăm "AAA" = concept "client pierdut" } // 3) nicăieri return { ok:true, found:false }; } /** Caută CNP într-o foaie cu antete pe headerRow și date de la startRow; returnează câmpurile cerute sau null */ function _findByCnpInSheet_(sheetName, cnpDigits, headerRow, startRow){ const sh = _sheet(sheetName); const last = sh.getLastRow(); headerRow = headerRow || 2; startRow = startRow || (headerRow + 1); if (last < startRow) return null; // mapare antete (case-insensitive) const Hrow = sh.getRange(headerRow, 1, 1, sh.getLastColumn()).getValues()[0] || []; const H = {}; Hrow.forEach((h,i)=>{ const k=String(h||'').trim().toUpperCase(); if (k) H[k]=i; }); const cCnp = H['CNP']; if (cCnp == null) return null; // căutare CNP (curățăm până la cifre — numeric/șir/format științific) const colVals = sh.getRange(startRow, cCnp+1, last - startRow + 1, 1).getValues(); let rowFound = -1; for (let i=0;i { const c = H[name]; if (c == null) return ''; return sh.getRange(rowFound, c+1).getValue(); }; const toHuman = (v) => _fmtDateHuman(v); // OBSERVATII poate fi în “OBSERVATII” sau “OBSERVATII ARATATE” const obs = (function(){ const c1 = H['OBSERVATII'], c2 = H['OBSERVATII ARATATE']; const v1 = (c1!=null) ? sh.getRange(rowFound, c1+1).getValue() : ''; const v2 = (c2!=null) ? sh.getRange(rowFound, c2+1).getValue() : ''; return _s(v1 || v2); })(); const tel = _s(get('TELEFON')); // ← ADĂUGAT const emailCreat = _s(get('E-MAIL CREAT')); // ← ADĂUGAT return { agent: _s(get('AGENT')), dataClient: toHuman(get('DATA CLIENT')), numeComplet: _s(get('NUME COMPLET')), status: _s(get('STATUS')), dua: toHuman(get('DUA')), statusSecundar:_s(get('STATUS SECUNDAR')), dva: toHuman(get('DVA')), observatii: obs, telefon: tel, // ← ADĂUGAT emailCreat: emailCreat // ← ADĂUGAT }; } /** Calculează următorul ID UNIC = (max ID numeric existent) + 1 */ function _nextIdUnic_(sh, H, startRow){ const idCol = _col(H, 'ID UNIC'); if (idCol < 0) throw new Error('Lipsește coloana "ID UNIC" în MDC.'); const last = sh.getLastRow(); if (last < startRow) return 1; const vals = sh.getRange(startRow, idCol + 1, last - startRow + 1, 1).getValues(); let maxId = 0; for (let i = 0; i < vals.length; i++){ const raw = String(vals[i][0] == null ? '' : vals[i][0]).trim(); if (!raw) continue; const n = Number(raw.replace(/\D+/g,'')); // suportă text/număr/format if (isFinite(n) && n > maxId) maxId = n; } return maxId + 1; } /** ====== CLIENT NOU – înregistrare în Supabase (curat) ====== */ function clientnou_register(token, cnpRaw) { const sess = _getSession(token); const tz = Session.getScriptTimeZone(); const today= new Date(); const shTokenUser = String(sess.user || '').trim(); const cnp = String(cnpRaw || '').replace(/\D+/g, ''); if (!cnp) return { ok:false, msg:'CNP lipsă.' }; // 0) Verificăm duplicate în ACTIVI const dupArr = supaGet_( SUPA_TABLE, 'cnp=eq.' + encodeURIComponent(cnp) + '&select=' + encodeURIComponent('id_unic,agent') + '&limit=1' ); if (dupArr && dupArr.length) { return { ok:false, exists:true, msg:'NU POTI INREGISTRA UN CLIENT ALOCAT DEJA LA UN MDC!' }; } // 1) Căutăm clientul în PIERDUȚI (AAA) let aaaHit = false; let fromLost = null; try { const lostArr = supaGet_( SUPA_TABLE_PIERDUTI, 'cnp=eq.' + encodeURIComponent(cnp) + '&select=' + encodeURIComponent([ 'id_unic','nume_complet','prenume','nume_familie','telefon','telefon_2', 'e_mail','e_mail_creat','judet_ci_munca','regiunea','judet','localitate', 'strada','nr_strada','bloc','scara','etaj','apart', 'observatii_aratate','observatii' ].join(',')) + '&limit=1' ); if (lostArr && lostArr.length) { fromLost = lostArr[0]; aaaHit = true; } } catch(_) {} const dataIso = Utilities.formatDate(today, tz, 'yyyy-MM-dd'); let newId, up; // 2) Dacă vine din PIERDUȚI → mutăm cu același id_unic (dacă e liber) if (fromLost) { let oldId = String(fromLost.id_unic || '').trim(); if (!oldId) oldId = _nextIdUnicSupabase_(); // Dacă ID există deja în ACTIVI → generăm altul let useId = oldId; try { const check = supaGet_( SUPA_TABLE, 'id_unic=eq.' + encodeURIComponent(oldId) + '&select=id_unic&limit=1' ); if (check && check.length) { useId = _nextIdUnicSupabase_(); } } catch(_) {} newId = useId; const prefill = { nume_complet: _s(fromLost.nume_complet), prenume: _s(fromLost.prenume), nume_familie: _s(fromLost.nume_familie), telefon: _s(fromLost.telefon), telefon_2: _s(fromLost.telefon_2), e_mail: _s(fromLost.e_mail), e_mail_creat: _s(fromLost.e_mail_creat), judet_ci_munca: _s(fromLost.judet_ci_munca), regiunea: _s(fromLost.regiunea), judet: _s(fromLost.judet), localitate: _s(fromLost.localitate), strada: _s(fromLost.strada), nr_strada: _s(fromLost.nr_strada), bloc: _s(fromLost.bloc), scara: _s(fromLost.scara), etaj: _s(fromLost.etaj), apart: _s(fromLost.apart), observatii_aratate: _s(fromLost.observatii_aratate), observatii: _s(fromLost.observatii) }; up = Object.assign({}, prefill, { id_unic: String(newId), agent: shTokenUser, cnp: cnp, data_client: dataIso, status: 'ALOCAT', dua: dataIso, status_secundar: 'ALOCAT', dva: null, dva_maxim: null }); } else { // 3) CLIENT NOU newId = _nextIdUnicSupabase_(); up = { id_unic: String(newId), agent: shTokenUser, cnp: cnp, data_client: dataIso, status: 'ALOCAT', dua: dataIso, status_secundar: 'ALOCAT', dva: null, dva_maxim: null }; } // 4) INSERT în ACTIVI const rec = supaInsertOne_(SUPA_TABLE, up); // 5) Ștergere din PIERDUȚI dacă e cazul if (fromLost) { try { const delUrl = `${SUPA_URL}/rest/v1/${SUPA_TABLE_PIERDUTI}?id_unic=eq.${encodeURIComponent(String(fromLost.id_unic || newId))}`; UrlFetchApp.fetch(delUrl, { method: 'delete', headers: supaHeaders_(false), muteHttpExceptions: true }); } catch(_) {} } // 6) Construim URL-ul editorului cu funcția curată const url = getEditorUrl(token, String(rec.id_unic)) + '&new=1' + (aaaHit ? '&warnTel=1' : ''); return { ok:true, id:String(rec.id_unic), row:0, url:url }; } /** ====== UTIL: weekend checker + next working day ====== */ function _isWeekend_(d){ var w=d.getDay(); return w===0 || w===6; } function _addWorkingDays_(date, n){ var d = new Date(date.getFullYear(), date.getMonth(), date.getDate()); var step = n >= 0 ? 1 : -1, left = Math.abs(n); while (left > 0){ d.setDate(d.getDate() + step); if (!_isWeekend_(d)) left--; } return d; } /** ====== EDITOR: caută CNP în MDC/AAA pentru pagina de editare ====== * Return: * { ok:true, where:'MDC'|'AAA'|null, agent:'', sameOwner:true|false } */ /** CAUTARE CNP în Supabase: mai întâi ACTIVI, apoi PIERDUȚI. */ function editor_cautaCnp(token, idUnic, cnp){ var sess = _getSession(token); cnp = String(cnp || '').replace(/\D+/g,''); if (cnp.length < 5) return { ok:false, msg:'CNP prea scurt.' }; var user = String(sess.user || '').trim().toUpperCase(); // 1) caută în bd_clienti_activi var qsAct = 'cnp=eq.' + encodeURIComponent(cnp); var rowsA = []; try{ rowsA = supaGet_(SUPA_TABLE, qsAct); }catch(_){ rowsA = []; } if (rowsA && rowsA.length){ var rA = rowsA[0]; var agent = String(rA.agent || '').trim(); var same = agent.toUpperCase() === user; return { ok:true, where:'MDC', // lăsăm "MDC" ca să nu stricăm JS-ul, dar semnificația e "ACTIVI" agent:agent, sameOwner:same, msg:'', }; } // 2) caută în bd_clienti_pierduti var rowsP = []; try{ rowsP = supaGet_(SUPA_TABLE_PIERDUTI, qsAct); }catch(_){ rowsP = []; } if (rowsP && rowsP.length){ return { ok:true, where:'AAA', // frontend tratează ca "Clienți Pierduți" agent:'', sameOwner:false, msg:'Clientul există în baza de date Clienți Pierduți, dar poți continua cu editarea lui!' }; } // 3) nu există nicăieri return { ok:true, where:'NONE', agent:'', sameOwner:false, msg:'Clientul nu există în baza de date, poți continua cu editarea lui!' }; } function editor_actualizeazaClientNotifica(token, cnpRaw){ var sess = _getSession(token); var sh = _sheet('MDC Clienti Activi'); var H = getHeaderMap_(); var cnp = String(cnpRaw||'').replace(/\D+/g,''); if (!cnp) return { ok:false, msg:'CNP lipsă.' }; var cCnp = _col(H,'CNP'); if (cCnp < 0) return { ok:false, msg:'Nu există coloana "CNP" în MDC.' }; var last = sh.getLastRow(); var rowFound = -1; if (last >= 4){ var vals = sh.getRange(4, cCnp+1, last-3, 1).getValues(); for (var i=0;i= 0){ telefonClient = _s(sh.getRange(rowFound, cTel+1).getValue()).trim(); } if (!telefonClient) telefonClient = '(necunoscut)'; // scrieri var azi = new Date(); var dvaMax = _addWorkingDays_(azi, 1); function setAt(header, val){ var c = _col(H, header); if (c >= 0) sh.getRange(rowFound, c+1).setValue(val==null?'':val); } setAt('STATUS', 'ALOCAT'); setAt('DUA', azi); setAt('STATUS SECUNDAR', 'ALOCAT'); setAt('DVA', azi); setAt('DVA MAXIM', dvaMax); // Observații / Istoric (append) — cu telefonul curent var tz = Session.getScriptTimeZone(); var dmy = Utilities.formatDate(azi, tz, 'dd/MM/yyyy'); var cObs = _col(H,'OBSERVATII') >= 0 ? 'OBSERVATII' : (_col(H,'OBSERVATII ARATATE') >= 0 ? 'OBSERVATII ARATATE' : null); if (!cObs) throw new Error('Nu găsesc nici "OBSERVATII" nici "OBSERVATII ARATATE" în MDC.'); var curObs = _s(sh.getRange(rowFound, _col(H,cObs)+1).getValue()); var newObsLine = dmy + ' – SISTEM – Clientul a reaplicat. ' + 'Numar de telefon curent: ' + telefonClient + '.'; var newObs = (curObs ? curObs + '\n' : '') + newObsLine; sh.getRange(rowFound, _col(H,cObs)+1).setValue(newObs); var curIst = _s(sh.getRange(rowFound, _col(H,'ISTORIC')+1).getValue()); var newIst = (curIst ? curIst + '\n' : '') + (dmy + ' – SISTEM – REAPLICAT.'); sh.getRange(rowFound, _col(H,'ISTORIC')+1).setValue(newIst); // emailuri (include si telefonul) var baseRecipients = [ 'robert.marcu@smart-credit.ro', 'alexandra.tacea@smart-credit.ro', 'iuliana.sanduc@smart-credit.ro' ]; var agentEmail = (function(){ try{ var shU = _sheet('Useri si Parole'); var lastU = shU.getLastRow(); if (lastU < 2) return ''; var Hrow = shU.getRange(1,1,1,shU.getLastColumn()).getValues()[0] || []; var HU = {}; Hrow.forEach(function(h,i){ var k=String(h||'').trim().toUpperCase(); if(k) HU[k]=i; }); var cUser = HU['USER'], cMail = (HU['E-MAIL']!=null ? HU['E-MAIL'] : (HU['EMAIL']!=null ? HU['EMAIL'] : null)); if (cUser==null || cMail==null) return ''; var rows = shU.getRange(2,1,lastU-1,shU.getLastColumn()).getValues(); for (var i=0;i'+body+'', noReply: true }); }catch(e){ // nu blocăm fluxul pe eroare la email } return { ok:true }; } /** Căutare „wide” în MDC după CNP sau Telefon. * - Utilizator: caută doar în clienții lui (AGENT == user), inclusiv RESPINS * - Manager/Controlor/Administrator: caută în toți * Returnează o linie în formatul așteptat de tabelul „Lista Clienți Activi”. */ function mdc_lookupByCnpTel(token, cnpRaw, telRaw){ const sess = _getSession(token); const isUtil = String(sess.role||'').toLowerCase().trim() === 'utilizator'; const me = _s(sess.user).trim(); const cnp = String(cnpRaw||'').replace(/\D+/g,''); const tel = String(telRaw||'').replace(/\D+/g,''); if (!cnp && !tel) return { ok:true, found:false }; // !!! ATENȚIE: am înlocuit "varsta" cu "data_nasterii" const select = 'id_unic,agent,nume_complet,data_nasterii,' + 'data_client,' + 'e_mail_creat,judet_ci_munca,valoare_venit,' + 'status,dua,status_secundar,dva,dva_maxim,delay,raport_bc,status_bc,' + 'potential_refin,tipologie_client,observatii_aratate,cnp,telefon,e_mail'; const conds = []; if (cnp) conds.push('cnp.ilike.*' + encodeURIComponent(cnp) + '*'); if (tel) conds.push('telefon.ilike.*' + encodeURIComponent(tel) + '*'); let qs = 'select=' + encodeURIComponent(select) + '&or=(' + conds.join(',') + ')'; if (isUtil) qs += '&agent=eq.' + encodeURIComponent(me); const arr = supaGet_(SUPA_TABLE, qs); if (!arr || !arr.length) return { ok:true, found:false }; const r = arr[0]; const duaTxt = _dispDate_(r.dua, r.dua); const dvaTxt = _dispDate_(r.dva, r.dva); const dvmTxt = _dispDate_(r.dva_maxim, r.dva_maxim); const age = _ageFromBirthDate(r.data_nasterii); // păstrează EXACT ordinea din listClientiActivi (22 coloane) const rowOut = [ _s(r.nume_complet), // 0 NUME COMPLET _s(age), // 1 VARSTA (din data_nasterii) _s(r.e_mail_creat), // 2 _s(r.judet_ci_munca), // 3 _s(r.valoare_venit), // 4 _s(r.status), // 5 duaTxt, // 6 _s(r.status_secundar), // 7 dvaTxt, // 8 dvmTxt, // 9 _s(r.delay), // 10 _s(r.raport_bc), // 11 _s(r.status_bc), // 12 _s(r.potential_refin), // 13 _s(r.tipologie_client), // 14 _s(r.observatii_aratate), // 15 _s(r.cnp), // 16 _s(r.telefon), // 17 _s(r.id_unic), // 18 _s(r.agent), // 19 _s(r.e_mail), // 20 _dispDate_(r.data_client, r.data_client) ]; return { ok:true, found:true, row: rowOut }; } /** ====== LISTA CA: un singur rând, după ID UNIC (pt refresh fin) ====== **/ function clientiActivi_getRowById(token, idUnic){ const sess = _getSession(token); idUnic = String(idUnic||'').trim(); if (!idUnic) return { ok:false }; const isUtil = String(sess.role||'').toLowerCase() === 'utilizator'; const me = _s(sess.user).trim(); const rec = supaOneById_( String(idUnic||''), 'id_unic,agent,nume_complet,data_nasterii,' + 'data_client,' + 'e_mail_creat,judet_ci_munca,valoare_venit,' + 'status,dua,status_secundar,dva,dva_maxim,delay,raport_bc,status_bc,' + 'potential_refin,tipologie_client,observatii_aratate,cnp,telefon,e_mail' ); if (!rec) return { ok:false }; // 🔒 Utilizator simplu vede DOAR clienții lui if (isUtil){ const ag = _s(rec.agent).trim(); if (ag && ag !== me){ // pentru clientul ăsta nu are voie – lăsăm UI-ul să facă fallback (full reload cu filtre corecte) return { ok:false }; } } const duaTxt = _dispDate_(rec.dua, rec.dua); const dvaTxt = _dispDate_(rec.dva, rec.dva); const dvmTxt = _dispDate_(rec.dva_maxim, rec.dva_maxim); const age = _ageFromBirthDate(rec.data_nasterii); const rowOut = [ rec.nume_complet || '', age || '', rec.e_mail_creat || '', rec.judet_ci_munca || '', rec.valoare_venit || '', rec.status || '', duaTxt, rec.status_secundar || '', dvaTxt, dvmTxt, rec.delay || '', rec.raport_bc || '', rec.status_bc || '', rec.potential_refin || '', rec.tipologie_client || '', rec.observatii_aratate || '', rec.cnp || '', rec.telefon || '', rec.id_unic || '', rec.agent || '', rec.e_mail || '', _dispDate_(rec.data_client, rec.data_client) ]; return { ok:true, row: rowOut }; } /** ===== INCASĂRI – SAVE în "BD Incasari" (row 3+) ===== **/ const INCASARI_SHEET = 'BD Incasari'; const INC_HEADER_ROW = 2; // antetele sunt pe rândul 2 const INC_FIRST_ROW = 3; // datele încep de la rândul 3 /** * Returnează un obiect map "ANTET -> index col 1-based" pentru un sheet. */ function _headerMap_(sh, headerRow){ const lastCol = sh.getLastColumn(); const heads = sh.getRange(headerRow, 1, 1, lastCol).getDisplayValues()[0]; const map = {}; heads.forEach((h, i) => { const k = String(h||'').trim().toUpperCase(); if (k) map[k] = i+1; }); return map; } /** Caută primul rând liber pe o coloană (1-based), începând de la rowStart. */ function _firstEmptyRow_(sh, col, rowStart){ const last = Math.max(sh.getLastRow(), rowStart-1); if (last < rowStart) return rowStart; const rng = sh.getRange(rowStart, col, last-rowStart+1, 1).getDisplayValues(); for (let i=0;i { codHr, locatie } din "Useri si Parole" (coloanele USER, COD HR, SEDIUL). */ function _userExtras_(userCode){ const code = String(userCode || '').trim(); if (!code) return { codHr:'', locatie:'' }; try{ const qs = 'USER=eq.' + encodeURIComponent(code) + '&select=' + encodeURIComponent('"COD HR",SEDIU') + '&limit=1'; const rows = supaGet_(SUPA_USERS_TABLE, qs); if (!rows || !rows.length) return { codHr:'', locatie:'' }; const r = rows[0]; return { codHr: String(r['COD HR'] || '').trim(), locatie: String(r.SEDIU || '').trim() }; }catch(_){ return { codHr:'', locatie:'' }; } } /** Parse date din yyyy-MM-dd -> Date */ function _fromYMD_(s){ if (!s) return null; const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!m) return null; return new Date(+m[1], +m[2]-1, +m[3]); } /** ====== UI – Bănci finanțatoare (Surse Tehnice!A3:A50) ====== **/ function ui_getBanciFinantare(){ const cache = CacheService.getScriptCache(); const CK = 'opt:banci:v1'; try { const hit = cache.get(CK); if (hit) return { ok:true, banci: JSON.parse(hit) }; } catch(_){} const sh = SpreadsheetApp.getActive().getSheetByName('Surse Tehnice'); if (!sh) return { ok:true, banci: [] }; const vals = sh.getRange(3, 1, 48, 1).getValues() .map(r => _s(r[0])).filter(_nz); try { cache.put(CK, JSON.stringify(vals), 6*60*60); } catch(_){} return { ok:true, banci: vals }; } /** ===== LOG GESTIONARE → Supabase (bd_logs_actiuni) ===== */ function logGestionareToSupabase_(sess, idUnic, existing, gestPayload, result, errorMsg) { try { const tz = Session.getScriptTimeZone(); const stamp = new Date(); const tsText = Utilities.formatDate(stamp, tz, 'dd/MM/yyyy'); const logId = Utilities.formatDate(stamp, tz, 'yyyyMMddHHmmss') + '-' + Utilities.getUuid(); const g = gestPayload || {}; const row = { 'TIMESTAMP': tsText, 'USER': _s(sess && sess.user), 'ROLE': _s(sess && sess.role), 'ID UNIC': idUnic ? Number(idUnic) : null, 'NUME COMPLET': _s(existing && existing.nume_complet), 'CNP': _s(existing && existing.cnp), 'ACTIUNE': 'GESTIONARE_SAVE', 'STATUS_OLD': _s(existing && existing.status), 'STATUS_NEW': _s(g.status), 'STATUS_SEC_OLD': _s(existing && existing.status_secundar), 'STATUS_SEC_NEW': _s(g.statusSecundar), 'OBS_NOU': _s(g.observatiiNoi), 'DUA_NEW': _s(g.dua), 'DVA_NEW': _s(g.dva), 'RAPORT_BC_NEW': _s(g.raportBC), 'RESULT': result || '', 'ERROR_MSG': errorMsg || '', 'LOG_ID': logId }; // LOG_ID este PK în bd_logs_actiuni supaUpsert_(SUPA_LOGS_TABLE, [row], 'LOG_ID'); } catch (e) { // log-ul nu trebuie să blocheze salvarea principală console.error('Supabase log error:', e); } } function findMdcRowByCnp_(cnpDigits){ cnpDigits = String(cnpDigits||'').replace(/\D+/g,''); if (!cnpDigits) return -1; const sh = _sheet(CLIENTI_SHEET); const H = getHeaderMap_(); const cCnp = _col(H,'CNP'); if (cCnp < 0) return -1; const startRow = 4, lastRow = sh.getLastRow(); const vals = sh.getRange(startRow, cCnp+1, Math.max(0,lastRow-startRow+1), 1).getValues(); for (let i=0;i a.col - b.col); let written = 0, i = 0; while (i < updates.length){ // începe un segment const start = updates[i].col; let end = start; const seg = [updates[i].val]; i++; // extindem segmentul cât timp coloanele sunt consecutive while (i < updates.length && updates[i].col === end + 1){ end = updates[i].col; seg.push(updates[i].val); i++; } // scriem segmentul: o singură operație setValues (1 x N) sh.getRange(row, start, 1, seg.length).setValues([seg]); written += seg.length; } return written; } /** * SALVARE UNIFICATĂ – într-un singur apel: * - Date Generale (payload.dg) * - Date Personale (payload.ip) * - Date Financiare (payload.if) * - Gestionare + Log (payload.gest) * Hint de rând: rowHint (SHEET_ROW din editor); dacă lipsește, folosește index_get sau find. */ function editor_saveAllMDC(token, idUnic, _rowHint /* ignorat */, payload){ const sess = _getSession(token); idUnic = String(idUnic||'').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); // 1) Citește înregistrarea curentă pt. permisiuni + pentru log/append const existing = supaOneById_(idUnic, 'id_unic,agent,nume_complet,cnp,status,status_secundar,dua,dva,dva_maxim,raport_bc,istoric,observatii'); if (!existing) throw new Error('ID UNIC inexistent în bd_clienti_activi.'); // permisiuni minime: Utilizator poate edita doar clienții proprii const isUtil = String(sess.role||'').toLowerCase() === 'utilizator'; const me = _s(sess.user).trim(); if (isUtil && _s(existing.agent).trim() && _s(existing.agent).trim() !== me){ throw new Error('Nu aveți drepturi pentru acest client.'); } // 2) Construim obiectul de upsert pe baza payload-ului const up = _mapEditorToDb_(idUnic, payload); // 3) Anexăm ISTORIC + OBSERVAȚII dacă vin în payload.gest let newIstoric = null, newObs = null; if (payload && payload.gest){ const tz = Session.getScriptTimeZone(); const azi = Utilities.formatDate(new Date(), tz, 'dd/MM/yyyy'); const user = _s(sess.user); const st = _s(payload.gest.status).trim(); const s2 = _s(payload.gest.statusSecundar).trim(); if (st || s2){ const lineIst = `${azi} - ${user} - ${[st, s2].filter(Boolean).join(' / ')}`; const curIst = _s(existing.istoric); newIstoric = curIst ? (curIst + '\n' + lineIst) : lineIst; // ← corect up.istoric = newIstoric; } const obsNou = _s(payload.gest.observatiiNoi).trim(); if (obsNou){ const curObs = _s(existing.observatii); const lineObs = `${azi} - ${user} - ${obsNou}`; newObs = curObs ? (curObs + '\n' + lineObs) : lineObs; up.observatii = newObs; up.observatii_aratate = _lastNLines(newObs, 5); } } const s2 = payload && payload.gest ? String(payload.gest.statusSecundar||'').trim().toUpperCase() : ''; if (s2 === 'BC NEELIGIBIL'){ const rbc = String(payload.gest.raportBC||'').trim().toUpperCase(); if (rbc !== 'STANDARD' && rbc !== 'SCORERISE') throw new Error('Pentru „BC NEELIGIBIL” trebuie selectat RAPORT BC = STANDARD sau SCORERISE.'); } // 4) Upsert în Supabase supaUpsert_(SUPA_TABLE, [up], 'id_unic'); // 5) LOG în Supabase (bd_logs_actiuni) – nu mai scriem în Google Sheets if (payload && payload.gest){ logGestionareToSupabase_( sess, idUnic, existing, payload.gest, 'OK', '' ); } // 6) Notifică live update (rămâne mecanismul tău existent) try { ca_pub_push_(idUnic); } catch(_){} return { ok:true, row: 2, istoric: newIstoric, observatii: newObs }; } function pingCA(token){ try { _getSession(token); return { ok:true }; } catch(e){ return { ok:false, error: e && e.message ? e.message : String(e) }; } } /** * Returnează datele clientului pentru generare contracte. * 1) Încearcă din Supabase (bd_clienti_activi) * 2) Fallback pe MDC + _mdcFields(row) (pentru perioadă de tranziție) * * Returnează un obiect cu aceleași chei ca _mdcFields: * NUME_COMPLET, PRENUME, NUME, CNP, JUDET_CI, LOCALITATE, STRADA, NR, BLOC, SC, ETAJ, AP, * TELEFON, E_MAIL, E_MAIL_CREAT */ function getClientContractData_(idUnic){ idUnic = String(idUnic || '').trim(); if (!idUnic) return null; // 1) Încercăm din Supabase try { var rec = supaOneById_( idUnic, 'id_unic,nume_complet,prenume,nume_familie,cnp,' + 'judet,localitate,strada,nr_strada,bloc,scara,etaj,apart,' + 'telefon,e_mail,e_mail_creat' ); if (rec){ return { NUME_COMPLET: _s(rec.nume_complet), PRENUME: _s(rec.prenume), NUME: _s(rec.nume_familie), CNP: _s(rec.cnp), JUDET_CI: _s(rec.judet), LOCALITATE: _s(rec.localitate), STRADA: _s(rec.strada), NR: _s(rec.nr_strada), BLOC: _s(rec.bloc), SC: _s(rec.scara), ETAJ: _s(rec.etaj), AP: _s(rec.apart), TELEFON: _s(rec.telefon), E_MAIL: _s(rec.e_mail), E_MAIL_CREAT: _s(rec.e_mail_creat) }; } } catch(e){ // dacă Supabase e jos, nu aruncăm; încercăm fallback-ul } // 2) Fallback: același ID din MDC + _mdcFields (perioadă de tranziție) try { var row = findMdcRowById_(idUnic); if (row >= 2){ return _mdcFields(row); } } catch(e2){} return null; } function _lastNLines(text, n){ if (!text) return ''; const arr = String(text).split(/\r?\n/); return arr.slice(-n).join('\n'); } /** * Called from ClientEditor.html when user clicks "PROCESEAZĂ CLIENT". * 1) Citește info BC din Supabase (bd_interogari_bc) după CNP * 2) Scrie în foaia consilierului (A6..V9) * 3) Utilities.sleep(5000) * 4) Rulează logica existentă de preluare procesare (din foaie) și returnează structura pt. UI */ function editor_proceseazaClient(token, idUnic, cnp, meta) { // 0) verifică sesiunea var sess = _getSession(token); // normalizează parametrii meta = meta || {}; cnp = String(cnp || '').replace(/\D+/g, ''); if (!cnp) { throw new Error('CNP invalid – nu pot procesa clientul.'); } // === GUARD – valoare venit obligatorie (VENIT 1 + VENIT 2) === var v1Num = Number(meta.valoareVenit1 || 0); var v2Num = Number(meta.valoareVenit2 || 0); if ((v1Num + v2Num) <= 0) { throw new Error('Nu poți procesa clientul fără VALOARE VENIT completată în INFORMAȚII FINANCIARE.'); } // === GUARD – FICO obligatoriu (din tabul PROCESARE) === var ficoRaw = String(meta.fico || '').trim(); if (!ficoRaw) { throw new Error('Nu poți procesa clientul fără FICO completat în secțiunea PROCESARE.'); } // dacă vrei să te asiguri că e număr, poți face o verificare lejeră: var ficoNum = Number(ficoRaw.replace(',', '.')); if (!isFinite(ficoNum) || ficoNum <= 0) { throw new Error('Valoarea FICO introdusă este invalidă. Verifică și reîncearcă.'); } // === GUARD – TIP RAPORT BC obligatoriu (din Gestionare Client) === var raportBC = String(meta.raportBC || '').trim(); if (!raportBC) { throw new Error('Nu poți procesa clientul fără tip RAPORT BC completat în GESTIONARE CLIENT.'); } // 1) Citește (opțional) rândul din bd_interogari_bc var bcRow = null; /* // === CHECK BC OBLIGATORIU – DEZACTIVAT TEMPORAR === // Citește rândul din bd_interogari_bc bcRow = fetchInterogariBCByCnp_(cnp); if (!bcRow) { throw new Error('Clientul nu poate fi procesat: nu are incarcat raport BC!'); } // Verifică DATA INTEROGARII (maxim 30 de zile vechime) if (bcRow['DATA INTEROGARII']) { var d = new Date(bcRow['DATA INTEROGARII']); if (!isNaN(d.getTime())) { var today = new Date(); var diffMs = today.getTime() - d.getTime(); var diffDays = diffMs / (1000 * 60 * 60 * 24); if (diffDays > 30) { throw new Error('Clientul nu poate fi procesat: data interogarii mai mare de 30 de zile fata de data curenta!'); } } } */ // TEMPORAR: doar încercăm să citim BC, fără să mai blocăm procesarea dacă lipsește bcRow = fetchInterogariBCByCnp_(cnp); // 2) Scrie valorile din raport BC în foaia consilierului, dacă există BC if (bcRow) { writeInterogariBCToUserSheet_(sess.user, bcRow); } // 3) Scrie valori suplimentare în foaia consilierului // folosind EXCLUSIV valorile venite din UI (meta) try { var shUser = getEditorSheet_(sess.user); // parse valori venit 1 + venit 2 din UI (pot fi cu virgulă sau punct) function parseMoneyClient(v) { if (v == null) return 0; var s = String(v).replace(/\./g, '').replace(/\s+/g, '').replace(',', '.'); var n = Number(s); return isFinite(n) ? n : 0; } var v1 = parseMoneyClient(meta.valoareVenit1); var v2 = parseMoneyClient(meta.valoareVenit2); var totalVenit = v1 + v2; // A303 = VALOARE VENIT 1 + VALOARE VENIT 2 shUser.getRange('A303').setValue(totalVenit || ''); // 🔹 B303 = FICO (din PROCESARE) shUser.getRange('B303').setValue(ficoNum || ''); // C303 = RAPORT BC (din tabul GESTIONARE CLIENT din UI) shUser.getRange('C303').setValue(raportBC || ''); // D303 = E-MAIL CREAT (din INFORMAȚII PERSONALE din UI) shUser.getRange('D303').setValue(meta.emailCreat || ''); // A306 = COD UTILIZATOR (RMA, SIO, etc.) shUser.getRange('A306').setValue(sess.user || ''); // D306 = NUME COMPLET CLIENT (din INFORMAȚII PERSONALE) shUser.getRange('D306').setValue(meta.numeComplet || ''); // E306 = CNP CLIENT (din INFORMAȚII PERSONALE) shUser.getRange('E306').setValue(meta.cnpClient || cnp || ''); } catch (e) { console.error('editor_proceseazaClient – scriere A303/B303/C303/D303/A306/D306/E306 din UI:', e); } // 4) Pauză 5 secunde înainte de citirea zonei A303:K317 Utilities.sleep(5000); // 5) Citește procesarea din foaia consilierului (A303:K317) și întoarce PREVIEW-ul pentru UI return editor_getProcesarePreviewCore(token, idUnic); } /** * Citește procesarea din foaia consilierului (A306:K320) și o trimite către editor * pentru PREVIEW (fără să o salveze în Supabase). * * Return: * { * ok: true/false, * msg?: string, * hdr: { * consilier, * statusProcesare, * numeClient, * cnpClient, * infoClient, * dataActualizareIso, * dataActualizareRo * }, * banci: [ * { banca, suma, info, status, motiv }, ... * ] * } */ function editor_getProcesarePreviewCore(token, idUnic){ const sess = _getSession(token); const userCode = String(sess.user || '').trim(); if (!userCode) throw new Error('User lipsă.'); idUnic = String(idUnic || '').trim(); if (!idUnic) return { ok:false, msg:'ID UNIC lipsă pentru client.' }; const ss = SpreadsheetApp.getActive(); const sh = ss.getSheetByName(userCode); if (!sh) return { ok:false, msg:'Nu există foaia utilizatorului ' + userCode }; // zona A306:K320 (15 rânduri, 11 coloane) const START_ROW = 306; const ROWS = 15; const START_COL = 1; const COLS = 11; SpreadsheetApp.flush(); const data = sh.getRange(START_ROW, START_COL, ROWS, COLS).getValues(); const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const toIso = v => { if (v instanceof Date){ return Utilities.formatDate(v, tz, 'yyyy-MM-dd'); } const s = String(v || '').trim(); if (!s) return ''; const m = s.match(/^(\d{1,2})[./](\d{1,2})[./](\d{4})$/); if (m){ const d = ('0'+m[1]).slice(-2); const mo = ('0'+m[2]).slice(-2); const y = m[3]; return `${y}-${mo}-${d}`; } const d = new Date(s); return isNaN(d) ? '' : Utilities.formatDate(d, tz, 'yyyy-MM-dd'); }; const fmtRo = v => { if (v instanceof Date){ return Utilities.formatDate(v, tz, 'dd.MM.yyyy'); } const s = String(v || '').trim(); const m = s.match(/^(\d{1,2})[./](\d{1,2})[./](\d{4})$/); if (m){ return ('0'+m[1]).slice(-2) + '.' + ('0'+m[2]).slice(-2) + '.' + m[3]; } const d = new Date(s); return isNaN(d) ? '' : Utilities.formatDate(d, tz, 'dd.MM.yyyy'); }; let hdr = null; const banci = []; data.forEach(row => { const cons = String(row[0] || '').trim(); // A – CONSILIER const dtRaw = row[1]; // B – DATA ACTUALIZARE const stProc = String(row[2] || '').trim(); // C – STATUS PROCESARE const nume = String(row[3] || '').trim(); // D – NUME CLIENT const cnp = String(row[4] || '').replace(/\D+/g,''); // E – CNP const infoCl = String(row[5] || '').trim(); // F – INFO CLIENT const banca = String(row[6] || '').trim(); // G – BANCĂ const suma = row[7]; // H – SUMA ESTIMATIVĂ (luăm brut ce e în celulă) const info = String(row[8] || '').trim(); // I – INFORMAȚII / RECOMANDĂRI const stB = String(row[9] || '').trim(); // J – STATUS const motiv = String(row[10]|| '').trim(); // K – MOTIV STATUS const hasHdrCells = cons || dtRaw || stProc || nume || cnp || infoCl; if (!hdr && hasHdrCells){ hdr = { consilier: cons, statusProcesare: stProc, numeClient: nume, cnpClient: cnp, infoClient: infoCl, dataActualizareIso: toIso(dtRaw), dataActualizareRo: fmtRo(dtRaw) }; } const hasBankCells = banca || suma || info || stB || motiv; if (hasBankCells){ banci.push({ banca: banca, suma: (suma == null ? '' : String(suma)), info: info, status: stB, motiv: motiv }); } }); if (!hdr && !banci.length){ return { ok:false, msg:'Zona A306:K320 este goală pentru acest user.' }; } return { ok: true, hdr: hdr || {}, banci: banci }; } /** * Citește UN singur rând din bd_interogari_bc pentru un CNP, * preferând cel mai nou "DATA INTEROGARII". */ function fetchInterogariBCByCnp_(cnp) { // coloanele cu spații trebuie să fie între ghilimele în select var cols = [ '"INFORMATII BC"', '"ALTE INFO BC"', '"MATRICE BC"', '"CREDIT 1"','"CREDIT 2"','"CREDIT 3"','"CREDIT 4"','"CREDIT 5"', '"CREDIT 6"','"CREDIT 7"','"CREDIT 8"','"CREDIT 9"','"CREDIT 10"', '"CREDIT 11"','"CREDIT 12"','"CREDIT 13"','"CREDIT 14"','"CREDIT 15"', '"CREDIT 16"','"CREDIT 17"','"CREDIT 18"','"CREDIT 19"','"CREDIT 20"', '"CREDIT 21"','"CREDIT 22"','"CREDIT 23"','"CREDIT 24"','"CREDIT 25"', '"CREDIT CODEB. 1"','"CREDIT CODEB. 2"','"CREDIT CODEB. 3"','"CREDIT CODEB. 4"','"CREDIT CODEB. 5"', // AICI completezi exact cum ai denumit coloanele pentru întârziere: '"INTARZIERI 1"','"INTARZIERI 2"','"INTARZIERI 3"','"INTARZIERI 4"','"INTARZIERI 5"', '"INTARZIERI 6"','"INTARZIERI 7"','"INTARZIERI 8"','"INTARZIERI 9"','"INTARZIERI 10"', // și pentru interogări: '"INTEROGARI 1"','"INTEROGARI 2"','"INTEROGARI 3"','"INTEROGARI 4"','"INTEROGARI 5"', '"OFERTA"', '"ALTE INFO"', '"DATA INTEROGARII"' ]; var select = cols.join(','); var url = SUPA_URL + '/rest/v1/bd_interogari_bc' + '?select=' + encodeURIComponent(select) + '&' + encodeURIComponent('CNP CLIENT') + '=eq.' + encodeURIComponent(cnp) + '&order=' + encodeURIComponent('"DATA INTEROGARII".desc') + '&limit=1'; var opt = { method: 'get', headers: supaHeaders_(true), // ai deja supaHeaders_ în proiect muteHttpExceptions: true }; var resp = UrlFetchApp.fetch(url, opt); var code = resp.getResponseCode(); if (code < 200 || code >= 300) { throw new Error('Supabase bd_interogari_bc error ' + code + ': ' + resp.getContentText()); } var arr = JSON.parse(resp.getContentText() || '[]'); return arr.length ? arr[0] : null; } /** * Scrie valorile din row (bd_interogari_bc) în foaia consilierului: * - A6..AB6 (INFORMATII BC, ALTE INFO BC, MATRICE BC, CREDIT 1..25) * - A9..V9 (CREDITE CODEB + INTARZIERI + INTEROGARI + OFERTA + ALTE INFO) */ function writeInterogariBCToUserSheet_(userCode, row) { // folosim helperul deja existent, care deschide foaia var sh = getEditorSheet_(userCode); if (!sh){ throw new Error('Nu am putut determina foaia pentru userul: ' + userCode); } // --- rând 6: A..AB --- var row6 = new Array(28); // A..AB = 28 coloane row6[0] = row['INFORMATII BC'] || ''; row6[1] = row['ALTE INFO BC'] || ''; row6[2] = row['MATRICE BC'] || ''; for (var i = 1; i <= 25; i++){ var key = 'CREDIT ' + i; row6[2 + i] = row[key] || ''; } sh.getRange(6, 1, 1, row6.length).setValues([row6]); // --- rând 8: A..V (CREDITE CODEB, INTARZIERI, INTEROGARI, OFERTA, ALTE INFO) --- var row8 = new Array(22); // A..E: CREDIT CODEB. 1..5 for (var c = 1; c <= 5; c++){ row8[c-1] = row['CREDIT CODEB. ' + c] || ''; } // F..O: INTARZIERI 1..10 var intarzStartCol = 5; for (var j = 1; j <= 10; j++){ row8[intarzStartCol + j - 1] = row['INTARZIERI ' + j] || ''; } // P..T: INTEROGARI 1..5 var interStartCol = 15; for (var k = 1; k <= 5; k++){ row8[interStartCol + k - 1] = row['INTEROGARI ' + k] || ''; } // U: OFERTA row8[20] = row['OFERTA'] || ''; // V: ALTE INFO row8[21] = row['ALTE INFO'] || ''; sh.getRange(8, 1, 1, row8.length).setValues([row8]); } /** * EXPORTĂ PROCESAREA din foaia consilierului (A306:K320) în Supabase. * - citește header + liniile de bancă * - apelează procesari_saveSupabase(...) din CodeIndex.gs */ function procesari_exportFromSheet(token, idUnic){ const sess = _getSession(token); const userCode = String(sess.user || '').trim(); if (!userCode) throw new Error('User lipsă.'); idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă pentru client.'); const ss = SpreadsheetApp.getActive(); const sh = ss.getSheetByName(userCode); if (!sh) throw new Error('Nu există foaia utilizatorului ' + userCode); const START_ROW = 306; const ROWS = 15; const START_COL = 1; const COLS = 11; SpreadsheetApp.flush(); const data = sh.getRange(START_ROW, START_COL, ROWS, COLS).getValues(); const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const toIso = v => { if (v instanceof Date){ return Utilities.formatDate(v, tz, 'yyyy-MM-dd'); } const s = String(v || '').trim(); if (!s) return ''; let m = s.match(/^(\d{1,2})[./](\d{1,2})[./](\d{4})$/); if (m){ const d = ('0'+m[1]).slice(-2), mo = ('0'+m[2]).slice(-2), y = m[3]; return `${y}-${mo}-${d}`; } const d = new Date(s); return isNaN(d) ? '' : Utilities.formatDate(d, tz, 'yyyy-MM-dd'); }; const parseMoney = v => { if (v == null || v === '') return null; const n = Number(String(v).replace(/\./g,'').replace(/\s+/g,'').replace(',','.')); return isFinite(n) ? n : null; }; let hdr = null; const banci = []; data.forEach(row => { const cons = String(row[0] || '').trim(); const dtRaw = row[1]; const stProc = String(row[2] || '').trim(); const nume = String(row[3] || '').trim(); const cnp = String(row[4] || '').replace(/\D+/g,''); const infoCl = String(row[5] || '').trim(); const banca = String(row[6] || '').trim(); const suma = parseMoney(row[7]); const info = String(row[8] || '').trim(); const stB = String(row[9] || '').trim(); const motiv = String(row[10]|| '').trim(); const hasHdrCells = cons || dtRaw || stProc || nume || cnp || infoCl; if (!hdr && hasHdrCells){ hdr = { consilier: cons || userCode, dataActualizareIso: toIso(dtRaw) || toIso(new Date()), status: stProc || 'INITIATA', numeClient: nume, cnp: cnp, infoClient: infoCl }; } if (banca){ banci.push({ banca: banca, suma: suma, info: info, status: stB, motiv: motiv }); } }); if (!hdr) throw new Error('Nu am găsit rândul de header în A306:K320 (CONSILIER / NUME CLIENT / CNP etc.).'); if (!banci.length) throw new Error('Nu există nicio bancă completată în coloanele G:K (BANCĂ / SUMĂ / INFO / STATUS / MOTIV).'); // === GUARD: nu permitem două procesări ACTIVE pentru același CNP === var cnpClient = (hdr && hdr.cnp) ? String(hdr.cnp || '').replace(/\D+/g,'') : ''; if (procesari_hasActiveForCnp_(cnpClient)) { throw new Error( 'Există deja o procesare activă (INITIATA sau IN LUCRU) pentru acest CNP. ' + 'Închide sau actualizează procesarea existentă înainte de a trimite una nouă.' ); } const res = procesari_saveSupabase(token, idUnic, hdr, banci); // === Email automat către procesari@smart-credit.ro === try { if (res && res.ok) { const agentCode = (hdr && hdr.consilier) ? String(hdr.consilier || '').trim() : String(sess.user || '').trim(); const numeClient = (hdr && hdr.numeClient) ? String(hdr.numeClient || '').trim() : ''; const cnpClient = (hdr && hdr.cnp) ? String(hdr.cnp || '').trim() : ''; const subject = `Agentul ${agentCode} a initiat o procesare noua: ${numeClient}, CNP ${cnpClient}`; const body = 'Buna ziua,\n\n' + `Agentul ${agentCode} a initiat o procesare noua pentru clientul ${numeClient}, CNP ${cnpClient}.\n\n` + 'Acest mesaj a fost generat automat din CRM SALES.'; MailApp.sendEmail({ to: 'procesari@smart-credit.ro', subject: subject, body: body, noReply: true }); } } catch (e) { console.error('procesari_exportFromSheet – trimitere email:', e); } // 👇 AICI E FIX-UL return res || { ok:true, hdrId:null, banks:banci.length }; } /** Verifică dacă există o procesare ACTIVĂ (INITIATA / IN LUCRU) pentru un CNP. */ function procesari_hasActiveForCnp_(cnpRaw){ var cnp = String(cnpRaw || '').replace(/\D+/g,''); if (!cnp) return false; // luăm toate procesările pentru acest CNP (doar coloana status_procesare) var qs = 'cnp_client=eq.' + encodeURIComponent(cnp) + '&select=' + encodeURIComponent('id,status_procesare') + '&order=' + encodeURIComponent('id') + '.desc&limit=50'; var rows = []; try{ rows = supaGet_(SUPA_PROC_HDR, qs); }catch(e){ // dacă Supabase a dat fail, nu blocăm – mai bine lăsăm să meargă decât să aruncăm eroare falsă return false; } if (!rows || !rows.length) return false; return rows.some(function(r){ var s = String(r.status_procesare || '').trim().toUpperCase(); return s === 'INITIATA' || s === 'IN LUCRU'; }); } /** Map companie -> tabel CPS din Supabase */ function _cpsTableForCompany_(companie){ companie = String(companie || '').trim().toUpperCase(); if (companie === 'ALL FINANCE CENTER') return SUPA_CPS_AFC; if (companie === 'RESTART CREDITARE CF') return SUPA_CPS_RCCF; if (companie === 'TRANSILVANIA SMC') return SUPA_CPS_TSMC; return null; } /** Citește din tabelul CPS contractul după NR. CTR. + CNP CLIENT */ function _cps_findContract_(companie, nrContract, cnpDigits){ const table = _cpsTableForCompany_(companie); if (!table) throw new Error('Compania selectată nu este mapată la un tabel CPS.'); const sel = encodeURIComponent('"NR. CTR.","CNP CLIENT","SUMA FINALA","STATUS FINAL"'); const qNr = encodeURIComponent('"NR. CTR."') + '=eq.' + encodeURIComponent(String(nrContract)); const qCnp = encodeURIComponent('"CNP CLIENT"') + '=eq.' + encodeURIComponent(String(cnpDigits)); const qs = 'select=' + sel + '&' + qNr + '&' + qCnp + '&limit=1'; const rows = supaGet_(table, qs); return (rows && rows[0]) || null; } /** Marchează STATUS FINAL = INCASAT + DATA INCASARII + MOD INCASARE în tabela CPS */ function _cps_markIncasat_(companie, nrContract, dataIncasareIso, modIncasare){ const table = _cpsTableForCompany_(companie); if (!table) throw new Error('Compania selectată nu este mapată la un tabel CPS.'); // în tabelele CPS datele sunt text; scriem dd.MM.yyyy let dataText = ''; if (dataIncasareIso){ const d = _fromYMD_(dataIncasareIso); if (d) dataText = Utilities.formatDate(d, Session.getScriptTimeZone(), 'dd.MM.yyyy'); } const patch = { 'NR. CTR.': Number(nrContract), 'STATUS FINAL': 'INCASAT', 'DATA INCASARII': dataText || null, 'MOD INCASARE': String(modIncasare || '').trim() || null }; // PATCH direct, nu upsert (coloane cu spații) const url = SUPA_URL + '/rest/v1/' + table + '?' + encodeURIComponent('"NR. CTR."') + '=eq.' + encodeURIComponent(String(nrContract)); const res = UrlFetchApp.fetch(url, { method: 'patch', contentType: 'application/json', headers: supaHeaders_(false), payload: JSON.stringify(patch), muteHttpExceptions: true }); if (res.getResponseCode() >= 300){ throw new Error('Supabase UPDATE CPS: ' + res.getContentText()); } return true; } /** * Salvează o încasare în Supabase (bd_incasari). * * Pași: * 1) Validare câmpuri de bază * 2) Validare contract în tabelele CPS (bd_cps_afc / bd_cps_rccf / bd_cps_tsmc): * - NR. CTR. + CNP CLIENT * - COMM. BRUT = SUMA FINALA * - marchează STATUS FINAL = INCASAT * 3) INSERT în bd_incasari (Supabase) */ function incasari_save(token, payload){ const sess = _getSession(token); const user = (sess.user || '').trim(); if (!payload) throw new Error('Payload gol pentru încasare.'); // --- helpers locale --- function parseMoneyRO_(v){ if (v == null) return NaN; return Number(String(v).replace(/\./g,'').replace(/\s+/g,'').replace(',', '.')); } function _isBankChosen(v){ const s = String(v||'').trim().toLowerCase(); return s && s !== 'alege banca' && s !== 'alege banca…' && s !== 'alege banca...' && s !== '-'; } // === extrase pentru validare === const companie = String(payload.compania || '').trim(); const nrContract = String(payload.nrContract || '').trim(); const cnpDigits = String(payload.cnp || '').replace(/\D+/g,''); const comBrutRaw = String(payload.comBrut || '').trim(); const banca1 = String(payload.b1_denumire || '').trim(); const idUnic = payload.idUnic ? String(payload.idUnic).trim() : ''; if (!companie || !nrContract || !cnpDigits || !comBrutRaw){ throw new Error('Completează COMPANIA, NR. CONTRACT, CNP și COMISION BRUT.'); } if (!_isBankChosen(banca1)){ throw new Error('Selectează o valoare la „DENUMIRE BANCA 1”.'); } // === 1) Validări & marcaj în tabelele CPS (Supabase) === const cpsRec = _cps_findContract_(companie, nrContract, cnpDigits); if (!cpsRec){ throw new Error('Numărul de contract nu există pentru acest CNP în baza CPS.'); } // CNP e deja filtrat în query, dar verificăm totuși const cnpBD = String(cpsRec['CNP CLIENT'] || '').replace(/\D+/g,''); if (!cnpBD || cnpBD !== cnpDigits){ throw new Error('Numărul de contract introdus nu aparține acestui client (CNP nu corespunde).'); } // COMM. BRUT introdus vs SUMA FINALA din CPS const sumaFinala = Number(cpsRec['SUMA FINALA'] || 0); const comBrut = parseMoneyRO_(comBrutRaw); if (!Number.isFinite(sumaFinala) || !Number.isFinite(comBrut)){ throw new Error('Nu pot interpreta numeric suma brută sau SUMA FINALA din CPS.'); } if (Math.abs(comBrut - sumaFinala) > 0.01){ throw new Error('Suma brută introdusă nu corespunde cu SUMA FINALA din contract!'); } // marchează în CPS: STATUS FINAL = INCASAT, DATA INCASARII, MOD INCASARE _cps_markIncasat_(companie, nrContract, payload.dataIncasare, payload.incasatPrin); // === 2) Determinăm AGENTUL CLIENTULUI (din bd_clienti_activi) === let clientAgent = user; if (idUnic){ try{ const cli = supaOneById_(idUnic, 'agent'); if (cli && cli.agent){ const ag = String(cli.agent || '').trim(); if (ag) clientAgent = ag; } } catch(e){ // dacă Supabase dă fail, păstrăm user-ul logat } } // extrase pentru AGENTUL CLIENTULUI (cod_hr + locatie) const extras = _userExtras_(clientAgent); // { codHr, locatie } din bd_useri_si_parole // === 3) INSERT în bd_incasari (Supabase) === const row = { id_unic: idUnic ? Number(idUnic) : null, agent: clientAgent, // 👈 agentul clientului cod_hr: extras.codHr || null, // 👈 cod HR agent locatie: extras.locatie|| null, // 👈 locația agentului prenume_client: payload.prenume || null, nume_complet_client: payload.numeComplet || null, cnp: cnpDigits || null, canal: payload.canal || null, campanie_recomandator:payload.campanie || null, adset_tel_recomandator:payload.adset || null, ad: payload.ad || null, nr_contract: nrContract, compania: companie, comm_brut: comBrut, comm_net: parseMoneyRO_(payload.comNet), // data_incasare este DATE în Supabase → trimitem direct yyyy-MM-dd din input type="date" data_incasare: payload.dataIncasare || null, tip_incasare: payload.tipIncasare || null, incasat_prin: payload.incasatPrin || null, comm_rec: parseMoneyRO_(payload.comRec), recomandator: payload.recomandator || null, denumire_banca_1: payload.b1_denumire || null, suma_totala_banca_1: parseMoneyRO_(payload.b1_sumaTot), suma_suplim_banca_1: parseMoneyRO_(payload.b1_suplim), denumire_banca_2: _isBankChosen(payload.b2_denumire) ? payload.b2_denumire : null, suma_totala_banca_2: _isBankChosen(payload.b2_denumire) ? parseMoneyRO_(payload.b2_sumaTot) : null, suma_suplim_banca_2: _isBankChosen(payload.b2_denumire) ? parseMoneyRO_(payload.b2_suplim) : null, denumire_banca_3: _isBankChosen(payload.b3_denumire) ? payload.b3_denumire : null, suma_totala_banca_3: _isBankChosen(payload.b3_denumire) ? parseMoneyRO_(payload.b3_sumaTot) : null, suma_suplim_banca_3: _isBankChosen(payload.b3_denumire) ? parseMoneyRO_(payload.b3_suplim) : null, denumire_banca_4: _isBankChosen(payload.b4_denumire) ? payload.b4_denumire : null, suma_totala_banca_4: _isBankChosen(payload.b4_denumire) ? parseMoneyRO_(payload.b4_sumaTot) : null, suma_suplim_banca_4: _isBankChosen(payload.b4_denumire) ? parseMoneyRO_(payload.b4_suplim) : null }; // curățăm NaN → null [ 'comm_net','comm_rec', 'suma_totala_banca_1','suma_suplim_banca_1', 'suma_totala_banca_2','suma_suplim_banca_2', 'suma_totala_banca_3','suma_suplim_banca_3', 'suma_totala_banca_4','suma_suplim_banca_4' ].forEach(k => { if (row[k] != null && !Number.isFinite(row[k])) row[k] = null; }); const inserted = supaInsertOne_('bd_incasari', row); return { ok:true, id: inserted.id || null }; } function programari_listForGest(token, dataIso, daysCount, agentFilter) { // 1) Sesiune let sess; try { sess = _getSession(token); } catch (e) { return { ok:false, error: e && e.message ? e.message : String(e) }; } const userCode = String(sess.user || '').trim().toUpperCase(); if (!userCode) return { ok:false, error:'NO_USER' }; // combinatia magica: USER LOGAT -> AGENT din bd_programari const agent = String(agentFilter || userCode).trim().toUpperCase(); const tz = Session.getScriptTimeZone(); const base = dataIso ? new Date(dataIso + 'T00:00:00') : new Date(); const n = Math.max(1, Number(daysCount || 1)); // 2) Construim lista de zile (de regulă 1 zi – cea selectată în popup) const days = []; let d = new Date(base); for (let i = 0; i < n; i++) { const iso = Utilities.formatDate(d, tz, 'yyyy-MM-dd'); const lbl = Utilities.formatDate(d, tz, 'dd.MM.yyyy'); days.push({ iso, label: lbl }); d = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1); } if (!days.length) { return { ok:true, days:[], times:[], events:{} }; } const from = days[0].iso; const to = days[days.length - 1].iso; // 3) Citim programările STRICT pentru agentul curent din bd_programari const selectCols = 'agent,data_programare,ora_programare,' + 'nume_client,telefon_client,locatia,scop_programare,status'; const conds = [ 'data_programare=gte.' + encodeURIComponent(from), 'data_programare=lte.' + encodeURIComponent(to), 'agent=eq.' + encodeURIComponent(agent) ]; const qs = 'select=' + encodeURIComponent(selectCols) + '&' + conds.join('&') + '&order=' + encodeURIComponent('data_programare.asc') + '&order=' + encodeURIComponent('ora_programare.asc'); const rows = supaGet_(SUPA_PROGRAMARI, qs) || []; // 4) Construim structura { days, times, events } pentru UI-ul din popup const events = {}; const timesSet = new Set(); days.forEach(day => { events[day.iso] = {}; }); rows.forEach(r => { const iso = String(r.data_programare || '').trim(); const time = _timeToHHmm(r.ora_programare); // ai deja helperul ăsta if (!iso || !time || !events[iso]) return; const label = [ _s(r.nume_client), _s(r.telefon_client), _s(r.locatia), _s(r.scop_programare), _s(r.status) ].filter(Boolean).join(' / '); if (!events[iso][time]) events[iso][time] = []; events[iso][time].push(label); timesSet.add(time); }); const times = Array.from(timesSet).sort(); // ex: ["09:00","09:30",...] return { ok:true, days, times, events }; } /** ========================= * ADMIN — ȘTERGERE CLIENT DIN bd_clienti_activi + LOG în bd_logs_actiuni * - doar ROLE = Administrator * - log: ACTIUNE = CLIENT_DELETE (cu RESULT / ERROR_MSG) * ========================= */ function logClientDeleteToSupabase_(sess, existing, reason, result, errorMsg){ try { const tz = Session.getScriptTimeZone(); const stamp = new Date(); const tsText = Utilities.formatDate(stamp, tz, 'dd/MM/yyyy'); const logId = Utilities.formatDate(stamp, tz, 'yyyyMMddHHmmss') + '-' + Utilities.getUuid(); const obsTxt = [ String(reason || '').trim(), (existing && existing.agent) ? ('OLD_AGENT=' + String(existing.agent || '').trim()) : '' ].filter(Boolean).join(' | ') || null; const row = { 'TIMESTAMP': tsText, 'USER': _s(sess && sess.user), 'ROLE': _s(sess && sess.role), 'ID UNIC': (existing && existing.id_unic) ? Number(existing.id_unic) : null, 'NUME COMPLET': _s(existing && existing.nume_complet), 'CNP': _s(existing && existing.cnp), 'ACTIUNE': 'CLIENT_DELETE', 'STATUS_OLD': _s(existing && existing.status), 'STATUS_NEW': 'DELETED', 'STATUS_SEC_OLD': _s(existing && existing.status_secundar), 'STATUS_SEC_NEW': null, 'OBS_NOU': obsTxt, 'DUA_NEW': null, 'DVA_NEW': null, 'RAPORT_BC_NEW': null, 'RESULT': result || '', 'ERROR_MSG': errorMsg || '', 'LOG_ID': logId }; // LOG_ID este PK în bd_logs_actiuni supaUpsert_(SUPA_LOGS_TABLE, [row], 'LOG_ID'); return logId; } catch (e) { // log-ul nu trebuie să blocheze ștergerea try { Logger.log('logClientDeleteToSupabase_ error: ' + (e && e.message ? e.message : e)); } catch(_){} return null; } } /** * Șterge DEFINITIV clientul din bd_clienti_activi (Supabase) + log în bd_logs_actiuni. * @param {string} token * @param {string|number} idUnic * @param {string} reason – motiv ștergere (obligatoriu) */ function admin_deleteClientFromBD(token, idUnic, reason){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase(); if (role !== 'administrator'){ throw new Error('Acces interzis. Doar Administratorul poate șterge clienți.'); } idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); reason = String(reason || '').trim(); if (!reason) throw new Error('Motivul ștergerii este obligatoriu (pentru audit).'); // luăm datele înainte de delete (ca să le avem în log) const existing = supaOneById_(idUnic, 'id_unic,agent,nume_complet,cnp,status,status_secundar'); if (!existing){ return { ok:false, msg:'Client inexistent în bd_clienti_activi.' }; } let result = 'OK'; let errMsg = ''; try { const url = `${SUPA_URL}/rest/v1/${SUPA_TABLE}?id_unic=eq.${encodeURIComponent(idUnic)}`; const res = UrlFetchApp.fetch(url, { method: 'delete', headers: supaHeaders_(false), muteHttpExceptions: true }); if (res.getResponseCode() >= 300){ throw new Error(res.getContentText()); } } catch (e) { result = 'ERROR'; errMsg = (e && e.message) ? e.message : String(e); } // LOG (best-effort, dar îl facem mereu) logClientDeleteToSupabase_(sess, existing, reason, result, errMsg); if (result !== 'OK'){ throw new Error('Nu s-a putut șterge clientul: ' + errMsg); } // live update (best-effort) – să forțeze refresh în Index try { updates_emit(idUnic); } catch(_){} return { ok:true }; } /** * ===== FRONTEND (ClientEditor) – Creare adresă e-mail ===== * Construiește automat: prenume.numeCNP@smcmail.ro * Apelează createEmailViaCPanel_ (care folosește tokenul cPanel din ScriptProperties). */ function createEmailForClient(prenume, nume, cnp){ return createEmailForClient_(prenume, nume, cnp); } function createEmailForClient(){ var a=[].slice.call(arguments||[]); // recomandat: createEmailForClient(TOKEN, ID_UNIC, prenume, nume, cnp) if(a.length>=5) return createEmailForClient_(a[0],a[1],a[2],a[3],a[4]); // compatibil vechi: createEmailForClient(prenume, nume, cnp) return createEmailForClient_(null,null,a[0],a[1],a[2]); } function createEmailForClient_(token,idUnic,prenume,nume,cnp){ prenume=String(prenume||'').trim(); nume=String(nume||'').trim(); var cnpDig=String(cnp||'').replace(/\D+/g,'').trim(); if(!prenume||!nume||!cnpDig) return{ok:false,statusMessage:'Completează prenume, nume și CNP.'}; var p=prenume.split(' ')[0].toLowerCase().normalize('NFD').replace(/[^a-z]/g,''); var n=nume.split(' ')[0].toLowerCase().normalize('NFD').replace(/[^a-z]/g,''); var emailFull=`${p}.${n}${cnpDig}@smcmail.ro`; var r=createEmailViaCPanel_(emailFull); // normalizează răspuns (ca UI să aibă mereu adresa) if(r&&r.ok&&!r.createdEmail) r.createdEmail=emailFull; // log în bd_adrese_email DOAR dacă s-a creat ACUM if(r&&r.ok&&r.created){ try{ _adreseEmail_logIfMissing_(token,idUnic,prenume,nume,cnpDig,emailFull); } catch(e){ Logger.log('bd_adrese_email log fail: '+(e&&e.message?e.message:e)); } } return r; } function _adreseEmail_logIfMissing_(token,idUnic,prenume,nume,cnpDig,email){ cnpDig=String(cnpDig||'').replace(/\D+/g,''); email=String(email||'').trim(); if(!email) return {ok:false,msg:'E-mail lipsă.'}; // cine a creat (creat_de / user_status) var creatDe=null; if(token){ try{ var sess=_getSession(token); creatDe=String(sess&&sess.user||'').trim()||null; }catch(_){ } } var who=creatDe||'SISTEM'; // există deja email-ul? var hit=supaGet_(SUPA_ADRESE_EMAIL,'adresa_email=eq.'+encodeURIComponent(email)+'&select=nr_crt&limit=1'); if(hit&&hit.length) return {ok:true,skipped:true}; // metadata: întâi după id_unic (activi), apoi după CNP (activi -> pierduți) var rec=null; try{ if(idUnic) rec=supaOneById_(String(idUnic||'').trim(),'cnp,nume_complet,agent,status_secundar,raport_bc',SUPA_TABLE); }catch(_){ rec=null; } if(!rec && cnpDig){ var sel=encodeURIComponent('cnp,nume_complet,agent,status_secundar,raport_bc'); try{ var a=supaGet_(SUPA_TABLE,'cnp=eq.'+encodeURIComponent(cnpDig)+'&select='+sel+'&limit=1'); rec=a&&a[0]||null; }catch(_){ rec=null; } if(!rec){ try{ var b=supaGet_(SUPA_TABLE_PIERDUTI,'cnp=eq.'+encodeURIComponent(cnpDig)+'&select='+sel+'&limit=1'); rec=b&&b[0]||null; }catch(_){ rec=null; } } } var tz=Session.getScriptTimeZone()||'Europe/Bucharest'; var d=Utilities.formatDate(new Date(),tz,'yyyy-MM-dd'); // INSERT var row={ cnp_client:(rec&&rec.cnp?String(rec.cnp):cnpDig).replace(/\D+/g,'')||null, nume_client:rec&&rec.nume_complet?String(rec.nume_complet).trim():([prenume,nume].filter(Boolean).join(' ').trim()||null), agent:rec&&rec.agent?String(rec.agent).trim():null, adresa_email:email, status_client:rec&&rec.status_secundar?String(rec.status_secundar).trim():null, // ✅ cerința ta: status_interogare:'NEDEFINITA', status_curent:'ACTIVA', data_status:d, user_status:who, data_creare:d, creat_de:who }; try{ supaInsertOne_(SUPA_ADRESE_EMAIL,row); }catch(e){ var m=String((e&&e.message)||e||''); if(/duplicate|already exists|unique/i.test(m)) return {ok:true,skipped:true}; throw e; } return {ok:true}; } /** Calculează max(id_unic) numeric din bd_clienti_activi – punct de plecare pentru ID_COUNTER. */ function _computeMaxIdFromSupabase_(){ // folosim ORDER BY id_unic DESC LIMIT 1 – mult mai eficient decât selectAll const qs = 'select=' + encodeURIComponent('id_unic') + '&order=id_unic.desc&limit=1'; let rows = []; try{ rows = supaGet_(SUPA_TABLE, qs); } catch(e){ rows = []; } const last = rows && rows[0] ? Number(rows[0].id_unic) : 0; return (isFinite(last) && last > 0) ? last : 0; } /** ===== RAPORT ZILNIC – Clienți în DELAY (per agent) ===== **/ //const DELAY_MIN_THRESHOLD = 2; //const DELAY_MANAGERS_CC = [ //'robert.marcu@smart-credit.ro', //'alexandra.tacea@smart-credit.ro', // 'iuliana.sanduc@smart-credit.ro' //]; //function delayReport_collectByAgent_(minDelay){ // const sh = _sheet(CLIENTI_SHEET); // const H = getHeaderMap_(); //const startRow = 4, lastRow = sh.getLastRow(); // if (lastRow < startRow) return {}; // const data = sh.getRange(startRow, 1, lastRow - startRow + 1, sh.getLastColumn()).getValues(); //function parseDelay(raw){ // if (raw == null || raw === '') return 0; // const s = String(raw).replace(',', '.'); // const m = s.match(/(\d+(?:\.\d+)?)/); // prinde și "≥5", ">=5", "5+" // return m ? Number(m[1]) : 0; // } // const byAgent = {}; //data.forEach(r => { // const agent = _s(_val(r,H,'AGENT')).trim(); // if (!agent) return; // const d = parseDelay(_val(r,H,'DELAY')); // if (d < minDelay) return; //if (!byAgent[agent]) byAgent[agent] = []; //byAgent[agent].push({ // agent, // nume: _s(_val(r,H,'NUME COMPLET')), // status: _s(_val(r,H,'STATUS')), // dua: _s(_val(r,H,'DUA')), // status2: _s(_val(r,H,'STATUS SECUNDAR')), // dva: _s(_val(r,H,'DVA')), // dvaMax: _s(_val(r,H,'DVA MAXIM')), // obs: _s(_val(r,H,'OBSERVATII ARATATE')) // }); //}); //return byAgent; //} //function delayReport_getUserEmailMap_(){ //const sh = _sheet(USERS_SHEET); //const last = sh.getLastRow(); //const map = {}; //if (last < 2) return map; //const Hrow = sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0] || []; //const H = {}; Hrow.forEach((h,i)=>{ const k=String(h||'').trim().toUpperCase(); if(k) H[k]=i; }); //const cUser = H['USER']; //const cEmail = (H['E-MAIL']!=null ? H['E-MAIL'] : (H['EMAIL']!=null ? H['EMAIL'] : null)); //if (cUser == null || cEmail == null) return map; //const rows = sh.getRange(2,1,last-1,sh.getLastColumn()).getValues(); //rows.forEach(r => { // const u = _s(r[cUser]).trim(); // const e = _s(r[cEmail]).trim(); // if (u && e) map[u] = e; // }); // return map; //} //function _esc_(s){ return String(s==null?'':s).replace(/[&<>"]/g, m => ({'&':'&','<':'<','>':'>','"':'"'}[m])); } //function delayReport_buildHtml_(rows, dateLabel, agent){ // const css = [ //'table{border-collapse:collapse;width:100%;font:10px/1.35 Arial,sans-serif}', //'th,td{border:1px solid #dbe3f3;padding:4px 6px;vertical-align:top;white-space:pre-wrap;word-break:break-word}', //'thead th{background:#26428b;color:#fff;text-align:center;font-weight:700;font-size:12px}', //'tbody tr:nth-child(odd){background:#f8fbff}', // '.h{font:14px Arial,sans-serif;color:#26428b;margin:0 0 8px 0;font-weight:700}' //].join(''); //const head = 'AGENTNUME CLIENTSTATUSDUASTATUS SECUNDARDVADVA MAXIMOBSERVAȚII '; // const body = rows.map(r => // '' //+ ''+_esc_(r.agent) +'' //+ ''+_esc_(r.nume) +'' // + ''+_esc_(r.status) +'' // + ''+_esc_(r.dua) +'' // + ''+_esc_(r.status2)+'' // + ''+_esc_(r.dva) +'' // + ''+_esc_(r.dvaMax) +'' // + ''+_esc_(r.obs) +'' // + '' //).join(''); //return '' // + '
Clienți în DELAY (≥ '+DELAY_MIN_THRESHOLD+') – '+_esc_(agent)+' — '+_esc_(dateLabel)+'
' // + ''+head+''+body+'
'; //} /** Trimite emailuri per agent (apel manual sau din UI). */ //function delayReport_sendPerAgent(token){ // if (token) _getSession(token); //const tz = SpreadsheetApp.getActive().getSpreadsheetTimeZone() || Session.getScriptTimeZone() || 'Europe/Bucharest'; //const dateLabel = Utilities.formatDate(new Date(), tz, 'dd.MM.yyyy'); //const byAgent = delayReport_collectByAgent_(DELAY_MIN_THRESHOLD); //const emailMap = delayReport_getUserEmailMap_(); //const cc = DELAY_MANAGERS_CC.join(','); //let sent=0, skipped=0; //const agents = Object.keys(byAgent); //for (var i = 0; i < agents.length; i++){ // var agent = agents[i]; //const rows = byAgent[agent]; //if (!rows || !rows.length) continue; //const to = emailMap[agent] || ''; //const html = delayReport_buildHtml_(rows, dateLabel, agent); //const subject = `[CRM SALES] Clienți în DELAY (≥${DELAY_MIN_THRESHOLD}) – ${agent} – ${dateLabel} (${rows.length})`; //const plain = `Lista clienților în delay pentru agentul ${agent}: ${rows.length} înregistrări – ${dateLabel}. (Deschide emailul în format ////HTML pentru tabel.)`; //try{ //if (to){ // MailApp.sendEmail({ to, cc, subject, htmlBody: html, body: plain, noReply: true }); //} else { // MailApp.sendEmail({ to: cc, subject: subject + ' [FĂRĂ EMAIL AGENT]', htmlBody: html, body: plain, noReply: true }); // } // sent++; //} catch(e){ skipped++; } // ⇩ spațiere între mailuri (5 secunde) //if (i < agents.length - 1) Utilities.sleep(5000); //} //return { ok:true, agents: agents.length, sent, skipped }; //} //** Creează/înlocuiește triggerul zilnic 08:30 pentru raportul per agent. Rulează o singură dată. */ //function delayReport_setupDailyTrigger(){ //ScriptApp.getProjectTriggers().forEach(t => { //if (t.getHandlerFunction && t.getHandlerFunction() === 'delayReport_sendPerAgent_trigger'){ // ScriptApp.deleteTrigger(t); //} //}); //ScriptApp.newTrigger('delayReport_sendPerAgent_trigger') //.timeBased().atHour(8).nearMinute(30).everyDays(1).create(); //} /** Handlerul rulat de triggerul zilnic. */ //function delayReport_sendPerAgent_trigger(){ //const dow = (new Date()).getDay(); // 0=Sun, 6=Sat //if (dow === 0 || dow === 6) return; // ieși în weekend //delayReport_sendPerAgent(null); //} /** ===== AGENȚI ACTIVI (CONSILIER) pt. Upload ===== */ function calls_getActiveAgents(){ const sh = _sheet(USERS_SHEET); const last = sh.getLastRow(); if (last < 2) return { ok:true, agenti: [] }; const Hrow = sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0] || []; const H = {}; Hrow.forEach((h,i)=>{ const k=String(h||'').trim().toUpperCase(); if(k) H[k]=i; }); const cUser = H['USER']; const cTip = H['TIP ANGAJAT']; const cHr = H['STATUS HR']; if (cUser==null || cTip==null || cHr==null) return { ok:true, agenti: [] }; const vals = sh.getRange(2,1,last-1,sh.getLastColumn()).getValues(); const out = []; vals.forEach(r=>{ const tip = String(r[cTip]||'').trim().toUpperCase(); const hr = String(r[cHr] ||'').trim().toUpperCase(); if (tip==='CONSILIER' && hr==='ACTIV'){ const code = String(r[cUser]||'').trim(); if (code) out.push(code); } }); out.sort((a,b)=> a.localeCompare(b,'ro')); return { ok:true, agenti: out }; } /** ====== IMPORT CSV APELURI → BD Apeluri&Mesaje (G:N, de la rândul 3) ====== **/ const CALLS_BD_SHEET = 'BD Apeluri&Mesaje'; // foaia ta existentă function _ensureCallsBD_(){ const ss = SpreadsheetApp.getActive(); const sh = ss.getSheetByName(CALLS_BD_SHEET); if (!sh) throw new Error('Nu există foaia "BD Apeluri&Mesaje".'); return sh; } // găsește primul rând liber în coloana G, dar minim 3 function _callsNextRow_(sh){ const last = sh.getLastRow(); if (last < 3) return 3; const colG = sh.getRange(3, 7, Math.max(0, last-2), 1).getValues(); // G3..G(last) let idx = colG.length - 1; while (idx >= 0 && String(colG[idx][0]||'').trim() === '') idx--; return 3 + idx + 1; // următorul după ultimul ne-gol } function _parseDurationToSec_(s){ s = String(s||'').trim(); if (!s || s.indexOf(':') < 0) return 0; const p = s.split(':').map(Number); if (p.length === 2) return p[0]*60 + p[1]; // MM:SS if (p.length === 3) return p[0]*3600 + p[1]*60 + p[2]; // HH:MM:SS return 0; } function _parseDMYhm_(s){ // CSV: "dd/MM/yyyy HH:mm" try{ const [dd, mm, rest] = String(s||'').split('/'); const yyyy = rest.slice(0,4), hm = rest.slice(5); const [HH, MM] = hm.split(':').map(Number); return new Date(Number(yyyy), Number(mm)-1, Number(dd), HH, MM, 0); }catch(e){ return new Date(s); } } function _mapDirection_(dir){ dir = String(dir||'').trim().toLowerCase(); if (dir === 'incoming') return 'INTRARE'; if (dir === 'outgoing') return 'IESIRE'; return dir.toUpperCase(); } function _mapStatus_(st){ st = String(st||'').trim().toLowerCase(); if (st === 'answered') return 'A RASPUNS'; if (st === 'unanswered') return 'NU A RASPUNS'; // alte statusuri (Missed/Rejected) le grupăm tot la "NU A RASPUNS" return 'NU A RASPUNS'; } /** * Importă un CSV de apeluri în BD Apeluri&Mesaje (G:N). * @param {string} token – token login (validare) * @param {string} agentCode – codul agentului selectat (merge în col. G = AGENT) * @param {object} fd – { data: "data:;base64,...", filename: "fisier.csv" } */ function calls_importCsv(token, agentCode, fd){ _getSession(token); if (!fd || !fd.data || !fd.filename) throw new Error('Fișier lipsă.'); const base64 = String(fd.data).split(',').pop(); const csvStr = Utilities.newBlob(Utilities.base64Decode(base64)).getDataAsString('UTF-8'); // Așteptăm antete: Name, Address, Direction, Status, Duration, Date const rows = Utilities.parseCsv(csvStr, ','); if (!rows || rows.length < 2) return { ok:true, imported:0 }; const H = rows[0].map(h => String(h||'').trim().toUpperCase()); const idx = (k)=> H.indexOf(k); const cName=idx('NAME'), cAddr=idx('ADDRESS'), cDir=idx('DIRECTION'), cSta =idx('STATUS'), cDur=idx('DURATION'), cDat=idx('DATE'); if ([cName,cAddr,cDir,cSta,cDur,cDat].some(i => i < 0)) throw new Error('CSV aşteptat cu antete: Name, Address, Direction, Status, Duration, Date'); const sh = _ensureCallsBD_(); const startRow = _callsNextRow_(sh); // primul rând liber în G // helper local: normalizează orice formă la "7xxxxxxxx" function _phoneToLocal7_(raw){ let d = String(raw||'').replace(/\D+/g,''); // doar cifre if (!d) return ''; if (d.startsWith('00')) d = d.slice(2); // 00… → internațional fără 00 if (d.startsWith('40')) d = d.slice(2); // 40… → local if (d.startsWith('0')) d = d.slice(1); // 0… → local fără 0 // dacă întregul e exact 7xxxxxxxx, păstrăm; altfel căutăm un segment valid if (/^7\d{8}$/.test(d)) return d; const m = d.match(/7\d{8}/); return m ? m[0] : ''; } const out = []; for (let i=1; i maxRows){ sh.insertRowsAfter(maxRows, neededEnd - maxRows); } // scrie în G:N de la startRow sh.getRange(startRow, 7, out.length, 8).setValues(out); // formate pentru DATA (L) și ORA (M) sh.getRange(startRow, 12, out.length, 1).setNumberFormat('dd/MM/yyyy'); // L sh.getRange(startRow, 13, out.length, 1).setNumberFormat('HH:mm'); // M } return { ok:true, imported: out.length }; } /** ====== MANAGEMENT / USERI (Supabase) ====== **/ const SUPA_USERS_TABLE = 'bd_useri_si_parole'; /** === LISTARE === **/ function mgmt_users_list(token) { const sess = _getSession(token); if (String(sess.role || '').toLowerCase() !== 'administrator') { return { ok: false, msg: 'Doar Administratorul poate accesa Useri.' }; } const qs = 'select=*'; const rows = supaGet_(SUPA_USERS_TABLE, qs); const out = rows.map(r => ({ user: r.USER || '', parola: r.PAROLA || '', tipUser: r['TIP USER'] || '', nume: r['NUME COMPLET USER'] || '', adresa: r.ADRESA || '', telefon: r.TELEFON || '', email: r['E-MAIL'] || '', tipAngajat: r['TIP ANGAJAT'] || '', departament: r.DEPARTAMENT || '', statusZilnic: r['STATUS ZILNIC'] || '', statusHR: r['STATUS HR'] || '', codHR: r['COD HR'] || '', sediul: r.SEDIUL || '' })); // sortare alfabetică după user out.sort((a, b) => a.user.localeCompare(b.user, 'ro')); return { ok: true, rows: out }; } function _usrKey_(u){return 'usrrev:'+String(u||'').trim().toUpperCase();} function _userRev_get_(u){ u=String(u||'').trim(); if(!u) return 0; const c=CacheService.getScriptCache(),k=_usrKey_(u),h=c.get(k); if(h!=null) return Number(h)||0; const qs='select=session_rev&USER=eq.'+encodeURIComponent(u)+'&limit=1'; const a=supaGet_(SUPA_USERS_TABLE,qs)||[]; const v=Number(a[0]&&a[0].session_rev)||0; try{c.put(k,String(v),60);}catch(_){} return v; } function _userRev_set_(u,v){ u=String(u||'').trim(); if(!u) return; try{CacheService.getScriptCache().put(_usrKey_(u),String(Number(v)||0),3600);}catch(_){} } function mgmt_users_kick(token,userCode){ const sess=_getSession(token); if(String(sess.role||'').toLowerCase()!=='administrator') return{ok:false,msg:'Doar Administratorul poate deloga useri.'}; const u=String(userCode||'').trim(); if(!u) return{ok:false,msg:'USER invalid.'}; const cur=_userRev_get_(u),next=cur+1; const url=`${SUPA_URL}/rest/v1/${SUPA_USERS_TABLE}?USER=eq.${encodeURIComponent(u)}`; const r=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify({session_rev:next}),muteHttpExceptions:true}); if(r.getResponseCode()>=300) throw new Error('Supabase UPDATE session_rev: '+r.getContentText()); _userRev_set_(u,next); return{ok:true,user:u,rev:next}; } /** === VALIDĂRI COMUNE === **/ function _mgmtNormPhone07(raw) { const d = String(raw || '').replace(/\D+/g, ''); if (!/^07\d{8}$/.test(d)) return ''; return d.replace(/^(\d{4})(\d{3})(\d{3})$/, '$1.$2.$3'); } function _validateUserData(p) { const user = String(p.user || '').trim(); const pass = String(p.pass || '').trim(); const tipU = String(p.tipUser || '').trim(); if (!user || !pass || !tipU) throw new Error('Completează USER, PAROLA și TIP USER.'); const tel = _mgmtNormPhone07(p.telefon || ''); if (!/^07\d{2}\.\d{3}\.\d{3}$/.test(tel)) throw new Error('Telefon invalid. Format 07XX.XXX.XXX'); const email = String(p.email || '').trim(); if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw new Error('E-mail invalid.'); return { user, pass, tipU, tel, email }; } /** === ADĂUGARE USER === **/ function mgmt_users_add(token, p) { const sess = _getSession(token); if (String(sess.role || '').toLowerCase() !== 'administrator') return { ok:false, msg:'Doar Administratorul poate adăuga useri.' }; const { user, pass, tipU, tel, email } = _validateUserData(p); const existing = supaGet_(SUPA_USERS_TABLE, 'USER=eq.' + encodeURIComponent(user)); if (existing && existing.length) return { ok:false, msg:'USER deja existent.' }; const row = { USER: user, PAROLA: pass, 'TIP USER': tipU, 'NUME COMPLET USER': String(p.nume || ''), ADRESA: String(p.adresa || ''), TELEFON: tel, 'E-MAIL': email, 'TIP ANGAJAT': String(p.tipAngajat || ''), DEPARTAMENT: String(p.departament || ''), SEDIUL: String(p.sediul || ''), // ✅ AICI 'STATUS ZILNIC': 'ACTIV', 'STATUS HR': 'ACTIV', 'COD HR': '' }; supaUpsert_(SUPA_USERS_TABLE, [row], 'USER'); const ss = SpreadsheetApp.getActive(); const tpl = ss.getSheetByName('RMA'); if (!tpl) return { ok:false, msg:'Nu găsesc foaia șablon „RMA”.' }; if (ss.getSheetByName(user)) return { ok:false, msg:'Există deja o foaie cu acest nume.' }; const dup = tpl.copyTo(ss); dup.setName(user); ss.setActiveSheet(dup); return { ok:true }; } /** === ȘTERGERE USER === **/ function mgmt_users_delete(token, userCode) { const sess = _getSession(token); if (String(sess.role || '').toLowerCase() !== 'administrator') { return { ok: false, msg: 'Doar Administratorul poate șterge useri.' }; } const user = String(userCode || '').trim(); if (!user) return { ok: false, msg: 'USER invalid.' }; // ștergere în Supabase const url = `${SUPA_URL}/rest/v1/${SUPA_USERS_TABLE}?USER=eq.${encodeURIComponent(user)}`; const res = UrlFetchApp.fetch(url, { method: 'delete', headers: supaHeaders_(), muteHttpExceptions: true }); if (res.getResponseCode() >= 300) { throw new Error('Supabase DELETE: ' + res.getContentText()); } // șterge foaia dedicată, dacă există const ss = SpreadsheetApp.getActive(); const sheet = ss.getSheetByName(user); if (sheet) ss.deleteSheet(sheet); return { ok: true }; } /** === ACTUALIZARE USER === **/ function mgmt_users_update(token, oldUser, p) { const sess = _getSession(token); if (String(sess.role || '').toLowerCase() !== 'administrator') return { ok:false, msg:'Doar Administratorul poate actualiza useri.' }; const oldKey = String(oldUser || '').trim(); if (!oldKey) return { ok:false, msg:'USER lipsă.' }; const patch = {}; let newKey = oldKey; if (p.hasOwnProperty('user')) { newKey = String(p.user || '').trim(); if (!newKey) return { ok:false, msg:'USER nu poate fi gol.' }; if (newKey.toUpperCase() !== oldKey.toUpperCase()) { const existing = supaGet_(SUPA_USERS_TABLE, 'USER=eq.' + encodeURIComponent(newKey)); if (existing && existing.length) return { ok:false, msg:'USER deja existent.' }; } patch.USER = newKey; } else patch.USER = oldKey; if (p.hasOwnProperty('pass')) patch.PAROLA = String(p.pass || ''); if (p.hasOwnProperty('tipUser')) patch['TIP USER'] = String(p.tipUser || ''); if (p.hasOwnProperty('nume')) patch['NUME COMPLET USER'] = String(p.nume || ''); if (p.hasOwnProperty('adresa')) patch.ADRESA = String(p.adresa || ''); if (p.hasOwnProperty('telefon')) { const tel = _mgmtNormPhone07(p.telefon || ''); if (!/^07\d{2}\.\d{3}\.\d{3}$/.test(tel)) return { ok:false, msg:'Telefon invalid. Format 07XX.XXX.XXX' }; patch.TELEFON = tel; } if (p.hasOwnProperty('email')) { const email = String(p.email || '').trim(); if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return { ok:false, msg:'E-mail invalid.' }; patch['E-MAIL'] = email; } if (p.hasOwnProperty('tipAngajat')) patch['TIP ANGAJAT'] = String(p.tipAngajat || ''); if (p.hasOwnProperty('departament')) patch.DEPARTAMENT = String(p.departament || ''); if (p.hasOwnProperty('sediul')) patch.SEDIUL = String(p.sediul || ''); // ✅ AICI supaUpsert_(SUPA_USERS_TABLE, [patch], 'USER'); if (newKey !== oldKey) { const ss = SpreadsheetApp.getActive(); const sh = ss.getSheetByName(oldKey); if (sh) sh.setName(newKey); } return { ok:true, userNew:newKey }; } /** ====== Rapoarte Sales – TARGET (A6:J6) ====== **/ const RAPOARTE_SALES_SHEET = 'Rapoarte Sales'; /** * Scrie în B3/D3 prima și ultima zi a lunii selectate, * așteaptă recalculele și returnează A6:J6 ca array de 10 valori * exact în ordinea coloanelor din UI. */ function sr_target_updateAndFetch(token, month, year){ _getSession(token); const m = Number(month), y = Number(year); if (!(m >= 1 && m <= 12) || !Number.isFinite(y)) { return { ok:false, msg:'Parametri lună/an invalizi.' }; } const sh = _sheet(RAPOARTE_SALES_SHEET); const lock = LockService.getDocumentLock(); if (!lock.tryLock(5000)) { return { ok:false, busy:true }; } let wroteDates = false, cleared = false; try { // 1) Scrie capetele de interval (luna selectată) const first = new Date(y, m - 1, 1); // prima zi calendaristică a lunii const last = new Date(y, m, 0); // ultima zi calendaristică a lunii sh.getRange('B3').setValue(first).setNumberFormat('dd/MM/yyyy'); sh.getRange('D3').setValue(last). setNumberFormat('dd/MM/yyyy'); SpreadsheetApp.flush(); wroteDates = true; // 2) Poll scurt până se calculează formulele din foaie const OUT_RANGE = 'A6:J6'; // exact cele 10 coloane din UI let vals = []; let ready = false; const t0 = Date.now(); const MAX_MS = 10000; // ~10s timeout while ((Date.now() - t0) < MAX_MS) { vals = sh.getRange(OUT_RANGE).getDisplayValues()[0] || []; // "gata" când cel puțin o celulă are conținut (nu doar spații) ready = vals.some(v => String(v || '').trim() !== ''); if (ready) break; Utilities.sleep(400); } // 3) Curăță B3/D3 (nu vrem să rămână setate pentru alt user) sh.getRangeList(['B3','D3']).clearContent(); SpreadsheetApp.flush(); cleared = true; // 4) Întoarce rândul întreg (A6:J6) ca "row" return { ok:true, ready, row: (ready ? vals : []), cleared }; } catch (e){ return { ok:false, msg: (e && e.message) || String(e) }; } finally { try { if (wroteDates && !cleared) { sh.getRangeList(['B3','D3']).clearContent(); SpreadsheetApp.flush(); } } catch(_){} try { lock.releaseLock(); } catch(_){} } } /** Doar citește A6:J6 (util când vrei să reîncerci fără să mai rescrii B3/D3). */ function sr_target_getRow(token){ _getSession(token); var sh = _sheet(RAPOARTE_SALES_SHEET); var row = sh.getRange(6,1,1,10).getDisplayValues()[0]; var ready = row.some(v => String(v).trim() !== ''); return { ok:true, ready, row: ready ? row : [] }; } /** Row rapid pentru un ID UNIC, via Index/Cache (validat). */ function clientiActivi_rowById(token, idUnic){ _getSession(token); idUnic = String(idUnic||'').trim(); if (!idUnic) return { ok:false, row:0 }; const hit = index_get(idUnic); return { ok: !!(hit && hit.row), row: (hit && hit.row) || 0 }; } /** * Observatii complete din MDC pentru pop-up (folosește rowHint pentru viteză). * @param {string} token * @param {string} idUnic * @param {number} rowHint - rândul din MDC (2-based) dacă este cunoscut; altfel 0 */ /** Observații complete din Supabase (bd_clienti_activi.observatii) pentru pop-up. */ function gest_getMoreObs(token, idUnic, rowHint){ _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) return { ok:false, msg:'ID UNIC lipsă.' }; var rec = supaOneById_(idUnic, 'observatii'); if (!rec) return { ok:false, msg:'Client inexistent în bd_clienti_activi.' }; return { ok: true, obs: _s(rec.observatii) || '' }; } /** Lookup global după ID_UNIC: întâi din index (clienți activi), apoi căutare full în MDC. */ function clienti_rowById_any(token, idUnic){ _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) return { ok:false, msg:'ID invalid', row:0 }; var row = 0; // 1) încearcă din index (de obicei populat doar cu „Activii”) var hit = index_get(idUnic); if (hit && hit.row) row = Number(hit.row) || 0; // 2) fallback: caută direct în foaia MDC (vede și RESPINS etc.) if (!row) row = findMdcRowById_(idUnic); return { ok: !!row, row: row || 0 }; } /** Returnează întregul rând (array) din MDC pentru ID_UNIC, indiferent de status. */ function clienti_getRowById_any(token, idUnic){ _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) return { ok:false }; var sh = _sheet(CLIENTI_SHEET); var row = findMdcRowById_(idUnic); if (!(row >= 2 && row <= sh.getLastRow())) return { ok:false }; var values = sh.getRange(row, 1, 1, sh.getLastColumn()).getDisplayValues()[0] || []; return { ok:true, row: values }; } /** ===== Rapoarte Sales – CLASAMENT VÂNZĂRI (din bd_incasari – Supabase) ===== **/ function sr_rank_list(token, fromIso, toIso){ _getSession(token); // doar validare sesiune const conds = []; if (fromIso) conds.push('data_incasare=gte.' + encodeURIComponent(fromIso)); if (toIso) conds.push('data_incasare=lte.' + encodeURIComponent(toIso)); const qs = 'select=' + encodeURIComponent('agent,data_incasare,comm_net,tip_incasare') + (conds.length ? '&' + conds.join('&') : ''); const rows = supaSelectAll_('bd_incasari', qs, 2000) || []; const acc = Object.create(null); rows.forEach(r => { const agent = _s(r.agent).trim(); if (!agent) return; const iso = (r.data_incasare || '').trim(); // deja yyyy-MM-dd if (!iso) return; const net = Number(r.comm_net || 0) || 0; const tip = String(r.tip_incasare || '').trim().toUpperCase(); const fin = tip.startsWith('FINANT'); // FINANTARE = dosar finanțat if (!acc[agent]) acc[agent] = { net:0, cnt:0, cntFin:0 }; acc[agent].net += net; acc[agent].cnt += 1; if (fin) acc[agent].cntFin += 1; }); const out = Object.keys(acc).map(agent => { const suma = acc[agent].net; const cnt = acc[agent].cnt || 0; const cntFin = acc[agent].cntFin || 0; // 10 p/dosar FINANȚAT + 10 p / 10.000 RON încasați (pe NET) const punctajRaw = cntFin * 10 + (suma / 10000) * 10; const punctaj = Math.round(punctajRaw * 100) / 100; const sumaMedie = cnt ? (suma / cnt) : 0; return { agent, punctaj, suma, nrDosare: cnt, sumaMedie }; }).sort((a,b) => (b.punctaj - a.punctaj) || (b.suma - a.suma) || a.agent.localeCompare(b.agent, 'ro') ); return { ok:true, rows: out }; } /** ===== Rapoarte Sales — ÎNCASĂRI TOTALE (detaliat pe agent, din bd_incasari) ===== **/ function sr_incasari_detalii(token, fromIso, toIso){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase(); const isUtil = (role === 'utilizator'); const me = String(sess.user || '').trim(); const conds = []; if (fromIso) conds.push('data_incasare=gte.' + encodeURIComponent(fromIso)); if (toIso) conds.push('data_incasare=lte.' + encodeURIComponent(toIso)); if (isUtil && me) conds.push('agent=eq.' + encodeURIComponent(me)); const qs = 'select=' + encodeURIComponent('agent,data_incasare,comm_net,nume_complet_client') + (conds.length ? '&' + conds.join('&') : '') + '&order=' + encodeURIComponent('data_incasare') + '.asc'; const rows = supaSelectAll_('bd_incasari', qs, 5000) || []; const acc = Object.create(null); let totalAll = 0; rows.forEach(r => { const agent = _s(r.agent).trim(); if (!agent) return; const iso = (r.data_incasare || '').trim(); if (!iso) return; const net = Number(r.comm_net || 0) || 0; const nume = _s(r.nume_complet_client) || '(necunoscut)'; if (!acc[agent]) acc[agent] = { agent, total:0, rows:[] }; acc[agent].rows.push({ client: nume, data: _fmtDateHuman(_fromYMD_(iso)), suma: net }); acc[agent].total += net; totalAll += net; }); const groups = Object.keys(acc).map(k => { const g = acc[k]; g.rows.sort((a,b) => (b.suma - a.suma) || (a.data.localeCompare(b.data, 'ro'))); return g; }).sort((a,b) => (b.total - a.total) || a.agent.localeCompare(b.agent, 'ro')); return { ok:true, groups, total: totalAll }; } /** ===== Rapoarte Sales – Target: breakdown zilnic (din bd_incasari) ===== **/ function sr_target_dailyBreakdown(token, month, year){ _getSession(token); const m = Number(month), y = Number(year); if (!(m>=1 && m<=12) || !Number.isFinite(y)) { return { ok:false, msg:'Parametri lună/an invalizi.' }; } // interval [prima zi calendaristică, ultima] const first = new Date(y, m-1, 1); const lastD = new Date(y, m, 0); const tz = Session.getScriptTimeZone(); const toIso = d => Utilities.formatDate(d, tz, 'yyyy-MM-dd'); const toLabel = d => Utilities.formatDate(d, tz, 'dd/MM/yyyy'); const isWeekend = d => { const w=d.getDay(); return (w===0||w===6); }; // listă zile lucrătoare din lună const days = []; for (let d=new Date(first); d<=lastD; d=new Date(d.getFullYear(), d.getMonth(), d.getDate()+1)){ if (!isWeekend(d)) days.push({ iso: toIso(d), label: toLabel(d), sum: 0 }); } const idxByIso = Object.create(null); days.forEach((x,i)=> idxByIso[x.iso]=i); // citim încasările din Supabase const qs = 'select=' + encodeURIComponent('data_incasare,comm_net') + '&data_incasare=gte.' + encodeURIComponent(toIso(first)) + '&data_incasare=lte.' + encodeURIComponent(toIso(lastD)); const rows = supaSelectAll_('bd_incasari', qs, 5000) || []; let total = 0; rows.forEach(r => { const iso = (r.data_incasare || '').trim(); const k = idxByIso[iso]; if (k == null) return; const net = Number(r.comm_net || 0) || 0; days[k].sum += net; total += net; }); return { ok:true, days, total }; } /** ====== Rapoarte (fișier extern) – setare interval luna anterioară ====== */ const RAPOARTE_EXT_FILE_ID = '1qz_T1KnzO8Fn5cu3rgs-NB3MDinvIr_0wjNsjU75SOU'; const RAPOARTE_SHEET_NAME = 'Rapoarte'; function sr_sendPrevMonthRange(token, month, year){ // validați sesiunea _getSession(token); const m = Number(month), y = Number(year); if (!(m >= 1 && m <= 12) || !Number.isFinite(y)){ return { ok:false, msg:'Parametri lună/an invalizi.' }; } // luna anterioară let pm = m - 1, py = y; if (pm === 0){ pm = 12; py = y - 1; } // prima și ultima zi calendaristică din luna anterioară const firstCal = new Date(py, pm - 1, 1); const lastCal = new Date(py, pm, 0); // helper: e weekend? const isWeekend = d => { const w = d.getDay(); return (w === 0 || w === 6); }; // prima zi lucrătoare const firstWD = new Date(firstCal); while (isWeekend(firstWD)) firstWD.setDate(firstWD.getDate() + 1); // ultima zi lucrătoare const lastWD = new Date(lastCal); while (isWeekend(lastWD)) lastWD.setDate(lastWD.getDate() - 1); // scriere în fișierul extern const ext = SpreadsheetApp.openById(RAPOARTE_EXT_FILE_ID); const sh = ext.getSheetByName(RAPOARTE_SHEET_NAME); if (!sh) return { ok:false, msg:'Nu găsesc foaia „Rapoarte” în fișierul extern.' }; sh.getRange('B3').setValue(firstWD).setNumberFormat('dd/MM/yyyy'); sh.getRange('D3').setValue(lastWD ).setNumberFormat('dd/MM/yyyy'); SpreadsheetApp.flush(); return { ok:true, first:firstWD, last:lastWD }; } /** ===== LIVE UPDATES: feed ușor bazat pe ScriptProperties ===== */ const UPD_SEQ_KEY = 'upd:seq'; const UPD_BUF_KEY = 'upd:buf'; const UPD_BUF_MAX = 300; // păstrăm ultimele 300 de update-uri (e < 9KB) function updates_emit(idUnic) { idUnic = String(idUnic || '').trim(); if (!idUnic) return 0; const lock = LockService.getScriptLock(); // mic lock să evităm ciupirea când vin salvări simultane lock.tryLock(200); try { const props = PropertiesService.getScriptProperties(); let seq = Number(props.getProperty(UPD_SEQ_KEY) || '0') + 1; const raw = props.getProperty(UPD_BUF_KEY) || ''; let arr = raw ? raw.split(',') : []; arr.push(seq + '|' + idUnic); if (arr.length > UPD_BUF_MAX) arr = arr.slice(arr.length - UPD_BUF_MAX); props.setProperty(UPD_SEQ_KEY, String(seq)); props.setProperty(UPD_BUF_KEY, arr.join(',')); return seq; } finally { try { lock.releaseLock(); } catch(_) {} } } /** * Frontend-ul întreabă la fiecare 12s: ce s-a schimbat după seq-ul meu? * Returnăm: * { ok:true, seq:, ids:[...] } * UI va cere fiecare id prin clientiActivi_getRowById (care aplică permisiunile). */ function updates_poll(token, sinceSeq) { _getSession(token); // validează sesiunea const props = PropertiesService.getScriptProperties(); const curSeq = Number(props.getProperty(UPD_SEQ_KEY) || '0'); const raw = props.getProperty(UPD_BUF_KEY) || ''; if (!raw) return { ok:true, seq: curSeq, ids: [] }; const items = raw.split(','); const want = Number(sinceSeq || 0); const seen = Object.create(null); const ids = []; for (let i = 0; i < items.length; i++) { const [sStr, id] = items[i].split('|'); const s = Number(sStr || '0'); if (s > want && id && !seen[id]) { seen[id] = 1; ids.push(id); } } return { ok:true, seq: curSeq, ids }; } /** Update STATUS / MOTIV / COMENTARII pentru o programare din bd_programari (cheie = id). */ function programari_updateStatus(token, progId, status, motiv, comentA, comentM){ const sess = _getSession(token); const id = Number(progId || 0); if (!id) throw new Error('ID programare invalid.'); const patch = {}; if (status != null){ patch.status = String(status || '').trim() || null; } if (motiv != null){ patch.motiv = String(motiv || '').trim() || null; } if (comentA != null){ patch.comentariu_agent = String(comentA || '').trim() || null; } // comentariu manager doar pentru Manager / Administrator / Controlor if (comentM != null){ const role = String(sess.role || '').toLowerCase(); const can = ['manager','administrator','controlor'].includes(role); if (!can) throw new Error('Nu ai drepturi la COMENTARIU MANAGER.'); patch.comentariu_manager = String(comentM || '').trim() || null; } if (!Object.keys(patch).length){ return { ok:true }; } patch.id = id; // important pentru supaUpsert_ cu conflict 'id' supaUpsert_(SUPA_PROGRAMARI, [patch], 'id'); return { ok:true }; } /** Wrapper folosit de Code.gs pentru a emite update-uri live către frontend. */ function ca_pub_push_(idUnic) { try { updates_emit(String(idUnic || '').trim()); } catch (e) { // lasă liniște – live-ul rămâne best-effort } } /** ===== Rapoarte Sales — ÎNCASĂRI PE BANCI (din bd_incasari – Supabase) ===== **/ function sr_incasari_banci(token, fromIso, toIso){ _getSession(token); const conds = []; if (fromIso) conds.push('data_incasare=gte.' + encodeURIComponent(fromIso)); if (toIso) conds.push('data_incasare=lte.' + encodeURIComponent(toIso)); const select = 'nume_complet_client,data_incasare,comm_brut,' + 'denumire_banca_1,suma_totala_banca_1,suma_suplim_banca_1,' + 'denumire_banca_2,suma_totala_banca_2,suma_suplim_banca_2,' + 'denumire_banca_3,suma_totala_banca_3,suma_suplim_banca_3,' + 'denumire_banca_4,suma_totala_banca_4,suma_suplim_banca_4'; const qs = 'select=' + encodeURIComponent(select) + (conds.length ? '&' + conds.join('&') : ''); const rows = supaSelectAll_('bd_incasari', qs, 5000) || []; const tz = Session.getScriptTimeZone(); const fmtDMY = iso => { if (!iso) return ''; const d = _fromYMD_(iso); return d ? Utilities.formatDate(d, tz, 'dd.MM.yyyy') : ''; }; const isNoCollab = s => /FARA\s*COLAB/i.test(String(s||'')); const bankMap = Object.create(null); rows.forEach(r => { const client = _s(r.nume_complet_client); const iso = (r.data_incasare || '').trim(); const data = fmtDMY(iso); const brut = Number(r.comm_brut || 0) || 0; function pushBank(den, tot, supl){ const banca = _s(den).trim(); if (!banca) return; if (isNoCollab(banca)) return; const key = banca.toUpperCase(); if (!bankMap[key]) bankMap[key] = { name:banca, rows:[] }; const sumaTot = Number(tot || 0) || 0; const sumaSupl = Number(supl || 0) || 0; const sumaIncasata = brut; const commMax = Math.round(sumaIncasata * 0.20 * 100) / 100; bankMap[key].rows.push({ client, data, sumaTotala: sumaTot, sumaSupliment: sumaSupl, sumaIncasata, commMaxRecom: commMax }); } pushBank(r.denumire_banca_1, r.suma_totala_banca_1, r.suma_suplim_banca_1); pushBank(r.denumire_banca_2, r.suma_totala_banca_2, r.suma_suplim_banca_2); pushBank(r.denumire_banca_3, r.suma_totala_banca_3, r.suma_suplim_banca_3); pushBank(r.denumire_banca_4, r.suma_totala_banca_4, r.suma_suplim_banca_4); }); const banks = Object.values(bankMap) .sort((a,b)=> a.name.localeCompare(b.name,'ro')) .map(b => { b.rows.sort((r1,r2) => { // sortăm crescător pe dată const p = s => { const m = String(s||'').split('.'); return (m.length===3) ? new Date(+m[2],+m[1]-1,+m[0]).getTime() : 0; }; return p(r1.data) - p(r2.data); }); return b; }); return { ok:true, from:fromIso, to:toIso, banks }; } /** * Export „Încasări pe bănci” în XLSX (base64 pentru download direct din UI), * folosind bd_incasari din Supabase. */ function sr_incasari_banci_export(token, fromIso, toIso) { _getSession(token); // validare sesiune // folosim deja agregarea de mai sus const res = sr_incasari_banci(token, fromIso, toIso); if (!res || !res.ok) { throw new Error(res && res.msg ? res.msg : 'Nu s-au putut obține încasările pe bănci.'); } const rows = []; (res.banks || []).forEach(b => { const name = b.name; (b.rows || []).forEach(r => { rows.push([ name, r.client, r.data, r.sumaTotala, r.sumaSupliment, r.sumaIncasata, r.commMaxRecom ]); }); }); return _incBanci__exportXlsx_(rows, fromIso, toIso); } /** * Creează Spreadsheet temporar, scrie header + date, convertește în XLSX (base64) și îl returnează. * NU șterge foaia implicită, doar o redenumește (evită „You can't remove all the sheets…”). */ function _incBanci__exportXlsx_(rows, fromIso, toIso){ // 1) Creează spreadsheet temporar și scrie datele const ss = SpreadsheetApp.create(`Raport banci ${fromIso||''} – ${toIso||''}`); const sh = ss.getSheets()[0]; // foaia implicită sh.setName('Raport Banci'); sh.clear(); const header = [ 'BANCA','NUME CLIENT','DATA', 'SUMA TOTALA','SUMA SUPLIMENTARA', 'SUMA INCASATA','COMM. MAX. RECOMANDAT' ]; sh.getRange(1,1,1,header.length).setValues([header]); if (rows && rows.length){ sh.getRange(2,1,rows.length,header.length).setValues(rows); sh.setFrozenRows(1); // Formatare sh.getRange(2,3,rows.length,1).setNumberFormat('dd/MM/yyyy'); // DATA sh.getRange(2,4,rows.length,4).setNumberFormat('#,##0.00'); // sumele } SpreadsheetApp.flush(); // 2) Export XLSX prin endpoint-ul de export const exportUrl = `https://docs.google.com/spreadsheets/d/${ss.getId()}/export?format=xlsx`; const resp = UrlFetchApp.fetch(exportUrl, { headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() }, muteHttpExceptions: true }); // Protecție: eroare la export → mesaj clar const code = resp.getResponseCode(); if (code < 200 || code >= 300){ try { DriveApp.getFileById(ss.getId()).setTrashed(true); } catch(_){} throw new Error('Export XLSX a eșuat (HTTP ' + code + ').'); } const blob = resp.getBlob().setName(ss.getName() + '.xlsx'); const content = Utilities.base64Encode(blob.getBytes()); // 3) Curățăm fișierul temporar din Drive try { DriveApp.getFileById(ss.getId()).setTrashed(true); } catch(_){} // 4) Înapoi la UI (frontend-ul tău așteaptă base64 + filename) return { ok:true, content, filename: blob.getName() }; } /** =================== SURSE TEHNICE – Ștergeri Speciale (E..I) =================== **/ const SURSE_TEHNICE_SHEET = 'Surse Tehnice'; // header pe rândul 2 function _st_sheet_(){ return _sheet(SURSE_TEHNICE_SHEET); } function _st_h_(){ return _headerMap_(_st_sheet_(), 2); } function _ensureAdmin_(sess){ const role = String(sess.role||'').toLowerCase(); if (role !== 'administrator') throw new Error('Acces permis doar Administratorului.'); } /** Listează rândurile SS (E..I) + opțiuni dropdown (din B și C) */ function st_ss_list(token){ const sess=_getSession(token);_ensureAdmin_(sess); const sel=encodeURIComponent('"NR CRT","DENUMIRE INTERNA SS","NUMAR/TIPURI CONTURI","PRET PLATIT","PRET CERUT INTERN","PRET CERUT EXTERN","EXPLICATII"'); const qs='select='+sel+'&order='+encodeURIComponent('"NR CRT"')+'.asc'; const a=supaSelectAll_(SUPA_LISTA_CREDITORI_SS,qs,2000)||[]; const b=supaSelectAll_(SUPA_LISTA_CREDITORI,'select='+encodeURIComponent('"DENUMIRE COMERCIALA"')+'&order='+encodeURIComponent('"DENUMIRE COMERCIALA"')+'.asc',3000)||[]; const den={};b.forEach(r=>{const d=String(r['DENUMIRE COMERCIALA']||'').trim();if(d)den[d]=1;}); a.forEach(r=>{const d=String(r['DENUMIRE INTERNA SS']||'').trim();if(d)den[d]=1;}); return{ok:true,rows:a.map(r=>({row:r['NR CRT'],den:r['DENUMIRE INTERNA SS']||'',conturi:r['NUMAR/TIPURI CONTURI']||'',pretPlat:r['PRET PLATIT']??'',pretInt:r['PRET CERUT INTERN']??'',pretExt:r['PRET CERUT EXTERN']??'',exp:r['EXPLICATII']||''})),opts:{denumiri:Object.keys(den).sort((x,y)=>x.localeCompare(y,'ro'))}}; } function st_ss_add(token,p){ const sess=_getSession(token);_ensureAdmin_(sess);p=p||{}; const den=String(p.den||'').trim(), cont=String(p.conturi||'').trim(), exp=String(p.exp||'').trim(); if(!den) return{ok:false,msg:'Completează denumirea internă.'}; const n=v=>{ v=String(v==null?'':v).trim(); if(v==='')return null; v=v.replace(/\./g,'').replace(/\s+/g,'').replace(',','.'); const x=Number(v); return isFinite(x)?Math.round(x):null; }; supaInsertOne_(SUPA_LISTA_CREDITORI_SS,{ 'DENUMIRE INTERNA SS':den, 'NUMAR/TIPURI CONTURI':cont||null, 'PRET PLATIT':n(p.pretPlat), 'PRET CERUT INTERN':n(p.pretInt), 'PRET CERUT EXTERN':n(p.pretExt), 'EXPLICATII':exp||null }); return{ok:true}; } function st_ss_update(token,nr,p){ const sess=_getSession(token);_ensureAdmin_(sess);nr=Number(nr||0);if(!nr) return{ok:false,msg:'NR invalid.'};p=p||{}; const n=v=>{v=String(v==null?'':v).trim();if(v==='')return null;v=v.replace(/\./g,'').replace(/\s+/g,'').replace(',','.');const x=Number(v);return isFinite(x)?Math.round(x):null;}; const body={'DENUMIRE INTERNA SS':String(p.den||'').trim()||null,'NUMAR/TIPURI CONTURI':String(p.conturi||'').trim()||null,'PRET PLATIT':n(p.pretPlat),'PRET CERUT INTERN':n(p.pretInt),'PRET CERUT EXTERN':n(p.pretExt),'EXPLICATII':String(p.exp||'').trim()||null}; const url=`${SUPA_URL}/rest/v1/${SUPA_LISTA_CREDITORI_SS}?${encodeURIComponent('"NR CRT"')}=eq.${encodeURIComponent(nr)}`; const res=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify(body),muteHttpExceptions:true}); if(res.getResponseCode()>=300) return{ok:false,msg:'Supabase UPDATE: '+res.getContentText()}; return{ok:true}; } function st_ss_delete(token,nr){ const sess=_getSession(token);_ensureAdmin_(sess);nr=Number(nr||0);if(!nr) return{ok:false,msg:'NR invalid.'}; const url=`${SUPA_URL}/rest/v1/${SUPA_LISTA_CREDITORI_SS}?${encodeURIComponent('"NR CRT"')}=eq.${encodeURIComponent(nr)}`; const res=UrlFetchApp.fetch(url,{method:'delete',headers:supaHeaders_(false),muteHttpExceptions:true}); if(res.getResponseCode()>=300) return{ok:false,msg:'Supabase DELETE: '+res.getContentText()}; return{ok:true}; } /** ===== CALCULATOR STERGERE BC — opțiuni & prețuri (E3:E300 -> H) ===== */ function bc_getOptionsAndPrices(token){ _getSession(token); const sel=encodeURIComponent('"DENUMIRE INTERNA SS","PRET CERUT INTERN"'); const qs='select='+sel+'&order='+encodeURIComponent('"DENUMIRE INTERNA SS"')+'.asc'; const a=supaSelectAll_(SUPA_LISTA_CREDITORI_SS,qs,2000)||[]; const priceByDen={},opt=[]; a.forEach(r=>{const d=String(r['DENUMIRE INTERNA SS']||'').trim();if(!d)return;opt.push(d);if(!(d in priceByDen))priceByDen[d]=Number(r['PRET CERUT INTERN']||0)||0;}); const options=Array.from(new Set(opt)).sort((x,y)=>x.localeCompare(y,'ro')); return{ok:true,options,priceByDen}; } /** ===== ȘTERGERI STANDARD – ÎN LUCRU (BD Stergeri BC, A:H) ===== * A: NUME CLIENT | B: CNP | C: STATUS GENERAL | D: CREDITORI | E: STATUS * F: OBSERVATII | G: DATA DEPUNERE | H: DATA REVENIRE */ function sb_std_listLucru(token){ const sess = _getSession(token); // <— ia user/rol din sesiune const sh = _sheet('BD Stergeri BC'); const last = sh.getLastRow(); if (last < 2) return { ok:true, groups: [] }; const H = _headerMap_(sh, 1); const cAgent = H['AGENT']; const cNume = H['NUME CLIENT'] || H['NUME']; const cCnp = H['CNP']; const cStg = H['STATUS GENERAL']; const cCred = H['CREDITORI'] || H['CREDITOR'] || H['CREDITOR 1'] || H['CREDITORI / BANCĂ']; const cStat = H['STATUS']; const cObs = H['OBSERVATII'] || H['OBSERVAȚII']; const cDep = H['DATA DEPUNERE']; const cRev = H['DATA REVENIRE']; if (!(cAgent && cNume && cCnp && cStg && cCred)) { return { ok:false, msg:'Antete lipsă în „BD Stergeri BC”: AGENT, NUME CLIENT, CNP, STATUS GENERAL, CREDITORI …' }; } const width = Math.max(cAgent, cNume, cCnp, cStg, cCred, cStat, cObs, cDep, cRev); const vals = sh.getRange(2, 1, last - 1, width).getDisplayValues(); // carry-down la nivel de foaie let prevAgent = '', prevNume = '', prevCnp = '', prevStg = ''; const bucket = Object.create(null); const groups = []; for (let i = 0; i < vals.length; i++){ const r = vals[i]; const agentR = String(r[cAgent-1] || '').trim(); const numeR = String(r[cNume-1] || '').trim(); const cnpR = String(r[cCnp-1] || '').trim(); const stgR = String(r[cStg-1] || '').trim().toUpperCase(); const agent = agentR || prevAgent; const nume = numeR || prevNume; const cnp = cnpR || prevCnp; const stg = stgR || prevStg; prevAgent = agent; prevNume = nume; prevCnp = cnp; if (stgR) prevStg = stgR; if (!nume && !cnp) continue; if (stg !== 'IN LUCRU') continue; const cred = String(r[cCred-1] || '').trim(); const st = String(r[cStat-1] || '').trim(); const obs = String(r[cObs-1] || '').trim(); const dep = String(r[cDep-1] || '').trim(); const rev = String(r[cRev-1] || '').trim(); const key = [agent, nume, cnp, stg].join('|'); if (!bucket[key]){ const g = { agent, nume, cnp, stg, banks: [], _i: i }; bucket[key] = g; groups.push(g); } if (cred || st || obs || dep || rev){ bucket[key].banks.push({ cred, st, obs, dep, rev }); } } groups.sort((a,b) => a._i - b._i); // —— FILTRARE pe rol: Utilizator vede doar AI LUI —— const isUtil = String(sess.role||'').toLowerCase() === 'utilizator'; const me = String(sess.user||'').trim().toUpperCase(); const filtered = isUtil ? groups.filter(g => String(g.agent||'').trim().toUpperCase() === me) : groups; return { ok:true, groups: filtered }; } /** ====== Export generic XLSX (folosit de "Exportă clienți") ====== */ function ca_export_xlsx(token, headers, rows){ _getSession(token); // validează sesiunea var tz = Session.getScriptTimeZone(); var stamp = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd HH.mm'); var name = 'Clienti Activi ' + stamp; return _exportGenericXlsx_(name, 'Clienti Activi', headers, rows); } /** * Creează un Spreadsheet temporar, scrie header + date, exportă XLSX (base64), * șterge fișierul temporar și întoarce { ok, content, filename }. */ function _exportGenericXlsx_(title, sheetName, headers, rows){ var ss = SpreadsheetApp.create(title); try{ var sh = ss.getSheets()[0]; sh.setName(sheetName || 'Export'); sh.clear(); // header sh.getRange(1,1,1,headers.length).setValues([headers]); sh.getRange(1,1,1,headers.length) .setFontWeight('bold') .setBackground('#6495ED') .setFontColor('#ffffff'); // rows if (rows && rows.length){ sh.getRange(2,1,rows.length,headers.length).setValues(rows); } sh.setFrozenRows(1); try { sh.autoResizeColumns(1, headers.length); } catch(_){} var exportUrl = 'https://docs.google.com/spreadsheets/d/' + ss.getId() + '/export?format=xlsx'; var resp = UrlFetchApp.fetch(exportUrl, { headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() }, muteHttpExceptions: true }); var code = resp.getResponseCode(); if (code < 200 || code >= 300){ throw new Error('Export XLSX a eșuat (HTTP ' + code + ').'); } var blob = resp.getBlob().setName(title + '.xlsx'); var content = Utilities.base64Encode(blob.getBytes()); return { ok:true, content: content, filename: blob.getName() }; } finally { try { DriveApp.getFileById(ss.getId()).setTrashed(true); } catch(_){} } } /** ===== TRAINING VIDEO – Supabase Storage (bucket "training") ===== **/ // helper: doar Administratorul poate încărca/șterge materiale function _trnEnsureAdmin_(sess){ const role = String(sess.role||'').toLowerCase(); if (role !== 'administrator') throw new Error('Doar Administratorul poate încărca/șterge materiale de training.'); } // construiește un nume safe pentru fișiere (fără caractere dubioase) function _trainingSafeName_(s){ return _safeKeyName_(s || 'material'); // reutilizăm helperul folosit și la documentele client } /** Upload video în Supabase Storage + insert în bd_materiale_training */ function training_uploadVideo(token, meta, fd){ const sess = _getSession(token); _trnEnsureAdmin_(sess); if (!fd || !fd.data || !fd.filename) throw new Error('Fișier lipsă.'); const base64 = String(fd.data).split(',').pop(); const bytes = Utilities.base64Decode(base64); const mime = (meta && meta.mimeType) || 'video/mp4'; const title = (meta && meta.title) || fd.filename; const cat = (meta && meta.category) || 'SPETE'; // folder din categorie (ex. "GENERAL" -> "general", "SPETE" -> "spete") const folder = String(cat || 'SPETE').toLowerCase().replace(/[^a-z0-9]+/g,'_') || 'general'; // nume safe (fără diacritice și caractere periculoase) const safeBase = _trainingSafeName_(title || fd.filename); const extMatch = /(\.[^.]+)$/.exec(fd.filename || ''); const ext = extMatch ? extMatch[1] : ''; // path în bucket (ex: "spete/intro_video.mp4") const objectPath = `${folder}/${safeBase}${ext}`; // 1) Upload în bucket-ul privat "training" supaStorage_uploadToBucket(SUPA_BUCKET_TRAINING, objectPath, bytes, mime); // 2) Insert metadata în bd_materiale_training const row = { title: title, category: cat, file_path: objectPath, mime_type: mime, size_bytes: bytes.length, uploader: sess.user || '' }; const rec = supaInsertOne_(SUPA_TRAINING_TABLE, row); // 3) URL semnat (1 oră) const url = supaStorage_signedUrlFromBucket(SUPA_BUCKET_TRAINING, objectPath, 3600); // păstrăm forma de răspuns de dinainte (preview/view) return { ok: true, id: rec.id, title: rec.title, category: rec.category, preview: url, view: url }; } /** Listare materiale (opțional filtrat pe categorie) din bd_materiale_training */ function training_listMaterials(token, category){ _getSession(token); let qs = 'select=' + encodeURIComponent('id,created_at,title,category,file_path') + '&order=' + encodeURIComponent('created_at') + '.desc'; if (category){ qs += '&category=eq.' + encodeURIComponent(category); } const arr = supaSelectAll_(SUPA_TRAINING_TABLE, qs, 500) || []; const rows = arr.map(r => { const url = supaStorage_signedUrlFromBucket(SUPA_BUCKET_TRAINING, r.file_path, 3600); return { createdAt: r.created_at, title: r.title, category: r.category, fileId: r.id, // ID-ul din bd_materiale_training preview: url, view: url }; }); return { ok:true, rows }; } /** Ștergere material: scoate fișierul din Storage + rândul din bd_materiale_training */ function training_deleteMaterial(token, fileId){ const sess = _getSession(token); _trnEnsureAdmin_(sess); const id = Number(fileId || 0); if (!id) return { ok:false, msg:'ID lipsă' }; // 1) citim path-ul din bd_materiale_training const qs = 'id=eq.' + encodeURIComponent(id) + '&select=' + encodeURIComponent('file_path'); const rows = supaGet_(SUPA_TRAINING_TABLE, qs); const rec = rows && rows[0]; if (rec && rec.file_path){ try { supaStorage_deleteFromBucket(SUPA_BUCKET_TRAINING, rec.file_path); } catch(e){ // nu blocăm ștergerea meta-data pe eroare de Storage, doar logăm console.error('Delete training file failed:', e); } } // 2) ștergem rândul din bd_materiale_training const url = `${SUPA_URL}/rest/v1/${SUPA_TRAINING_TABLE}?id=eq.${encodeURIComponent(id)}`; const res = UrlFetchApp.fetch(url, { method: 'delete', headers: supaHeaders_(false), muteHttpExceptions: true }); if (res.getResponseCode() >= 300){ throw new Error('Supabase DELETE training material: ' + res.getContentText()); } return { ok:true }; } /** ===== TO DO – backend ===== */ const TODO_SHEET = 'TO DO'; /** Citește toate taskurile din Supabase */ function todo_list(token) { _getSession(token); // validare sesiune const qs = 'select=*'; let rows = supaSelectAll_(SUPA_TODO, qs, 1000) || []; // 🔹 păstrăm DOAR rândurile care chiar au text în "to_do_problema" rows = rows.filter(r => { const txt = (r.to_do_problema || '').toString().trim(); return txt !== ''; }); // sortăm: mai întâi nefinalizate (NU), apoi finalizate (DA), ambele DESC după data const toDate = s => { const d = new Date(s); return isNaN(d) ? 0 : d.getTime(); }; rows.sort((a, b) => { const ra = (a.rezolvat || '').trim().toUpperCase() === 'DA' ? 1 : 0; const rb = (b.rezolvat || '').trim().toUpperCase() === 'DA' ? 1 : 0; if (ra !== rb) return ra - rb; const da = a.data || ''; const db = b.data || ''; return toDate(db) - toDate(da); // cele mai noi primele }); return { ok: true, rows }; } /** Adaugă un task nou – scrie în primul rând liber (unde to_do_problema IS NULL) */ function todo_add(token, p) { _getSession(token); // Caută primul rând liber (to_do_problema IS NULL) const qs = 'select=' + encodeURIComponent('"task_id","to_do_problema"') + '&to_do_problema=is.null' + '&order=' + encodeURIComponent('"task_id"') + '.asc&limit=1'; const res = supaGet_(SUPA_TODO, qs); if (!res || !res.length) { throw new Error('Nu există rânduri libere pentru alocarea unui task nou.'); } const row = res[0]; const taskId = row.task_id; const today = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd'); const patch = { task_id: taskId, data: today, to_do_problema: String(p.to_do_problema || '').trim(), localizare: String(p.localizare || '').trim(), detalii: String(p.detalii || '').trim(), corelatie: String(p.corelatie || '').trim(), deadline: String(p.deadline || '').trim(), modalitatea_de_rezolvare: String(p.modalitatea_de_rezolvare || '').trim(), rezolvat: 'NU' }; supaUpsert_(SUPA_TODO, [patch], 'task_id'); return { ok: true, task_id: taskId }; } /** Actualizează un task (editare) */ function todo_update(token, taskId, payload) { _getSession(token); if (!taskId) throw new Error('Lipsă task_id pentru update.'); const patch = {}; const allowed = ['to_do_problema', 'localizare', 'detalii', 'corelatie', 'deadline', 'modalitatea_de_rezolvare']; allowed.forEach(k => { if (payload.hasOwnProperty(k)) { patch[k] = String(payload[k] || '').trim(); } }); if (Object.keys(patch).length === 0) return { ok: false, msg: 'Nimic de actualizat.' }; patch.task_id = Number(taskId); supaUpsert_(SUPA_TODO, [patch], 'task_id'); return { ok: true }; } /** Marchează un task ca finalizat */ function todo_finalize(token, taskId) { _getSession(token); if (!taskId) throw new Error('Lipsă task_id pentru finalizare.'); const patch = { task_id: Number(taskId), rezolvat: 'DA' }; supaUpsert_(SUPA_TODO, [patch], 'task_id'); return { ok: true }; } /** * Generează un ID UNIC NOU, sigur la concurență, fără să umblăm la structura tabelului. * Folosește: * - ScriptProperties pentru a păstra ultimul ID folosit * - ScriptLock (LockService) pentru a evita coliziuni când 2 useri adaugă simultan */ function _nextIdUnicSupabase_(){ const lock = LockService.getScriptLock(); // așteaptă max 5s până prinde lock sau aruncă eroare lock.waitLock(5000); try{ const props = PropertiesService.getScriptProperties(); const lastRaw = props.getProperty(ID_COUNTER_KEY); let last = Number(lastRaw); // luăm întotdeauna max(id_unic) din Supabase const maxSup = _computeMaxIdFromSupabase_(); // dacă ScriptProperties e gol / corupt / mai mic decât Supabase → sincronizăm if (!isFinite(last) || last < maxSup) { last = maxSup; } const next = last + 1; props.setProperty(ID_COUNTER_KEY, String(next)); return String(next); } finally { try { lock.releaseLock(); } catch(_){} } } /** Debug/Control: citește ultimul ID din Supabase și "next:id_unic" din ScriptProperties. */ function supa_peekNextId(){ const props = PropertiesService.getScriptProperties(); const counterRaw = props.getProperty(ID_COUNTER_KEY); const counter = Number(counterRaw); const maxSup = _computeMaxIdFromSupabase_(); return { ok: true, supabaseLastId: maxSup, // ultimul ID din Supabase propCounterRaw: counterRaw || null, // ce e salvat acum în ScriptProperties propCounter: isFinite(counter) ? counter : null, suggestedNextFreeId: maxSup + 1 }; } /** Resetează explicit contorul "next:id_unic" la (max(id_unic) + 1) din Supabase. */ function supa_resyncIdCounterFromSupabase(){ const maxSup = _computeMaxIdFromSupabase_(); const next = maxSup + 1; PropertiesService.getScriptProperties().setProperty(ID_COUNTER_KEY, String(next)); Logger.log('Contor ID resetat la ' + next + ' (max din Supabase = ' + maxSup + ').'); return { ok:true, maxId:maxSup, nextId:next }; } /** INSERT simplu (fără upsert) care întoarce rândul inserat. */ function supaInsertOne_(table, row){ const url = SUPA_URL + '/rest/v1/' + table + '?select=*'; const headers = supaHeaders_(false); headers['Prefer'] = 'return=representation'; const res = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', headers: headers, payload: JSON.stringify([row]), muteHttpExceptions: true }); const code = res.getResponseCode(); const body = res.getContentText(); if (code < 200 || code >= 300){ throw new Error('Supabase INSERT: ' + body); } const arr = body ? JSON.parse(body) : []; return (arr && arr[0]) ? arr[0] : row; } /** * Loghează un mesaj trimis către un client în bd_mesaje_clienti. * @param {object} clientRow - rând din bd_clienti_activi (așa cum vine din supaSelectAll_/supaGet_) * @param {string} tipMesaj - ex: 'NAR 1', 'NAR 2', 'ALOCARE AGENT' */ function mesaje_logClientMessage_(clientRow, tipMesaj) { if (!clientRow) return; const idClient = clientRow.id_unic; if (!idClient) return; // fără ID nu logăm const row = { id_client: Number(idClient), data_client: clientRow.data_client || null, // deja yyyy-MM-dd în Supabase nume_client: _s(clientRow.nume_complet || ''), telefon: _s(clientRow.telefon || ''), status: _s(clientRow.status || ''), dua: clientRow.dua || null, // yyyy-MM-dd status_secundar: _s(clientRow.status_secundar || ''), canal: _s(clientRow.canal || ''), tip_mesaj: String(tipMesaj || '').trim() }; try { supaInsertOne_(SUPA_MESAJ_CLIENTI, row); } catch (e) { // nu blocăm fluxul dacă logul eșuează Logger.log('mesaje_logClientMessage_ error: ' + (e && e.message)); } } /** ===== STORAGE GENERIC – mai multe bucket-uri (documente, training etc.) ===== **/ // Upload generic într-un bucket la path-ul objectPath (fără numele bucket-ului în față) function supaStorage_uploadToBucket(bucket, objectPath, bytes, mimeType) { objectPath = String(objectPath || '').replace(/^\/+/, ''); const fullPath = `${bucket}/${objectPath}`; const url = `${SUPA_STORAGE_URL}/object/${fullPath}`; const res = UrlFetchApp.fetch(url, { method: 'POST', contentType: mimeType || 'application/octet-stream', headers: { 'Authorization': 'Bearer ' + SUPA_KEY, 'x-upsert': 'true' }, payload: bytes, muteHttpExceptions: true }); const code = res.getResponseCode(); if (code >= 300) { throw new Error('Supabase Storage upload failed: ' + res.getContentText()); } return { ok: true, bucket, path: objectPath }; } // Creează URL semnat pentru un obiect dintr-un bucket privat function supaStorage_signedUrlFromBucket(bucket, objectPath, expiresSec) { objectPath = String(objectPath || '').replace(/^\/+/, ''); const fullPath = `${bucket}/${objectPath}`; const signUrl = `${SUPA_STORAGE_URL}/object/sign/${fullPath}`; const body = { expiresIn: expiresSec || 3600 }; const res = UrlFetchApp.fetch(signUrl, { method: 'POST', contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + SUPA_KEY }, payload: JSON.stringify(body), muteHttpExceptions: true }); const code = res.getResponseCode(); if (code >= 300) { throw new Error('Signed URL creation failed: ' + res.getContentText()); } const data = JSON.parse(res.getContentText()); const rel = (data.signedURL || data.signedUrl || '').replace(/^\/+/, ''); return `${SUPA_STORAGE_URL}/${rel}`; } // Ștergere obiect din bucket function supaStorage_deleteFromBucket(bucket, objectPath) { objectPath = String(objectPath || '').replace(/^\/+/, ''); const fullPath = `${bucket}/${objectPath}`; const url = `${SUPA_STORAGE_URL}/object/${fullPath}`; const res = UrlFetchApp.fetch(url, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + SUPA_KEY }, muteHttpExceptions: true }); const code = res.getResponseCode(); if (code >= 300) { throw new Error('Supabase Storage delete failed: ' + res.getContentText()); } return { ok:true }; } /** WRAPPERE pentru documentele de client (bucket: documente_clienti) – compatibile cu codul vechi **/ function supaStorage_upload(cnp, objectName, bytes, mimeType) { const objectPath = `${cnp}/${objectName}`; // ex: "1981111410011/fisier.pdf" supaStorage_uploadToBucket(SUPA_BUCKET, objectPath, bytes, mimeType); return { ok:true, path: objectPath }; // path fără numele bucket-ului } function supaStorage_signedUrl(path, expiresSec) { // path este fără numele bucketului (ex: "1981111410011/fisier.pdf") return supaStorage_signedUrlFromBucket(SUPA_BUCKET, path, expiresSec); } function supaDocuments_insert(meta) { supaUpsert_('bd_documente_clienti', [meta], 'id'); } // Curăță numele fișierului pentru Supabase Storage (fără diacritice & caractere dubioase) function _safeKeyName_(raw){ let s = String(raw || ''); // scoatem diacriticele (ĂÂÎȘȚ, etc.) try{ s = s.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } catch(e){ // dacă normalize nu există în runtime-ul Apps Script mai vechi, ignorăm și rămân diacriticele } // elimină caractere clar problematice pentru key s = s.replace(/[\\/:*?"<>|]+/g, ' '); // exact ca _sanitize, dar extins // comprimăm spațiile multiple s = s.replace(/\s+/g, ' ').trim(); return s; } /** Denumire standardizată pentru fișier */ function _buildDocName_(fd){ function _sanitize(s){ // păstrăm aceleași reguli, dar fără diacritice return _safeKeyName_(s); } function _toDmy(iso){ const m = String(iso || '').trim().match(/^(\d{4})-(\d{2})-(\d{2})$/); return m ? (m[3] + '.' + m[2] + '.' + m[1]) : _sanitize(iso); } const nume = _sanitize(fd.numeClient || ''); const cnp = _sanitize(fd.clientId || ''); const tip = _sanitize(fd.tipDocument || ''); const dmy = _toDmy(fd.dataIncarcare || ''); const ext = (/(\.[^.]+)$/.exec(fd.filename || '') || [])[1] || ''; // exemplu: "MARIUS VALENTIN BALAN - 1981111410011 - RAPORT ANAF - 19.11.2025" const base = [nume, cnp, tip, dmy].filter(Boolean).join(' - ') || 'document'; // încă o trecere prin _safeKeyName_ ca să fim 100% siguri return _safeKeyName_(base) + ext; } function uploadDocument(fd){ if (!fd || !fd.data || !fd.filename || !fd.clientId) { throw new Error('Payload incomplet pentru upload.'); } const cnp = String(fd.clientId || '').replace(/\D+/g, ''); if (!cnp) throw new Error('CNP lipsă pentru upload.'); const base64 = String(fd.data).split(',').pop(); const bytes = Utilities.base64Decode(base64); const mimeType = fd.mimeType || 'application/octet-stream'; // denumirea standardizată, deja curățată pentru Storage const fileName = _buildDocName_(fd); // 1) Upload în Supabase Storage (bucket/documente_clienti/CNP/filename) const up = supaStorage_upload(cnp, fileName, bytes, mimeType); // 2) Insert meta în bd_documente_clienti const meta = { id_unic: fd.idUnic || null, cnp: cnp, tip_document: fd.tipDocument || '', data_incarcare: fd.dataIncarcare || null, filename: fileName, path: up.path, // ex: "1981111410011/MARIUS VALENTIN BALAN - ....pdf" mime_type: mimeType, size_bytes: bytes.length, uploaded_by: fd.uploadedBy || '' }; supaDocuments_insert(meta); return { ok: true, path: up.path }; } function listDocuments(clientId){ clientId = String(clientId || '').replace(/\D+/g, ''); if (!clientId) return { ok: true, files: [] }; // helper definit în CodeIndex.gs return listDocumentsFromSupabase(clientId); } function listDocumentsFromSupabase(cnp) { cnp = String(cnp || '').replace(/\D+/g, ''); if (!cnp) return { ok: true, files: [] }; const qs = 'cnp=eq.' + encodeURIComponent(cnp); const docs = supaGet_('bd_documente_clienti', qs); const out = (docs || []).map(d => { // data_incarcare este DATE; o punem ca string simplu yyyy-MM-dd const dRaw = d.data_incarcare ? String(d.data_incarcare).substr(0, 10) : ''; return { id: d.id, tip: d.tip_document || '', data: dRaw, // generic dataIncarcare: dRaw, // compatibil cu UI (dacă folosești acest nume) name: d.filename || '', filename: d.filename || '', url: supaStorage_signedUrl(d.path, 3600), // URL semnat 1h size: d.size_bytes || null, path: d.path || '' }; }); return { ok: true, files: out }; } /** * Mapare rând din "De preluat in Supabase" -> obiect pentru bd_clienti_activi (Supabase). * * Coloane foaie (1-based): * 1: AGENT * 2: DATA CLIENT * 3: NUME COMPLET * 4: CNP * 5: VARSTA * 6: TELEFON * 7: E-MAIL CREAT * 8: JUDET CI/MUNCA * 9: VALOARE VENIT (NU se trimite – coloana valoare_venit e generată în Supabase) * 10: STATUS * 11: DUA * 12: STATUS SECUNDAR * 13: DVA * 14: DVA MAXIM * 15: RAPORT BC * 16: OBSERVATII ARATATE * 17: OBSERVATII * 18: CANAL * 19: CAMPANIE / RECOMANDARE * 20: ADSET / TEL. RECOMANDATOR * 21: AD * 22: INFO. SUPLIMENTARE * 23: VENIT DECLARAT * 24: VECHIME DECLARATA * 25: PRENUME * 26: NUME FAMILIE * 27: TELEFON 2 * 28: E-MAIL */ function _preluatRowToSupa_(row, idUnic){ const COL = { AGENT: 1, DATA_CLIENT: 2, NUME_COMPLET: 3, CNP: 4, VARSTA: 5, TELEFON: 6, EMAIL_CREAT: 7, JUDET_CI_MUNCA: 8, VALOARE_VENIT: 9, // NU o trimitem în Supabase STATUS: 10, DUA: 11, STATUS_SECUNDAR: 12, DVA: 13, DVA_MAXIM: 14, RAPORT_BC: 15, OBS_ARATATE: 16, OBS: 17, CANAL: 18, CAMPANIE: 19, ADSET_TEL: 20, AD: 21, INFO_SUPL: 22, VENIT_DECLARAT: 23, VECHIME_DECLARATA: 24, PRENUME: 25, NUME_FAMILIE: 26, TELEFON2: 27, EMAIL: 28 }; // text simplu pentru câmpuri non-dată const s = v => { if (v instanceof Date) { return Utilities.formatDate( v, Session.getScriptTimeZone() || 'Europe/Bucharest', 'dd/MM/yyyy' ); } const str = (v == null ? '' : String(v)).trim(); if (!str) return ''; const m = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); if (m) { return ('0'+m[1]).slice(-2) + '/' + ('0'+m[2]).slice(-2) + '/' + m[3]; } return str; }; const dig = v => s(v).replace(/\D+/g,'') || null; // dd.MM.yyyy / dd/MM/yyyy / yyyy-MM-dd -> ISO yyyy-MM-dd (sau null) const dIso = v => { const iso = _toIsoDateFromRo_(v); return iso || null; }; const dataClient = row[COL.DATA_CLIENT-1]; const dua = row[COL.DUA-1]; const dva = row[COL.DVA-1]; const dvm = row[COL.DVA_MAXIM-1]; const out = { id_unic: String(idUnic), agent: s(row[COL.AGENT-1]), // coloane DATE în Supabase -> trimitem ISO data_client: dIso(dataClient), nume_complet: s(row[COL.NUME_COMPLET-1]), cnp: dig(row[COL.CNP-1]), telefon: s(row[COL.TELEFON-1]), e_mail_creat: s(row[COL.EMAIL_CREAT-1]), judet_ci_munca: s(row[COL.JUDET_CI_MUNCA-1]), status: s(row[COL.STATUS-1]), dua: dIso(dua), status_secundar: s(row[COL.STATUS_SECUNDAR-1]), dva: dIso(dva), dva_maxim: dIso(dvm), raport_bc: s(row[COL.RAPORT_BC-1]), observatii_aratate: s(row[COL.OBS_ARATATE-1]), observatii: s(row[COL.OBS-1]), canal: s(row[COL.CANAL-1]), campanie_recomandator: s(row[COL.CAMPANIE-1]), adset_tel_recomandator: s(row[COL.ADSET_TEL-1]), ad: s(row[COL.AD-1]), info_suplimentare: s(row[COL.INFO_SUPL-1]), venit_declarat: s(row[COL.VENIT_DECLARAT-1]), vechime_declarata: s(row[COL.VECHIME_DECLARATA-1]), prenume: s(row[COL.PRENUME-1]), nume_familie: s(row[COL.NUME_FAMILIE-1]), telefon_2: s(row[COL.TELEFON2-1]), e_mail: s(row[COL.EMAIL-1]) }; Object.keys(out).forEach(k => { if (out[k] === '') out[k] = null; }); return out; } function ca_importDePreluatInSupabase_manual(){ // forțăm rularea, ignorând fereastra orară return ca_importDePreluatInSupabase(null, true); } /** * IMPORTĂ ÎN GRUP din foaia "De preluat in Supabase" în bd_clienti_activi (Supabase). * * Protecții: * - rulează doar Luni–Vineri, între 10:00 și 16:00 (ultima rulare la 16:00) * - dacă în C2 nu există text cu lungime > 5 → nu importă nimic * * Poate fi apelată: * - din trigger time-driven (fără parametru) * - din UI: ca_importDePreluatInSupabase(__token) */ function ca_importDePreluatInSupabase(token, force){ // 0) Fereastra de lucru if (!force && !_isBusinessWindowCA_()){ Logger.log('ca_importDePreluatInSupabase: oprit – în afara intervalului Lu–Vi 10:00–16:00.'); return { ok:true, imported:0, skipped:'outside_business_hours' }; } // 1) Validare sesiune doar dacă vine token (din UI) if (token) { try { _getSession(token); } catch(e){ throw new Error('Sesiune invalidă sau expirată.'); } } const ss = SpreadsheetApp.getActive(); const sh = ss.getSheetByName('De preluat in Supabase'); if (!sh) throw new Error('Nu există foaia "De preluat in Supabase" în acest fișier.'); // 2) „Trigger” în C2 – dacă textul are lungime <= 5 → nu facem nimic const trigText = String(sh.getRange('C2').getDisplayValue() || '').trim(); if (trigText.length <= 5){ Logger.log('ca_importDePreluatInSupabase: oprit – C2 are text prea scurt: "' + trigText + '".'); return { ok:true, imported:0, skipped:'no_trigger_flag' }; } // 3) Citim rândurile cu date (A2:AB) const START_ROW = 2; // antet pe rândul 1 const lastRow = sh.getLastRow(); const lastCol = 28; // A..AB if (lastRow < START_ROW){ Logger.log('ca_importDePreluatInSupabase: niciun rând de procesat (sub rândul 1).'); return { ok:true, imported:0 }; } const nRows = lastRow - START_ROW + 1; const data = sh.getRange(START_ROW, 1, nRows, lastCol).getValues(); // [ [A..AB], ... ] const supaRows = []; for (let i = 0; i < data.length; i++){ const row = data[i]; // rând complet gol? îl sărim const hasData = row.some(c => String(c || '').trim() !== ''); if (!hasData) continue; // generăm ID UNIC sigur la concurență const idUnic = _nextIdUnicSupabase_(); const rec = _preluatRowToSupa_(row, idUnic); supaRows.push(rec); } if (!supaRows.length){ Logger.log('ca_importDePreluatInSupabase: nu s-au găsit rânduri cu date.'); return { ok:true, imported:0 }; } // 4) UPSERT în batch-uri pentru Supabase const CHUNK = 300; for (let i = 0; i < supaRows.length; i += CHUNK){ const slice = supaRows.slice(i, i + CHUNK); supaUpsert_(SUPA_TABLE, slice, 'id_unic'); } // 5) Golește conținutul rândurilor 2..lastRow (A:AB), păstrând formatarea sh.getRange(START_ROW, 1, nRows, lastCol).clearContent(); Logger.log('ca_importDePreluatInSupabase: ' + supaRows.length + ' clienți importați în bd_clienti_activi.'); return { ok:true, imported: supaRows.length }; } /** * Fereastră de lucru pentru import CA: * - Luni–Vineri * - între 10:00 și 16:00 (16:00 inclusiv) */ function _isBusinessWindowCA_(){ var tz = 'Europe/Bucharest'; var now = new Date(); var day = Utilities.formatDate(now, tz, 'EEE'); // Mon..Sun var isWeekday = ['Mon','Tue','Wed','Thu','Fri'].indexOf(day) !== -1; if (!isWeekday) return false; var hh = parseInt(Utilities.formatDate(now, tz, 'H'), 10); // 0..23 var mm = parseInt(Utilities.formatDate(now, tz, 'm'), 10); // 0..59 var mins = hh * 60 + mm; // 10:00 (600) – 16:00 (960) inclusiv return (mins >= 10*60) && (mins <= 16*60); } /** * Salvează o procesare în Supabase: * - 1 rând în bd_procesari_hdr * - N rânduri în bd_procesari_banca * * @param {string} token – token sesiune CRM * @param {string|number} idUnic – ID UNIC client * @param {object} hdr – { consilier, dataActualizareIso, status, numeClient, cnp, infoClient } * @param {Array} banci – array cu { banca, suma, info, status, motiv } */ function procesari_saveSupabase(token, idUnic, hdr, banci){ _getSession(token); // doar validează sesiunea idUnic = String(idUnic || '').trim(); if (!idUnic) throw new Error('ID UNIC lipsă.'); hdr = hdr || {}; banci = banci || []; // 1) HEADER const rowHdr = { id_unic: Number(idUnic), consilier: String(hdr.consilier || '').trim(), data_actualizare: hdr.dataActualizareIso || new Date().toISOString().slice(0,10), // yyyy-MM-dd status_procesare: String(hdr.status || 'INITIATA').trim(), nume_client: String(hdr.numeClient || '').trim(), cnp_client: String(hdr.cnp || '').trim(), info_client: String(hdr.infoClient || '').trim(), // 🔹 nou: motiv la nivel de dosar motiv_procesare: String(hdr.motivProcesare || '').trim() || null }; // folosim INSERT simplu, ca să obținem ID-ul header-ului const insertedHdr = supaInsertOne_(SUPA_PROC_HDR, rowHdr); const hdrId = insertedHdr.id; if (!hdrId) throw new Error('Nu am primit id pentru bd_procesari_hdr.'); // 2) BĂNCI – construim rândurile fiice const rowsB = []; banci.forEach((b, idx) => { if (!b || !b.banca) return; // ignorăm liniile „goale” rowsB.push({ hdr_id: hdrId, banca: String(b.banca || '').trim(), suma_estimata: b.suma != null ? Number(b.suma) : null, info: String(b.info || '').trim(), status: String(b.status|| '').trim(), motiv_status: String(b.motiv || '').trim(), ord: (idx+1) }); }); if (rowsB.length){ // INSERT bulk, nu avem nevoie de upsert aici const url = SUPA_URL + '/rest/v1/' + SUPA_PROC_BANCA + '?select=*'; const res = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', headers: supaHeaders_(false), payload: JSON.stringify(rowsB), muteHttpExceptions: true }); if (res.getResponseCode() >= 300){ throw new Error('Supabase INSERT banci: ' + res.getContentText()); } } return { ok:true, hdrId, banks: rowsB.length }; } /** * Listează procesările din Supabase și le transformă în formatul tabelului UI. * * PROCIDX (JS) = { * NR:0, CONS:1, DATEH:2, STPROC:3, NUME:4, CNP:5, INFOCLI:6, * BANCA:7, SUMA:8, INFO:9, STBANK:10, MOTIV:11, * DATEISO:12, ROW:13, MOTIVPROC:14, ID_UNIC:15 * } */ function procesari_list(token){ const sess = _getSession(token); // validare și user/rol const role = String(sess.role || '').toLowerCase(); const isUtil = (role === 'utilizator'); const me = String(sess.user || '').trim(); // 1) header-ele (bd_procesari_hdr) const hdrConds = []; if (isUtil && me){ hdrConds.push('consilier=eq.' + encodeURIComponent(me)); } const hdrQs = 'select=' + encodeURIComponent('id,id_unic,consilier,data_actualizare,status_procesare,nume_client,cnp_client,info_client,motiv_procesare') + (hdrConds.length ? '&' + hdrConds.join('&') : '') + '&order=' + encodeURIComponent('data_actualizare') + '.desc,' + encodeURIComponent('id') + '.desc'; const hdrRows = supaSelectAll_(SUPA_PROC_HDR, hdrQs, 1000) || []; if (!hdrRows.length) return { ok:true, rows:[] }; // 2) bănci (bd_procesari_banca) doar pentru header-ele de mai sus const hdrIds = hdrRows.map(h => h.id); let bancaRows = []; if (hdrIds.length){ const inList = hdrIds.join(','); const bancaQs = 'select=' + encodeURIComponent('id,hdr_id,banca,suma_estimata,info,status,motiv_status,ord') + '&hdr_id=in.(' + encodeURIComponent(inList) + ')' + '&order=' + encodeURIComponent('hdr_id') + '.asc,' + encodeURIComponent('ord') + '.asc'; bancaRows = supaSelectAll_(SUPA_PROC_BANCA, bancaQs, 2000) || []; } const byHdr = {}; bancaRows.forEach(b => { const hid = b.hdr_id; if (!byHdr[hid]) byHdr[hid] = []; byHdr[hid].push(b); }); const tz = Session.getScriptTimeZone(); const rows = []; let nr = 0; hdrRows.forEach(h => { nr++; const iso = h.data_actualizare || null; const dataRo = iso ? Utilities.formatDate(new Date(iso), tz, 'dd.MM.yyyy') : ''; // rând HEADER rows.push([ nr, // 0 NR h.consilier || '', // 1 CONS dataRo, // 2 DATEH (text) h.status_procesare || '', // 3 STPROC h.nume_client || '', // 4 NUME h.cnp_client || '', // 5 CNP h.info_client || '', // 6 INFO CLIENT '', '', '', '', '', // 7..11 BANCA..MOTIV STATUS (goale pe header) iso, // 12 DATEISO h.id, // 13 ROW = id header h.motiv_procesare || '', // 14 MOTIV PROCESARE (header) h.id_unic || null // 15 ID_UNIC client (din bd_clienti_activi) ]); // rânduri BANCĂ aferente (byHdr[h.id] || []).forEach(b => { rows.push([ '', '', '', '', '', '', // 0..5 – goale (vin din header) '', // 6 – INFO CLIENT (gol) b.banca || '', // 7 – BANCA b.suma_estimata != null ? b.suma_estimata : '', // 8 – SUMA EST. b.info || '', // 9 – INFO b.status || '', // 10 – STBANK b.motiv_status || '', // 11 – MOTIV STATUS null, // 12 – DATEISO b.id, // 13 – ROW = id rând bancă '', // 14 – MOTIV PROCESARE (doar headerul îl folosește) null // 15 – ID_UNIC (nu ne trebuie pe rând bancă) ]); }); }); return { ok:true, rows:rows }; } /** Patch pentru header (bd_procesari_hdr) – data_actualizare / status_procesare */ /** Patch pentru header (bd_procesari_hdr) – data_actualizare / status_procesare */ function procesari_updateHeader(token, hdrId, payload){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase(); const can = ['manager','administrator','controlor'].includes(role); if (!can) { throw new Error('Nu aveți drepturi să modificați statusul procesării.'); } const id = Number(hdrId || 0); if (!id) { throw new Error('ID header invalid.'); } const patch = {}; if (payload && Object.prototype.hasOwnProperty.call(payload, 'dateIso')) { patch.data_actualizare = payload.dateIso || null; } if(payload&&Object.prototype.hasOwnProperty.call(payload,'statusProc')){ const sp=String(payload.statusProc||'').trim().toUpperCase(); if(sp==='RESPINSA'&&role!=='administrator') throw new Error('Doar Administratorul poate seta statusul procesării la RESPINSA.'); patch.status_procesare=sp||null; } // 🔹 nou: motiv_procesare if (payload && Object.prototype.hasOwnProperty.call(payload, 'motivProcesare')) { patch.motiv_procesare = String(payload.motivProcesare || '').trim() || null; } if (!Object.keys(patch).length) { return { ok: true }; } // PATCH direct, NU mai folosim upsert (ca să nu încerce să insereze un rând nou) const url = SUPA_URL + '/rest/v1/' + SUPA_PROC_HDR + '?id=eq.' + encodeURIComponent(id); const res = UrlFetchApp.fetch(url, { method: 'patch', contentType: 'application/json', headers: supaHeaders_(false), payload: JSON.stringify(patch), muteHttpExceptions: true }); const code = res.getResponseCode(); if (code >= 300) { throw new Error('Supabase UPDATE header: ' + res.getContentText()); } return { ok: true }; } /** Patch pentru liniile bancă (bd_procesari_banca) – STATUS/MOTIV */ function procesari_updateBank(token, bankId, patch){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase(); const canEditStatus = ['manager','administrator','controlor'].includes(role); const id = Number(bankId || 0); if (!id) throw new Error('ID bancă invalid.'); patch = patch || {}; const body = {}; // STATUS bancă (INICIAT / DEPUS / COMPLETARI / APROBAT / RESPINS / TRAS) if (Object.prototype.hasOwnProperty.call(patch, 'status')){ if (!canEditStatus) { throw new Error('Nu aveți drepturi să modificați STATUS pe bancă.'); } body.status = String(patch.status || '').trim() || null; } // MOTIV STATUS if (Object.prototype.hasOwnProperty.call(patch, 'motiv')){ body.motiv_status = String(patch.motiv || '').trim() || null; } // nimic de actualizat if (!Object.keys(body).length) return { ok:true }; // PATCH direct în bd_procesari_banca, fără upsert și fără updated_at const url = SUPA_URL + '/rest/v1/' + SUPA_PROC_BANCA + '?id=eq.' + encodeURIComponent(id); const res = UrlFetchApp.fetch(url, { method: 'patch', contentType: 'application/json', headers: supaHeaders_(false), payload: JSON.stringify(body), muteHttpExceptions: true }); const code = res.getResponseCode(); if (code >= 300){ throw new Error('Supabase UPDATE bank: ' + res.getContentText()); } return { ok:true }; } /** ===== Rapoarte Sales – Statistici Campanie (CANAL = "CAMPANIE SMC") ===== * Filtrează după data_client între [fromIso, toIso] (inclusiv). * Pentru Utilizator simplu → doar clienții agentului; pentru roluri superioare → toți. * * Return: * { * ok:true, * from:"yyyy-MM-dd", * to:"yyyy-MM-dd", * total:, * counts:{ ... }, * perAgents:[ ... ], * byDay:[ { iso,label,count,avgPerAgent }, ... ], * agentsCount: * } */ function sr_stats_campanie(token, fromIso, toIso){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase(); const isUtil = (role === 'administrator' ? false : (role === 'utilizator')); const me = _s(sess.user).trim(); // ===== nr. agenți activi (CONSILIER + STATUS HR = ACTIV) ===== let agentsCount = 0; try { const agRes = calls_getActiveAgents(); if (agRes && agRes.ok && Array.isArray(agRes.agenti)) { agentsCount = agRes.agenti.length; } } catch(e){ agentsCount = 0; } // ===== filtrare bd_clienti_activi doar pe CAMPANIE SMC ===== const conds = [ 'canal=eq.' + encodeURIComponent('CAMPANIE SMC') ]; if (fromIso) conds.push('data_client=gte.' + encodeURIComponent(fromIso)); if (toIso) conds.push('data_client=lte.' + encodeURIComponent(toIso)); if (isUtil && me){ conds.push('agent=eq.' + encodeURIComponent(me)); } const selectCA = 'id_unic,agent,data_client,' + 'status,status_secundar,canal'; const qsCA = 'select=' + encodeURIComponent(selectCA) + (conds.length ? '&' + conds.join('&') : ''); const caRows = supaSelectAll_(SUPA_TABLE, qsCA, 1000) || []; const up = s => String(s || '').trim().toUpperCase(); const norm = s => _s(s).trim(); function newCounters(){ return { total: 0, in_contact: 0, in_lucru: 0, prospect: 0, finantat: 0, respins_total: 0, bc_neeligibil: 0, blacklist: 0, cl_refuza: 0, nar_final: 0, nok_precalificare: 0, deja_existent: 0, telefon_invalid: 0 }; } function addCounters(dst, src){ for (var k in src){ if (Object.prototype.hasOwnProperty.call(src, k)){ dst[k] = (dst[k] || 0) + (src[k] || 0); } } } const globalCounters = newCounters(); const byAgent = Object.create(null); // dayMap – câți clienți ALOCAȚI CĂTRE FO (bd_clienti_activi, CAMPANIE SMC) pe fiecare zi const dayMap = Object.create(null); const tz = Session.getScriptTimeZone(); const todayIso = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd'); caRows.forEach(r => { const agentKey = norm(r.agent) || '(NEALOCAT)'; const st = up(r.status); const s2 = up(r.status_secundar); const isoDay = (r.data_client || '').trim(); if (isoDay){ dayMap[isoDay] = (dayMap[isoDay] || 0) + 1; } const c = newCounters(); c.total = 1; if (st === 'ALOCAT' || st === 'CONTACTAT') { c.in_contact++; } if (st === 'IN LUCRU' || st === 'PROGRAMAT') { c.in_lucru++; } if (st === 'PROSPECT') { c.prospect++; } if (st === 'FINANTAT') { c.finantat++; } if (st === 'RESPINS') { c.respins_total++; if (s2 === 'BC NEELIGIBIL') c.bc_neeligibil++; else if (s2 === 'BLACKLIST') c.blacklist++; else if (s2 === 'CL. REFUZA COLABORAREA') c.cl_refuza++; else if (s2 === 'NAR FINAL') c.nar_final++; else if (s2 === 'NOK PRECALIFICARE') c.nok_precalificare++; else if (s2 === 'DEJA EXISTENT') c.deja_existent++; else if (s2 === 'TELEFON INVALID') c.telefon_invalid++; } addCounters(globalCounters, c); if (!byAgent[agentKey]) { byAgent[agentKey] = newCounters(); } addCounters(byAgent[agentKey], c); }); // ===== statistici pe agent (tabelul din mijloc) ===== const perAgents = Object.keys(byAgent) .sort((a,b) => a.localeCompare(b,'ro')) .map(agent => { const cnt = byAgent[agent]; return { agent: agent, total: cnt.total, in_contact: cnt.in_contact, in_lucru: cnt.in_lucru, prospect: cnt.prospect, finantat: cnt.finantat, respins_total: cnt.respins_total, bc_neeligibil: cnt.bc_neeligibil, blacklist: cnt.blacklist, cl_refuza: cnt.cl_refuza, nar_final: cnt.nar_final, nok_precalificare: cnt.nok_precalificare, deja_existent: cnt.deja_existent, telefon_invalid: cnt.telefon_invalid }; }); // ====== MAP-URI PENTRU ELIGIBILI / NEELIGIBILI (bd_eligibili / bd_neeligibili) ====== const eligByDay = Object.create(null); const reapByDay = Object.create(null); const neByDay = Object.create(null); if (fromIso && toIso){ // ELIGIBILI const condsE = []; if (fromIso) condsE.push('data_client=gte.' + encodeURIComponent(fromIso)); if (toIso) condsE.push('data_client=lte.' + encodeURIComponent(toIso)); const selE = 'data_client,de_preluat'; const qsE = 'select=' + encodeURIComponent(selE) + (condsE.length ? '&' + condsE.join('&') : ''); const eRows = supaSelectAll_(SUPA_ELIGIBILI, qsE, 1000) || []; eRows.forEach(r => { const iso = (r.data_client || '').trim(); if (!iso) return; eligByDay[iso] = (eligByDay[iso] || 0) + 1; if (String(r.de_preluat || '').trim().toUpperCase() === 'REAPLICANT'){ reapByDay[iso] = (reapByDay[iso] || 0) + 1; } }); // NEELIGIBILI const condsN = []; if (fromIso) condsN.push('data_client=gte.' + encodeURIComponent(fromIso)); if (toIso) condsN.push('data_client=lte.' + encodeURIComponent(toIso)); const selN = 'data_client'; const qsN = 'select=' + encodeURIComponent(selN) + (condsN.length ? '&' + condsN.join('&') : ''); const nRows = supaSelectAll_(SUPA_TABLE_NEELIGIBILI, qsN, 1000) || []; nRows.forEach(r => { const iso = (r.data_client || '').trim(); if (!iso) return; neByDay[iso] = (neByDay[iso] || 0) + 1; }); } // ===== breakdown pe zile (nou: multe coloane) ===== const byDay = []; if (fromIso && toIso){ const endIso = (toIso <= todayIso) ? toIso : todayIso; let cur = new Date(fromIso + 'T00:00:00'); const end = new Date(endIso + 'T00:00:00'); while (cur <= end){ const iso = Utilities.formatDate(cur, tz, 'yyyy-MM-dd'); const label = Utilities.formatDate(cur, tz, 'dd/MM/yyyy'); const foCnt = dayMap[iso] || 0; // ALOCAȚI CĂTRE FO const neCnt = neByDay[iso] || 0; // NEELIGIBILI const elCnt = eligByDay[iso] || 0; // ELIGIBILI const reapCnt = reapByDay[iso] || 0; // REAPLICANȚI const elFinal = elCnt - reapCnt; // ELIGIBILI FINAL const totalAp = neCnt + elCnt; // TOTAL APLICAȚII const denom = agentsCount || (perAgents && perAgents.length) || 0; const avg = denom ? (foCnt / denom) : 0; byDay.push({ iso: iso, label: label, totalApps: totalAp, neelig: neCnt, elig: elCnt, reap: reapCnt, eligFinal: elFinal, foAlloc: foCnt, count: foCnt, avgPerAgent:avg }); cur.setDate(cur.getDate() + 1); } } return { ok: true, from: fromIso || '', to: toIso || '', total: globalCounters.total, counts: { in_contact: globalCounters.in_contact, in_lucru: globalCounters.in_lucru, prospect: globalCounters.prospect, finantat: globalCounters.finantat, respins_total: globalCounters.respins_total, bc_neeligibil: globalCounters.bc_neeligibil, blacklist: globalCounters.blacklist, cl_refuza: globalCounters.cl_refuza, nar_final: globalCounters.nar_final, nok_precalificare: globalCounters.nok_precalificare, deja_existent: globalCounters.deja_existent, telefon_invalid: globalCounters.telefon_invalid }, perAgents: perAgents, byDay: byDay, agentsCount: agentsCount }; } function gest_getDataClient(token, idUnic){ _getSession(token); idUnic = String(idUnic || '').trim(); if (!idUnic) return { ok:false, msg:'ID UNIC lipsă.' }; var rec = supaOneById_(idUnic, 'data_client'); var txt = rec.data_client ? String(rec.data_client) : ''; var iso = _toIsoDateFromRo_(txt) || ''; // dacă e deja yyyy-MM-dd, o lasă așa return { ok: true, data_client: txt, data_client_iso: iso // yyyy-MM-dd sau '' }; } /** ===== LISTĂ APLICANȚI ELIGIBILI (ADMIN ONLY) ===== */ function eligibili_list(token){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase(); if (role !== 'administrator') { return { ok:false, msg:'Doar Administratorul poate accesa „Lista Aplicanți”.' }; } const select = 'id_unic,data_client,nume_client,telefon,email,' + 'varsta,tip_venit,valoare_venit,judet,vechime,popriri,' + 'zona,eligibil_fo,agent,status,dua,status_secundar,dva,' + 'preluat,de_preluat'; const qs = 'select=' + encodeURIComponent(select) + '&order=' + encodeURIComponent('data_client') + '.desc'; const rows = supaSelectAll_(SUPA_ELIGIBILI, qs, 1000) || []; return { ok:true, rows: rows }; } /** ===== LISTĂ REAPLICANȚI (ADMIN ONLY) ===== */ function eligibili_listReap(token){ const sess = _getSession(token); const role = String(sess.role || '').toLowerCase(); if (role !== 'administrator') { return { ok:false, msg:'Doar Administratorul poate accesa „Reaplicanți”.' }; } const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const now = new Date(); // 0 = duminică, 1 = luni, ... 6 = sâmbătă let dow = now.getDay(); // 0..6 if (dow === 0) dow = 7; // 7 = duminică (convenția ta 1..7) function addDays(d, n){ const x = new Date(d.getTime()); x.setDate(x.getDate() + n); return x; } const toIso = d => Utilities.formatDate(d, tz, 'yyyy-MM-dd'); let zile = []; if (dow === 1) { // luni → vineri, sâmbătă, duminică zile = [ addDays(now,-1), addDays(now,-2), addDays(now,-3) ]; } else if (dow >= 2 && dow <= 5){ // marți–vineri → doar ieri zile = [ addDays(now,-1) ]; } else { // weekend – nu afișăm nimic zile = []; } if (!zile.length){ return { ok:true, rows:[] }; } const isoDays = zile.map(toIso); let conds = ['de_preluat=eq.REAPLICANT']; if (isoDays.length === 1){ conds.push('data_client=eq.' + encodeURIComponent(isoDays[0])); } else { conds.push('data_client=in.(' + isoDays.map(encodeURIComponent).join(',') + ')'); } const select = 'id_unic,data_client,nume_client,telefon,email,' + 'varsta,tip_venit,valoare_venit,judet,vechime,popriri,' + 'zona,eligibil_fo,agent,status,dua,status_secundar,dva'; const qs = 'select=' + encodeURIComponent(select) + '&' + conds.join('&') + '&order=' + encodeURIComponent('data_client') + '.desc'; const rows = supaSelectAll_(SUPA_ELIGIBILI, qs, 1000) || []; return { ok:true, rows, days: isoDays }; } /** ===== REAPLICANȚI – TRIMITERE PE E-MAIL (BUTON + TRIGGER 09:00) ===== * UI (buton): google.script.run.eligibili_emailReap(__token) * Trigger: eligibili_emailReap_cron(e) * Ambele folosesc: _eligibili_emailReap_send_() */ // 1) BUTON (UI) – rămâne valabil function eligibili_emailReap(token){ const sess = _getSession(token); if (String(sess.role || '').toLowerCase() !== 'administrator'){ return { ok:false, msg:'Doar Administratorul poate trimite acest raport.' }; } return _eligibili_emailReap_send_(); } // 2) SETUP TRIGGER – rulezi o singură dată manual din editor function eligibili_emailReap_setupDailyTrigger_0900(){ const handler = 'eligibili_emailReap_cron'; // evităm duplicate ScriptApp.getProjectTriggers().forEach(t => { if (t.getHandlerFunction && t.getHandlerFunction() === handler){ ScriptApp.deleteTrigger(t); } }); ScriptApp.newTrigger(handler) .timeBased() .atHour(9) .nearMinute(0) .everyDays(1) .create(); return { ok:true, msg:'Trigger „Reaplicanți” setat zilnic la 09:00.' }; } // (opțional) remove trigger function eligibili_emailReap_removeDailyTrigger_0900(){ const handler = 'eligibili_emailReap_cron'; let removed = 0; ScriptApp.getProjectTriggers().forEach(t => { if (t.getHandlerFunction && t.getHandlerFunction() === handler){ ScriptApp.deleteTrigger(t); removed++; } }); return { ok:true, removed }; } // 3) HANDLER TRIGGER (fără token) function eligibili_emailReap_cron(e){ // securitate: rulează doar dacă e trigger real if (!_isRealTimeTriggerEvent_(e, 'eligibili_emailReap_cron')) return { ok:false, msg:'FORBIDDEN' }; const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const dow = Utilities.formatDate(new Date(), tz, 'EEE'); // Mon..Sun if (dow === 'Sat' || dow === 'Sun') return { ok:true, skipped:'weekend' }; // anti-dublu-send în aceeași zi const todayIso = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd'); const props = PropertiesService.getScriptProperties(); const SENT_KEY = 'reaplicanti:sent:' + todayIso; if (props.getProperty(SENT_KEY) === 'DA') return { ok:true, skipped:'already_sent', date: todayIso }; const res = _eligibili_emailReap_send_(); if (res && res.ok && Number(res.sentTo || 0) > 0){ props.setProperty(SENT_KEY, 'DA'); } return res; } // 4) Protecție trigger real function _isRealTimeTriggerEvent_(e, handlerName){ try{ const uid = e && e.triggerUid ? String(e.triggerUid) : ''; if (!uid) return false; return ScriptApp.getProjectTriggers().some(t => typeof t.getUniqueId === 'function' && t.getUniqueId() === uid && t.getHandlerFunction && t.getHandlerFunction() === handlerName ); }catch(_){ return false; } } // 5) LOGICA EFECTIVĂ DE TRIMITERE (folosită de buton + trigger) function _eligibili_emailReap_send_(){ const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const now = new Date(); // Aceleași reguli ca eligibili_listReap: // luni -> vineri+sâmbătă+duminică; marți–vineri -> doar ieri; weekend -> nimic let dow = now.getDay(); if (dow === 0) dow = 7; // 1..7 (7=duminică) const addDays = (d,n)=>{ const x=new Date(d.getTime()); x.setDate(x.getDate()+n); return x; }; const toIso = d => Utilities.formatDate(d, tz, 'yyyy-MM-dd'); let isoDays = []; if (dow === 1) isoDays = [toIso(addDays(now,-1)), toIso(addDays(now,-2)), toIso(addDays(now,-3))]; else if (dow >= 2 && dow <= 5) isoDays = [toIso(addDays(now,-1))]; else return { ok:true, skipped:'weekend', sentTo:0, rows:0, days:[] }; // Reaplicanți din bd_eligibili const conds = ['de_preluat=eq.REAPLICANT']; if (isoDays.length === 1){ conds.push('data_client=eq.' + encodeURIComponent(isoDays[0])); } else { conds.push('data_client=in.(' + isoDays.map(encodeURIComponent).join(',') + ')'); } const select = 'id_unic,data_client,nume_client,telefon,email,' + 'varsta,tip_venit,valoare_venit,judet,vechime,popriri,' + 'zona,eligibil_fo,agent,status,dua,status_secundar,dva'; const qs = 'select=' + encodeURIComponent(select) + '&' + conds.join('&') + '&order=' + encodeURIComponent('data_client') + '.desc'; const rows = supaSelectAll_(SUPA_ELIGIBILI, qs, 1000) || []; if (!rows.length) return { ok:true, skipped:'no_rows', sentTo:0, rows:0, days: isoDays }; // Destinatari (ca în funcția ta actuală): DIRECTOR / MANAGER DEPARTAMENT / CONSILIER, STATUS HR = ACTIV const colTip = encodeURIComponent('"TIP ANGAJAT"'); const colHR = encodeURIComponent('"STATUS HR"'); const inVals = ['DIRECTOR','MANAGER DEPARTAMENT','CONSILIER'].map(encodeURIComponent).join(','); const qsUsers = 'select=' + encodeURIComponent('"E-MAIL","TIP ANGAJAT","STATUS HR"') + '&' + colTip + '=in.(' + inVals + ')' + '&' + colHR + '=eq.' + encodeURIComponent('ACTIV'); const users = supaSelectAll_(SUPA_USERS_TABLE, qsUsers, 500) || []; const emails = Array.from(new Set(users.map(u => String(u['E-MAIL'] || '').trim()).filter(Boolean))); if (!emails.length) return { ok:false, msg:'Nu am găsit destinatari în bd_useri_si_parole.' }; const todayLabel = Utilities.formatDate(new Date(), tz, 'dd.MM.yyyy'); const subject = 'Reaplicanti ' + todayLabel; const esc = s => String(s==null?'':s).replace(/[&<>"]/g, m=>({'&':'&','<':'<','>':'>'}[m])); const css = [ 'table{border-collapse:collapse;width:100%;font:11px/1.35 Arial,sans-serif}', 'th,td{border:1px solid #dbe3f3;padding:4px 6px;vertical-align:top;white-space:pre-wrap;word-break:break-word}', 'thead th{background:#26428b;color:#fff;text-align:center;font-weight:700;font-size:12px}', 'tbody tr:nth-child(odd){background:#f8fbff}' ].join(''); function dateClientBlock(r){ return [ 'Varsta: ' + (r.varsta || ''), 'Tip Venit: ' + (r.tip_venit || ''), 'Valoare Venit: ' + (r.valoare_venit || ''), 'Judet: ' + (r.judet || ''), 'Vechime: ' + (r.vechime || ''), 'Popriri: ' + (r.popriri || '') ].join('\n'); } const head = [ 'DATA CLIENT','NUME CLIENT','TELEFON','E-MAIL', 'DATE CLIENT','ZONA','ELIGIBIL FO','AGENT', 'STATUS','DUA','STATUS SECUNDAR','DVA' ]; const headHtml = '' + head.map(h => ''+esc(h)+'').join('') + ''; const bodyHtml = rows.map(r => ( '' + ''+esc(r.data_client || '')+'' + ''+esc(r.nume_client || '')+'' + ''+esc(r.telefon || '')+'' + ''+esc(r.email || '')+'' + ''+esc(dateClientBlock(r))+'' + ''+esc(r.zona || '')+'' + ''+esc(r.eligibil_fo || '')+'' + ''+esc(r.agent || '')+'' + ''+esc(r.status || '')+'' + ''+esc(r.dua || '')+'' + ''+esc(r.status_secundar || '')+'' + ''+esc(r.dva || '')+'' + '' )).join(''); const html = '' + '

Lista reaplicanților ('+todayLabel+')

' + '
Zile incluse: ' + esc(isoDays.join(', ')) + '
' + ''+headHtml+''+bodyHtml+'
'; const text = 'Lista reaplicanților la data de ' + todayLabel + ' conține ' + rows.length + ' înregistrări. Deschide emailul în format HTML pentru tabel.'; const to = emails[0]; const cc = emails.slice(1).join(','); MailApp.sendEmail({ to, cc, subject, body:text, htmlBody:html, noReply:true }); return { ok:true, sentTo: emails.length, rows: rows.length, days: isoDays }; } /** * Creează adresa de e-mail pentru un client, folosind câmpul e_mail_creat * din bd_clienti_activi. Dacă nu există, întoarce mesajul cerut. * * Folosit din popup-ul "Gestionare client" (Main.html). */ function gest_createEmailForClient(token,idUnic){ const sess=_getSession(token); idUnic=String(idUnic||'').trim(); if(!idUnic) return{ok:false,statusMessage:'ID UNIC lipsă.'}; const rec=supaOneById_(idUnic,'e_mail_creat,cnp,nume_complet,agent,status_secundar,raport_bc'); const email=rec&&rec.e_mail_creat?String(rec.e_mail_creat).trim():''; if(!email) return{ok:false,statusMessage:'CLIENTUL NU ARE ADRESA DE E-MAIL CREATA ANTERIOR! PENTRU CREAREA UNEI ADRESE NOI TREBUIE SA EDITEZI CLIENTUL STANDARD!'}; const r=createEmailViaCPanel_(email); // ✅ doar dacă s-a CREAT acum (nu “EXISTA DEJA”) -> scriem în bd_adrese_email dacă lipsește if(r&&r.ok&&r.created){ try{ const hit=supaGet_(SUPA_ADRESE_EMAIL,'adresa_email=eq.'+encodeURIComponent(email)+'&select=nr_crt&limit=1'); if(!(hit&&hit.length)){ const tz=Session.getScriptTimeZone()||'Europe/Bucharest'; const d=Utilities.formatDate(new Date(),tz,'yyyy-MM-dd'); supaInsertOne_(SUPA_ADRESE_EMAIL,{ cnp_client:rec&&rec.cnp?String(rec.cnp).replace(/\D+/g,''):null, nume_client:rec&&rec.nume_complet?String(rec.nume_complet).trim():null, agent:rec&&rec.agent?String(rec.agent).trim():null, adresa_email:email, status_client:rec&&rec.status_secundar?String(rec.status_secundar).trim():null, status_interogare:'NEDEFINITA', data_creare:d, creat_de:String(sess.user||'').trim()||null, status_curent:'ACTIVA', data_status:d, user_status:String(sess.user||'').trim()||null }); } }catch(e){ Logger.log('bd_adrese_email insert fail: '+e); } } return r; } function createEmailViaAPI_(fullEmail) { fullEmail = String(fullEmail || '').trim(); if (!fullEmail || fullEmail.indexOf('@') === -1) { return { ok: false, statusMessage: 'E-mail invalid!' }; } var parts = fullEmail.split('@'); var localPart = parts[0]; var domain = parts[1]; var password = 'SmartCredit123!@#'; var quota = '1024'; var apiUrl = 'https://api.smart-credit.ro/proxy?' + 'email=' + encodeURIComponent(localPart) + '&domain=' + encodeURIComponent(domain) + '&password=' + encodeURIComponent(password) + '"a=' + encodeURIComponent(quota); var opts = { method: 'get', headers: { 'x-proxy-token': 'YourSecretTokenHere' }, muteHttpExceptions: true }; try { var resp = UrlFetchApp.fetch(apiUrl, opts); if (resp.getResponseCode() !== 200) { return { ok:false, statusMessage:'HTTP ' + resp.getResponseCode() }; } var json = JSON.parse(resp.getContentText() || '{}'); if (json.status === 1) { return { ok: true, statusMessage: 'ADRESA DE E-MAIL A FOST CREATA!', createdEmail: fullEmail, newPassword: password }; } var err = (json.errors || json.error || json.message || ['Unknown error']).toString(); if (err.toLowerCase().includes('already exists')) { return { ok: true, statusMessage: 'ADRESA EXISTA DEJA!', createdEmail: fullEmail }; } return { ok:false, statusMessage:'EROARE: ' + err }; } catch (e) { return { ok:false, statusMessage:'Exception: ' + e.message }; } } function _cpanelGetToken_(){ const t = PropertiesService.getScriptProperties().getProperty('CPANEL_API_TOKEN'); if (!t) throw new Error('Lipsește CPANEL_API_TOKEN din ScriptProperties.'); return t; } function createEmailViaCPanel_(emailFull){ emailFull=String(emailFull||'').trim(); if(!emailFull.includes('@')) return{ok:false,statusMessage:'E-mail invalid!'}; const [localPart,domain]=emailFull.split('@'),password='SmartCredit123!@#',quota=1024; const url='https://www.smcmail.ro:2083/execute/Email/add_pop?email='+encodeURIComponent(localPart)+'&domain='+encodeURIComponent(domain)+'&password='+encodeURIComponent(password)+'"a='+encodeURIComponent(quota); const opts={method:'get',headers:{'Authorization':'cpanel smcmailr:'+_cpanelGetToken_()},muteHttpExceptions:true}; try{ const resp=UrlFetchApp.fetch(url,opts),code=resp.getResponseCode(),txt=resp.getContentText(); if(code!==200) return{ok:false,statusMessage:'HTTP '+code+': '+txt}; const data=JSON.parse(txt); if(data.status===1) return{ok:true,created:true,statusMessage:'ADRESA DE E-MAIL A FOST CREATA!',createdEmail:emailFull,newPassword:password}; const errors=(data.errors||['']).join(' ').toLowerCase(); if(errors.includes('exist')) return{ok:true,created:false,exists:true,statusMessage:'ADRESA EXISTA DEJA!',createdEmail:emailFull}; return{ok:false,statusMessage:'EROARE: '+JSON.stringify(data.errors||data)}; }catch(e){ return{ok:false,statusMessage:'Exception: '+e.message}; } } /** ===== SOLICITĂRI INTEROGARE BC (bd_solicitari_bc) ===== * Inserează o solicitare nouă STANDARD / SCORERISE pentru un client. * * @param {string} token – tokenul sesiunii CRM * @param {string} idUnic – ID UNIC client (din bd_clienti_activi) * @param {object} payload – { agent, client, cnp, tip_bc } */ function bc_solicitareInterogare(token,idUnic,payload){ const sess=_getSession(token); idUnic=String(idUnic||'').trim(); payload=payload||{}; if(!idUnic) throw new Error('ID UNIC lipsă pentru solicitarea BC.'); const agent=String(payload.agent||sess.user||'').trim(), client=String(payload.client||'').trim(), cnp=String(payload.cnp||'').replace(/\D+/g,''), tip=String(payload.tip_bc||'').trim().toUpperCase(), st2=String(payload.status_client||'').trim().toUpperCase(), role=String(sess.role||'').toLowerCase(); if(!agent) throw new Error('AGENT lipsă pentru solicitarea BC.'); if(!client) throw new Error('Nume client lipsă pentru solicitarea BC.'); if(!cnp) throw new Error('CNP lipsă pentru solicitarea BC.'); if(tip!=='STANDARD'&&tip!=='SCORERISE') throw new Error('Tip BC invalid. Se acceptă doar STANDARD sau SCORERISE.'); if(!st2) throw new Error('STATUS SECUNDAR lipsă pentru solicitarea BC.'); const rec=supaOneById_(idUnic,'e_mail_creat'), email=(rec&&rec.e_mail_creat)?String(rec.e_mail_creat).trim():''; if(tip==='STANDARD'&&!email) throw new Error('Clientul nu are completat câmpul „E-MAIL CREAT” SAU nu a avut niciodata e-mail creat pe smcmail.ro'); const tz=Session.getScriptTimeZone()||'Europe/Bucharest', isoToday=Utilities.formatDate(new Date(),tz,'yyyy-MM-dd'); const supaOne_= (table,qs)=> (supaGet_(table,qs)||[])[0]||null; const getLatestCiJpegPath_ = (cnp)=>{ const qs= 'select='+encodeURIComponent('path,data_incarcare')+ '&cnp=eq.'+encodeURIComponent(cnp)+ '&tip_document=eq.'+encodeURIComponent('CARTE DE IDENTITATE')+ '&mime_type=eq.'+encodeURIComponent('image/jpeg')+ '&order='+encodeURIComponent('data_incarcare')+'.desc&limit=1'; const r=supaOne_('bd_documente_clienti',qs); return (r&&r.path)?String(r.path).trim():''; }; const callEdgeScorerise_ = (body)=>{ const url=String(SUPA_URL||'').replace(/\/$/,'')+'/functions/v1/interogari_scorerise'; const opt={ method:'post', muteHttpExceptions:true, contentType:'application/json', payload:JSON.stringify(body||{}), headers:{Authorization:'Bearer '+SUPA_KEY,apikey:SUPA_KEY} }; const resp=UrlFetchApp.fetch(url,opt); const code=resp.getResponseCode(); const txt=resp.getContentText()||''; let j={}; try{ j=JSON.parse(txt)||{}; }catch(_){} if(code>=200&&code<300&&j&&j.ok) return j; return {ok:false,error:(j&&j.error)?j.error:('HTTP_'+code),raw:txt.slice(0,600)}; }; // ========================= // 1) REGULI "ACEAȘI ZI" // ========================= // STANDARD: dacă azi există o solicitare STANDARD cu STATUS != REZOLVAT (sau NULL) => blocăm (pentru toți) if(tip==='STANDARD'){ const qs= 'select='+encodeURIComponent('id_interogare,status,data')+ '&cnp=eq.'+encodeURIComponent(cnp)+ '&tip_bc=eq.'+encodeURIComponent('STANDARD')+ '&data=eq.'+encodeURIComponent(isoToday)+ '&or=(status.is.null,status.neq.'+encodeURIComponent('REZOLVAT')+')'+ '&limit=1'; const hit=supaOne_(SUPA_SOLICITARI_BC,qs); if(hit) return {ok:false,msg:'Atentie! Ai deja o solicitare nerezolvata pentru acest client! Asteapta finalizarea acesteia!'}; } // SCORERISE: dacă user=Utilizator și STATUS2 NU e FINANTARE/STERGERE BC și azi există deja o solicitare SCORERISE => blocăm if(tip==='SCORERISE' && role==='utilizator' && st2!=='FINANTARE' && st2!=='STERGERE BC'){ const qs= 'select='+encodeURIComponent('id_interogare,data')+ '&cnp=eq.'+encodeURIComponent(cnp)+ '&tip_bc=eq.'+encodeURIComponent('SCORERISE')+ '&data=eq.'+encodeURIComponent(isoToday)+ '&limit=1'; const hit=supaOne_(SUPA_SOLICITARI_BC,qs); if(hit) return {ok:false,msg:'Atentie! Pentru acest client ai solicitat deja o interogare BC azi! Urmatoarea solicitare o poti face doar maine!'}; } // ========================= // 2) SCORERISE: CI JPEG (cel mai recent) + apel edge // ========================= let ciPath='', sc=null; if(tip==='SCORERISE'){ ciPath=getLatestCiJpegPath_(cnp); if(!ciPath) return {ok:false,msg:'Pentru a interoga BC-ul este necesar sa atasezi CI in format jpeg'}; } // ========================= // 3) Round-robin (DOAR pentru STANDARD) // ========================= let asistent = agent, to = ''; if (tip === 'STANDARD') { const rr = bc_pickNextActiveAsistent_(); asistent = (rr && rr.user) ? rr.user : agent; to = (rr && rr.user) ? (rr.email || bc_userEmail_(asistent)) : bc_userEmail_(asistent); } // ========================= // 4) Inserare solicitare în bd_solicitari_bc (flux normal) // ========================= const row={ data:isoToday, agent,asistent,client,cnp, email:email||null, tip_bc:tip, status:null, status_client:st2||null }; const inserted=supaInsertOne_(SUPA_SOLICITARI_BC,row); const id_interogare=inserted.id_interogare||inserted.id||null; // ========================= // 5) Dacă SCORERISE: rulează edge și întoarce rezultatul către UI // ========================= if(tip==='SCORERISE'){ sc=callEdgeScorerise_({ cnp, numeClient: client, ci_path: ciPath, sursa: String(sess.user||agent||'CRM').trim() }); if(!sc || !sc.ok){ return { ok:false, id_interogare, email, scorerise: sc||null, msg:'Interogarea BC SCORERISE a eșuat: '+(sc&&sc.error?sc.error:'Eroare necunoscută') }; } } // mail către asistent (ca înainte) if(to){ try{ MailApp.sendEmail(to,'O noua interogare ti-a fost alocata','Verifica sub-meniul „Interogari BC”.'); } catch(e){ Logger.log('Mail fail: '+e); } } return { ok:true, id_interogare, email, tip_bc:tip, scorerise: sc||null, msg: (tip==='SCORERISE') ? ('Interogarea BC SCORERISE a fost efectuată cu succes.'+(sc&&sc.score!=null?(' SCORE='+sc.score):'')) : 'Solicitarea de interogare BC a fost înregistrată.' }; } /** Listează solicitările BC care NU sunt REZOLVATE (status <> 'REZOLVAT') * și care au tip_bc = 'STANDARD'. */ function bc_listSolicitari(token){ const sess=_getSession(token),role=String(sess.role||'').toLowerCase(),me=String(sess.user||'').trim().toUpperCase(); let tip=String(sess.tipAngajat||'').trim().toUpperCase(); if(!tip&&me){try{const colU=encodeURIComponent('"USER"'),q='select='+encodeURIComponent('"TIP ANGAJAT"')+'&'+colU+'=eq.'+encodeURIComponent(me)+'&limit=1';const a=supaGet_(SUPA_USERS_TABLE,q)||[];tip=String(a[0]&&a[0]['TIP ANGAJAT']||'').trim().toUpperCase();}catch(_){tip='';}} const priv=['administrator','manager','controlor'].includes(role); let qs='select='+encodeURIComponent('id_interogare,data,agent,asistent,client,cnp,email,email_2,email_3,tip_bc,status')+ '&order='+encodeURIComponent('data.asc,agent.asc')+ '&tip_bc=eq.'+encodeURIComponent('STANDARD')+ '&or=(status.is.null,status.not.in.('+encodeURIComponent('REZOLVAT')+','+encodeURIComponent('ESUAT')+'))'; if(!priv&&me){ qs += (tip==='ASISTENT') ? ('&asistent=eq.'+encodeURIComponent(me)) : ('&agent=eq.'+encodeURIComponent(me)); } const arr=supaSelectAll_(SUPA_SOLICITARI_BC,qs,1000)||[]; const rows=arr.map(r=>({ id_interogare:r.id_interogare, data:r.data, agent:r.agent, asistent:r.asistent||'', client:r.client, cnp:r.cnp, email:r.email||'', email2:r.email_2||'', email3:r.email_3||'' })); return{ok:true,rows}; } function bc_email_create(token,idInterogare,slot){ const sess=_getSession(token); const id=Number(idInterogare||0),s=Number(slot||0); if(!id) throw new Error('ID interogare invalid.'); if(!(s>=1&&s<=3)) throw new Error('Slot invalid.'); const qs='id_interogare=eq.'+encodeURIComponent(id)+'&select='+encodeURIComponent('id_interogare,cnp,client,email,email_2,email_3')+'&limit=1'; const a=supaGet_(SUPA_SOLICITARI_BC,qs)||[],r=a[0]; if(!r) return {ok:false,statusMessage:'Solicitare negăsită.'}; const email=String((s===2?r.email_2:s===3?r.email_3:r.email)||'').trim(); if(!email) return {ok:false,statusMessage:'Adresa de e-mail lipsește.'}; const resp=createEmailViaCPanel_(email); if(resp&&resp.ok&&!resp.createdEmail) resp.createdEmail=email; // dacă s-a creat ACUM → log în bd_adrese_email (best effort) if(resp&&resp.ok&&resp.created){ try{ const parts=String(r.client||'').trim().split(/\s+/); const pren=parts[0]||'', nume=parts.slice(1).join(' ')||''; _adreseEmail_logIfMissing_(token,null,pren,nume,String(r.cnp||''),email); }catch(_){} } return resp; } /** helper: citește un client după CNP dintr-un tabel (activi/pierduti) */ function _bc_clientByCnp_(table,cnp){ const qs='cnp=eq.'+encodeURIComponent(cnp)+'&select='+encodeURIComponent('id_unic,cnp,e_mail_creat,email_creat_2,email_creat_3')+'&limit=1'; const a=supaGet_(table,qs)||[]; return a[0]||null; } /** helper: patch pe id_unic (fără upsert) */ function _bc_patchClientById_(table,idUnic,patch){ const url=`${SUPA_URL}/rest/v1/${table}?id_unic=eq.${encodeURIComponent(String(idUnic))}`; const r=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify(patch||{}),muteHttpExceptions:true}); if(r.getResponseCode()>=300) throw new Error('Supabase UPDATE '+table+': '+r.getContentText()); return true; } /** helper: calculează patch-ul “shift” în funcție de slot */ function _bc_shiftPatch_(rec,slot){ const e2=String(rec.email_creat_2||'').trim()||null; const e3=String(rec.email_creat_3||'').trim()||null; if(slot===1) return {e_mail_creat:e2,email_creat_2:e3,email_creat_3:null}; if(slot===2) return {e_mail_creat:e2,email_creat_2:null}; return {e_mail_creat:null}; // slot===3 } /** helper: aplică shift în bd_clienti_activi / bd_clienti_pierduti (sursa de adevăr) */ function _bc_applyShiftByCnp_(cnp,slot){ const out={ok:false,updated:[]}; const tables=[SUPA_TABLE,SUPA_TABLE_PIERDUTI]; let after=null; for(const t of tables){ const rec=_bc_clientByCnp_(t,cnp); if(!rec) continue; const patch=_bc_shiftPatch_(rec,slot); _bc_patchClientById_(t,rec.id_unic,patch); out.updated.push({table:t,id_unic:String(rec.id_unic)}); if(!after) after=patch; } if(!out.updated.length) return {ok:false,msg:'Client negăsit după CNP în bd_clienti_activi / bd_clienti_pierduti.'}; out.ok=true; out.after=after||null; return out; } /** * Marchează VBC “eșuată” pe slot 1/2/3: * - SHIFT în sursa de adevăr (bd_clienti_activi / bd_clienti_pierduti) * - audit best-effort în bd_solicitari_bc (nu ștergem emailurile din solicitare) */ function bc_email_markFailed(token,idInterogare,slot){ const sess=_getSession(token); const id=Number(idInterogare||0),s=Number(slot||0); if(!id||!(s>=1&&s<=3)) throw new Error('Parametri invalidi.'); const q='id_interogare=eq.'+encodeURIComponent(id)+'&select='+encodeURIComponent('id_interogare,cnp,email,email_2,email_3')+'&limit=1'; const r=(supaGet_(SUPA_SOLICITARI_BC,q)||[])[0]; if(!r) return {ok:false,msg:'Solicitare negăsită.'}; const col=s===2?'email_2':s===3?'email_3':'email'; const cur=String(r[col]||''); if(!cur.trim()) return {ok:false,msg:'Slot gol.'}; if(/\bVBC\s+ESUAT/i.test(cur)) return {ok:false,msg:'Deja marcat eșuat.'}; // 1) SHIFT în sursa de adevăr (activi + pierduți) const cnp=String(r.cnp||'').replace(/\D+/g,''); const shift=_bc_applyShiftByCnp_(cnp,s); if(!shift.ok) return shift; // 2) APPEND audit sub email, în bd_solicitari_bc const tz=Session.getScriptTimeZone()||'Europe/Bucharest'; const at=Utilities.formatDate(new Date(),tz,'yyyy-MM-dd HH:mm:ss'); const by=String(sess.user||'').trim(); const audit=`\nVBC ESUATĂ (${by} • ${at})`; const patch={}; patch[col]=cur+audit; const url=`${SUPA_URL}/rest/v1/${SUPA_SOLICITARI_BC}?id_interogare=eq.${encodeURIComponent(id)}`; const rr=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify(patch),muteHttpExceptions:true}); if(rr.getResponseCode()>=300) throw new Error(rr.getContentText()); return {ok:true,by,at}; } /** ===== Interogari BC – upload RAPORT BC (PDF) + flag optional în bd_solicitari_bc ===== */ function bc_uploadRaport(token, idInterogare, meta, fd){ const sess = _getSession(token); const id = Number(idInterogare || 0); if (!id) throw new Error('ID interogare invalid.'); if (!fd || !fd.data || !fd.filename){ throw new Error('Fișier PDF lipsă.'); } const cnp = String(meta && meta.cnp || '').replace(/\D+/g,''); const nume = String(meta && meta.client || '').trim(); if (!cnp) throw new Error('CNP lipsă pentru upload.'); if (!nume) throw new Error('Nume client lipsă pentru denumirea fișierului.'); const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const todayIso = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd'); // payload compatibil cu uploadDocument() const fd2 = { data: fd.data, filename: fd.filename, mimeType: fd.mimeType || 'application/pdf', tipDocument: 'RAPORT BC', dataIncarcare: todayIso, clientId: cnp, numeClient: nume, idUnic: meta && meta.idUnic || null, uploadedBy: sess.user || '' }; // 1) Upload efectiv în Supabase (bucket documente_clienti + bd_documente_clienti) const res = uploadDocument(fd2); // 2) Opțional: marcăm în bd_solicitari_bc că raportul e încărcat. // Dacă nu există coloana "raport_incarcat", ignorăm eroarea. try { const url = `${SUPA_URL}/rest/v1/bd_solicitari_bc?id_interogare=eq.${encodeURIComponent(id)}`; const patch = { raport_incarcat: true }; const resp = UrlFetchApp.fetch(url, { method: 'patch', contentType: 'application/json', headers: supaHeaders_(false), payload: JSON.stringify(patch), muteHttpExceptions: true }); const code = resp.getResponseCode(); if (code >= 300){ const body = resp.getContentText() || ''; // dacă mesajul conține numele coloanei, o ignorăm (coloana e opțională) if (body.indexOf('raport_incarcat') === -1){ throw new Error(body); } } } catch(e){ // nu blocăm fluxul pe eroare de flag console.error('bc_uploadRaport – set raport_incarcat a eșuat:', e); } return { ok:true, path: (res && res.path) || null }; } /** ===== Interogari BC – FINALIZEAZĂ solicitarea (status = 'REZOLVAT') ===== */ function bc_finalizeSolicitare(token, idInterogare){ _getSession(token); // doar validare sesiune const id = Number(idInterogare || 0); if (!id) throw new Error('ID interogare invalid.'); const url = `${SUPA_URL}/rest/v1/bd_solicitari_bc?id_interogare=eq.${encodeURIComponent(id)}`; const patch = { status: 'REZOLVAT' }; const resp = UrlFetchApp.fetch(url, { method: 'patch', contentType: 'application/json', headers: supaHeaders_(false), payload: JSON.stringify(patch), muteHttpExceptions: true }); const code = resp.getResponseCode(); if (code >= 300){ throw new Error('Supabase UPDATE bd_solicitari_bc: ' + resp.getContentText()); } return { ok:true }; } function bc_esueazaSolicitare(token, idInterogare){ _getSession(token); const id = Number(idInterogare || 0); if (!id) throw new Error('ID interogare invalid.'); const url = `${SUPA_URL}/rest/v1/bd_solicitari_bc?id_interogare=eq.${encodeURIComponent(id)}`; const patch = { status: 'ESUAT' }; const resp = UrlFetchApp.fetch(url, { method: 'patch', contentType: 'application/json', headers: supaHeaders_(false), payload: JSON.stringify(patch), muteHttpExceptions: true }); if (resp.getResponseCode() >= 300){ throw new Error('Supabase UPDATE bd_solicitari_bc: ' + resp.getContentText()); } return { ok:true }; } /** * Transformă 07xx… în +407xx…, acceptă și variante cu 0040 / 40 / spații / liniuțe. * Dacă nu iese un mobil valid 7xxxxxxxx → întoarce ''. */ function normalizeRoPhoneForWhatsApp(raw) { let d = String(raw || '').replace(/\D+/g, ''); // doar cifre if (!d) return ''; if (d.startsWith('00')) d = d.slice(2); // 0040xxxxxxxx → 40xxxxxxxx if (d.startsWith('40')) d = d.slice(2); // 40xxxxxxxxx → xxxxxxxxx if (d.startsWith('0')) d = d.slice(1); // 07xxxxxxxx → 7xxxxxxxx if (!/^7\d{8}$/.test(d)) return ''; // trebuie să fie 7 + 8 cifre return '+40' + d; // format E.164 pt WhatsApp } /** Token ManyChat (centralizat) */ function manychat_getToken_(){ const t = PropertiesService.getScriptProperties().getProperty(MANYCHAT_API_PROP_AGENT); if (!t) throw new Error('Lipsește MANYCHAT_API_TOKEN în Script Properties (ManyChat).'); return t; } /** Ia numele complet al agentului (USER) din bd_useri_si_parole – cache v2 + CAPS. */ function manychat_getAgentFullName_(agentCode){ agentCode = String(agentCode || '').trim(); if (!agentCode) return ''; const cache = CacheService.getScriptCache(); const CK = 'mc:agentFullName:v2:' + agentCode; // v2 = sparge cache-ul vechi function norm_(v){ const s = String(v == null ? '' : v).trim(); return s ? s.toUpperCase() : ''; } // 1) cache try{ const hit = cache.get(CK); const v = norm_(hit); if (v) return v; }catch(_){} // 2) Supabase let full = ''; try{ const qs = 'USER=eq.' + encodeURIComponent(agentCode) + '&select=' + encodeURIComponent('"NUME COMPLET USER"') + '&limit=1'; const rows = supaGet_(SUPA_USERS_TABLE, qs); if (rows && rows.length){ full = norm_(rows[0]['NUME COMPLET USER'] || ''); } }catch(_){} // 3) fallback if (!full) full = norm_(agentCode) || agentCode; // 4) cache 6h try{ cache.put(CK, full, 6*60*60); }catch(_){} return full; } function manychat_setCustomField_(subscriberId, fieldId, fieldValue){ const token = manychat_getToken_(); const url = MANYCHAT_BASE_URL_AGENT + '/fb/subscriber/setCustomField'; const payload = { subscriber_id: Number(subscriberId), field_id: Number(fieldId), field_value: (fieldValue == null ? '' : String(fieldValue)) }; const res = UrlFetchApp.fetch(url, { method: 'post', muteHttpExceptions: true, contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + token }, payload: JSON.stringify(payload) }); const code = res.getResponseCode(); const body = res.getContentText() || ''; if (code < 200 || code >= 300){ throw new Error('ManyChat setCustomField ' + code + ': ' + body); } // ✅ verificăm și status din JSON let json = null; try { json = JSON.parse(body); } catch(_){} if (json && String(json.status || '').toLowerCase() !== 'success'){ throw new Error('ManyChat setCustomField ERROR: ' + body); } return true; } /** Aplică un TAG după nume (și tratăm non-2xx ca eroare). */ function manychat_addTagByName_(subscriberId, tagName){ const token = manychat_getToken_(); const url = MANYCHAT_BASE_URL_AGENT + '/fb/subscriber/addTagByName'; const payload = { subscriber_id: Number(subscriberId), tag_name: String(tagName || '').trim() }; const res = UrlFetchApp.fetch(url, { method: 'post', muteHttpExceptions: true, contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + token }, payload: JSON.stringify(payload) }); const code = res.getResponseCode(); if (code < 200 || code >= 300){ throw new Error('ManyChat addTagByName ' + code + ': ' + (res.getContentText() || '')); } return true; } function _mcAddWorkingDays_(date, n){ var d = new Date(date.getFullYear(), date.getMonth(), date.getDate()); var step = n >= 0 ? 1 : -1; var left = Math.abs(n); while (left > 0){ d.setDate(d.getDate() + step); var w = d.getDay(); // 0=Sun, 6=Sat if (w !== 0 && w !== 6) left--; } return d; } function _mcRoDow_(date){ var names = ['duminica','luni','marti','miercuri','joi','vineri','sambata']; return names[date.getDay()] || ''; } /** * Calculează câmpurile pentru mesajul de ALOCARE AGENT: * - Ziua_maxima: vineri->luni, luni-joi->maine (weekend->luni, ca fallback safe) * - Ziua_saptamanii + Data_contact_client: +2 zile lucrătoare */ function manychat_buildAlocareFields_(){ var tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; var now = new Date(); var y = Number(Utilities.formatDate(now, tz, 'yyyy')); var m = Number(Utilities.formatDate(now, tz, 'MM')); var d = Number(Utilities.formatDate(now, tz, 'dd')); var today = new Date(y, m-1, d); var dow = today.getDay(); // 0=Sun ... 5=Fri ... 6=Sat var ziuaMaxima = (dow === 5) ? 'luni' : ((dow === 6 || dow === 0) ? 'luni' : 'maine'); var contactDate = _mcAddWorkingDays_(today, 2); // a 2-a zi lucrătoare var ziuaSapt = _mcRoDow_(contactDate); // ex: "miercuri" var dataContact = Utilities.formatDate(contactDate, tz, 'dd.MM.yyyy'); return { ziuaMaxima: ziuaMaxima, ziuaSapt: ziuaSapt, dataContact: dataContact }; } /** * Creează/actualizează un contact în ManyChat, setează CUF cu numele agentului, * apoi aplică TAG-ul care declanșează flow-ul. * * name – nume complet client * phone – telefon E.164 (+407xxxxxxxx) * tagName – numele TAG-ului (ex: "AlocareAgent") * agentFullName – numele agentului (ex: "POPESCU ION") */ function manychatEnsureContactWithTag( name, phoneE164, tagName, agentFullName, agentPhone, ziuaMaxima, ziuaSaptamanii, dataContactClient, opts ){ opts = opts || {}; const dbg = (opts.debug !== false); // implicit: true ca să vezi tot în logs const sleepBeforeTagMs = Number(opts.sleepBeforeTagMs || 1800); // IMPORTANT const sleepBetweenCUFsMs = Number(opts.sleepBetweenCUFsMs || 150); // optional const reSetDateField = (opts.reSetDateField !== false); // implicit: true const token = manychat_getToken_(); function post_(path, payload, label){ const url = MANYCHAT_BASE_URL_AGENT + path; if (dbg) Logger.log('→ ' + label + ' payload: ' + JSON.stringify(payload)); const res = UrlFetchApp.fetch(url, { method: 'post', muteHttpExceptions: true, contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + token }, payload: JSON.stringify(payload) }); const code = res.getResponseCode(); const body = res.getContentText() || ''; if (dbg) Logger.log('← ' + label + ' response: ' + body); if (code < 200 || code >= 300){ throw new Error('ManyChat ' + label + ' ' + code + ': ' + body); } return body ? JSON.parse(body) : {}; } // ------------------------- // 1) createSubscriber // ------------------------- const parts = String(name || '').trim().split(/\s+/); const firstName = parts[0] || ''; const lastName = parts.slice(1).join(' ') || ''; const createPayload = { first_name: firstName, last_name: lastName || undefined, whatsapp_phone: String(phoneE164 || '').trim(), has_opt_in_sms: false, has_opt_in_email: false }; const created = post_('/fb/subscriber/createSubscriber', createPayload, 'createSubscriber'); let subscriberId = null; try{ const d = created && created.data; if (d){ if (d.subscriber && d.subscriber.id) subscriberId = d.subscriber.id; else if (d.id) subscriberId = d.id; } }catch(_){} subscriberId = Number(subscriberId); if (!subscriberId) throw new Error('ManyChat createSubscriber: subscriber_id lipsă.'); if (dbg) Logger.log('subscriber_id = ' + subscriberId); // ------------------------- // 2) setCustomField (CUF-uri) // ------------------------- const nm = String(agentFullName || '').trim(); const ph = String(agentPhone || '').trim(); const zm = String(ziuaMaxima || '').trim(); const zs = String(ziuaSaptamanii || '').trim(); const dc = String(dataContactClient || '').trim(); if (!nm) throw new Error('AgentFullName lipsă (cuf_14023121).'); if (!ph) throw new Error('Telefon agent lipsă (cuf_14023122).'); if (!zm) throw new Error('Ziua_maxima lipsă (cuf_14042345).'); if (!zs) throw new Error('Ziua_saptamanii lipsă (cuf_14042340).'); if (!dc) throw new Error('Data_contact_client lipsă (cuf_14044861).'); // folosim ID-urile globale din CodeIndex.gs (cum le-ai definit) const CUF_AGENT_NAME_ID = MANYCHAT_AGENT_FIELD_ID; // 14023121 const CUF_AGENT_PHONE_ID = (typeof MANYCHAT_AGENT_PHONE_FIELD_ID !== 'undefined') ? MANYCHAT_AGENT_PHONE_FIELD_ID : 14023122; const CUF_ZIUA_MAXIMA_ID = MANYCHAT_CUF_ZIUA_MAXIMA_ID; // 14042345 const CUF_ZIUA_SAPT_ID = MANYCHAT_CUF_ZIUA_SAPTAMANA_ID; // 14042340 const CUF_DATA_CONTACT_ID = MANYCHAT_CUF_DATA_CONTACT_ID; // 14044861 function setCUF_(fieldId, value, label){ post_('/fb/subscriber/setCustomField', { subscriber_id: subscriberId, field_id: Number(fieldId), field_value: String(value) }, label); if (sleepBetweenCUFsMs > 0) Utilities.sleep(sleepBetweenCUFsMs); } setCUF_(CUF_AGENT_NAME_ID, nm, 'CUF nume agent (cuf_14023121)'); setCUF_(CUF_AGENT_PHONE_ID, ph, 'CUF telefon agent (cuf_14023122)'); setCUF_(CUF_ZIUA_MAXIMA_ID, zm, 'CUF ziua_maxima (cuf_14042345)'); setCUF_(CUF_ZIUA_SAPT_ID, zs, 'CUF ziua_saptamanii (cuf_14042340)'); // data_contact_client – setăm + (opțional) setăm încă o dată, apoi pauză înainte de TAG setCUF_(CUF_DATA_CONTACT_ID, dc, 'CUF data_contact_client (cuf_14044861)'); if (reSetDateField){ // „armare” extra pentru cazul tău (exact bug-ul pe ultimul CUF) Utilities.sleep(250); setCUF_(CUF_DATA_CONTACT_ID, dc, 'CUF data_contact_client (RE-SET)'); } if (dbg) Logger.log('… sleep înainte de TAG: ' + sleepBeforeTagMs + 'ms'); Utilities.sleep(sleepBeforeTagMs); // ------------------------- // 3) addTagByName (declanșează flow-ul) // ------------------------- post_('/fb/subscriber/addTagByName', { subscriber_id: subscriberId, tag_name: String(tagName || '').trim() }, 'addTagByName'); return subscriberId; } /** * MANYCHAT – trimite notificări de „alocare agent” către clienții noi ALOCAȚI. * * Logica: * - selectează din bd_clienti_activi clienții cu: * status = 'ALOCAT' * dua = azi * canal = 'CAMPANIE SMC' * notificare_alocare_agent IS NULL * - telefon normalizat (07xx... → +407xx...) * - pentru fiecare: * - ia numele complet al agentului din bd_useri_si_parole * - creează/actualizează contact + setează CUF agent + aplică TAG-ul * - la succes -> notificare_alocare_agent = 'DA' */ function manychat_notifyAgentAssigned(token){ // dacă vine din UI, validăm sesiunea if (token) { try { _getSession(token); } catch (e) { Logger.log('manychat_notifyAgentAssigned – sesiune invalidă: ' + (e && e.message)); throw e; } } // ====== CONFIG CUF-uri (hardcodate ca să fie self-contained) ====== const CUF_AGENT_NAME_ID = 14023121; // {{cuf_14023121}} – nume agent const CUF_AGENT_PHONE_ID = 14023122; // {{cuf_14023122}} – telefon agent const CUF_ZIUA_MAXIMA_ID = 14042345; // {{cuf_14042345}} – "maine" / "luni" const CUF_ZIUA_SAPTAMANA_ID = 14042340; // {{cuf_14042340}} – "marti/miercuri/joi/vineri/luni" const CUF_DATA_CONTACT_ID = 14044861; // {{cuf_14044861}} – dd.MM.yyyy const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const now = new Date(); // „azi” calculat în timezone-ul scriptului (evită drift) const yy = Number(Utilities.formatDate(now, tz, 'yyyy')); const mm = Number(Utilities.formatDate(now, tz, 'MM')); const dd = Number(Utilities.formatDate(now, tz, 'dd')); const today = new Date(yy, mm - 1, dd); const todayIso = Utilities.formatDate(today, tz, 'yyyy-MM-dd'); // ====== calculează o singură dată câmpurile din mesaj (valabile pentru toți clienții din run) ====== function addWorkDays_(base, n){ var d = new Date(base.getFullYear(), base.getMonth(), base.getDate()); var left = Math.max(0, Number(n || 0)); while (left > 0){ d.setDate(d.getDate() + 1); var w = d.getDay(); // 0=Sun, 6=Sat if (w !== 0 && w !== 6) left--; } return d; } function roDow_(date){ var names = ['duminica','luni','marti','miercuri','joi','vineri','sambata']; return names[date.getDay()] || ''; } var dow = today.getDay(); // 0..6 var ziuaMaxima = (dow === 5) ? 'luni' : ((dow === 6 || dow === 0) ? 'luni' : 'maine'); // vineri->luni, luni-joi->maine (weekend fallback->luni) var contactDate = addWorkDays_(today, 2); // +2 zile lucrătoare var ziuaSapt = roDow_(contactDate); // ex: "miercuri" var dataContact = Utilities.formatDate(contactDate, tz, 'dd.MM.yyyy'); Logger.log('=== manychat_notifyAgentAssigned – START ' + todayIso + ' ==='); const select = 'id_unic,agent,nume_complet,telefon,canal,status,dua,notificare_alocare_agent'; const conds = [ 'status=eq.' + encodeURIComponent('ALOCAT'), 'dua=eq.' + encodeURIComponent(todayIso), 'canal=eq.' + encodeURIComponent('CAMPANIE SMC'), 'notificare_alocare_agent=is.null' ]; const qs = 'select=' + encodeURIComponent(select) + '&' + conds.join('&'); let rows = []; try { rows = supaSelectAll_(SUPA_TABLE, qs, 500) || []; Logger.log('manychat_notifyAgentAssigned – rows fetched: ' + rows.length); } catch (e) { Logger.log('manychat_notifyAgentAssigned – eroare la supaSelectAll_: ' + (e && e.message)); throw e; } if (!rows.length){ Logger.log('manychat_notifyAgentAssigned – niciun client eligibil pentru notificare.'); Logger.log('=== manychat_notifyAgentAssigned – END (0) ==='); return { ok:true, date: todayIso, total: 0, notified: 0, failed: 0, fields: { ziuaMaxima: ziuaMaxima, ziuaSaptamanii: ziuaSapt, dataContactClient: dataContact }, errors:[] }; } const notifiedIds = []; const failedIds = []; const errors = []; rows.forEach(r => { const idUnic = String(r.id_unic || '').trim(); const nume = String(r.nume_complet || '').trim(); const telRaw = String(r.telefon || '').trim(); const agentCode = String(r.agent || '').trim(); if (!idUnic || !nume || !telRaw || !agentCode){ failedIds.push(idUnic || '(fara ID)'); errors.push('Date incomplete pentru ID ' + (idUnic || '(fara ID)') + ': nume=' + nume + ', tel=' + telRaw + ', agent=' + agentCode); return; } const phoneE164 = normalizeRoPhoneForWhatsApp(telRaw); if (!phoneE164){ failedIds.push(idUnic); errors.push('Telefon invalid pentru ID ' + idUnic + ': ' + telRaw); return; } const agentFull = manychat_getAgentFullName_(agentCode); let agentPhone = ''; try { agentPhone = (typeof manychat_getAgentPhone_ === 'function') ? manychat_getAgentPhone_(agentCode) : ''; } catch(_){ agentPhone = ''; } if (!String(agentFull || '').trim()){ failedIds.push(idUnic); errors.push('Nume agent lipsă pentru ID ' + idUnic + ' (agent=' + agentCode + ').'); return; } if (!String(agentPhone || '').trim()){ failedIds.push(idUnic); errors.push('Telefon agent lipsă pentru ID ' + idUnic + ' (agent=' + agentCode + ').'); return; } try { // 1) createSubscriber const tokenMC = manychat_getToken_(); const url = MANYCHAT_BASE_URL_AGENT + '/fb/subscriber/createSubscriber'; const parts = String(nume || '').trim().split(/\s+/); const firstName = parts[0] || ''; const lastName = parts.slice(1).join(' ') || ''; const payload = { first_name: firstName, last_name: lastName || undefined, whatsapp_phone: String(phoneE164 || '').trim(), has_opt_in_sms: false, has_opt_in_email: false }; const resp = UrlFetchApp.fetch(url, { method: 'post', muteHttpExceptions: true, contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + tokenMC }, payload: JSON.stringify(payload) }); const code = resp.getResponseCode(); const body = resp.getContentText() || ''; if (code < 200 || code >= 300){ throw new Error('ManyChat createSubscriber ' + code + ': ' + body); } let subscriberId = null; try{ const json = body ? JSON.parse(body) : null; const data = json && json.data; if (data){ if (data.subscriber && data.subscriber.id) subscriberId = data.subscriber.id; else if (data.id) subscriberId = data.id; } }catch(_){} if (!subscriberId){ throw new Error('ManyChat createSubscriber: subscriber_id lipsă în răspuns.'); } // 2) set CUF-uri (ÎNAINTE de TAG) manychat_setCustomField_(subscriberId, CUF_AGENT_NAME_ID, String(agentFull).trim()); manychat_setCustomField_(subscriberId, CUF_AGENT_PHONE_ID, String(agentPhone).trim()); manychat_setCustomField_(subscriberId, CUF_ZIUA_MAXIMA_ID, String(ziuaMaxima).trim()); manychat_setCustomField_(subscriberId, CUF_ZIUA_SAPTAMANA_ID, String(ziuaSapt).trim()); manychat_setCustomField_(subscriberId, CUF_DATA_CONTACT_ID, String(dataContact).trim()); // 3) TAG (declanșează flow-ul) manychat_addTagByName_(subscriberId, MANYCHAT_TAG_AGENT_ALOC); notifiedIds.push(idUnic); Utilities.sleep(200); } catch (e) { failedIds.push(idUnic); errors.push('ManyChat error pentru ID ' + idUnic + ': ' + (e && e.message ? e.message : String(e))); } }); Logger.log('manychat_notifyAgentAssigned – notified: ' + notifiedIds.length + ', failed: ' + failedIds.length); if (errors.length){ Logger.log('manychat_notifyAgentAssigned – errors:\n' + errors.join('\n')); } // marcare în Supabase doar pt. cei notificați if (notifiedIds.length){ const patches = notifiedIds.map(id => ({ id_unic: String(id), notificare_alocare_agent: 'DA' })); try { supaUpsert_(SUPA_TABLE, patches, 'id_unic'); Logger.log('manychat_notifyAgentAssigned – supaUpsert notificare_alocare_agent OK pentru ' + patches.length + ' clienți.'); } catch (e) { Logger.log('manychat_notifyAgentAssigned – eroare la supaUpsert notificare_alocare_agent: ' + (e && e.message)); errors.push('Eroare supaUpsert notificare_alocare_agent: ' + (e && e.message)); } } Logger.log('=== manychat_notifyAgentAssigned – END ==='); return { ok: true, date: todayIso, total: rows.length, notified: notifiedIds.length, failed: failedIds.length, fields: { ziuaMaxima: ziuaMaxima, ziuaSaptamanii: ziuaSapt, dataContactClient: dataContact }, errors: errors }; } /** * Variantă pentru trigger time-driven (fără token de sesiune). * O poți folosi direct la "Triggers" → every hour / every 30 min etc. */ function manychat_notifyAgentAssigned_cron(){ return manychat_notifyAgentAssigned(null); } // ===== MANYCHAT – NAR1 (no answer) ===== const MANYCHAT_TAG_NAR1_LUNI = 'NAR 1 LUNI'; const MANYCHAT_TAG_NAR1_CS = 'NAR 1 CS'; const MANYCHAT_AGENT_PHONE_FIELD_ID = 14023122; // cuf_14023122 function manychat_getAgentPhone_(agentCode){ agentCode = String(agentCode || '').trim(); if (!agentCode) return ''; const cache = CacheService.getScriptCache(); const CK = 'mc:agentPhone:v2:' + agentCode; // v2 = sparge cache-ul vechi function normPhone(raw){ const dig = String(raw || '').replace(/\D+/g,''); // păstrăm DOAR cifrele 07######## if (/^07\d{8}$/.test(dig)) return dig; return String(raw || '').trim(); } try{ const hit = cache.get(CK); if (hit) return normPhone(hit); // ✅ normalizează și hit-ul din cache }catch(_){} let tel = ''; try{ const qs = 'USER=eq.' + encodeURIComponent(agentCode) + '&select=' + encodeURIComponent('TELEFON') + '&limit=1'; const rows = supaGet_(SUPA_USERS_TABLE, qs); if (rows && rows.length){ tel = String(rows[0].TELEFON || '').trim(); } }catch(_){} const out = normPhone(tel); try{ cache.put(CK, out || '', 6*60*60); }catch(_){} return out || ''; } // ziua lucrătoare anterioară (în TZ-ul scriptului) function _prevWorkingDayInfo_(tz){ const now = new Date(); // construim “azi” în tz (evită drift de timezone) const y = Number(Utilities.formatDate(now, tz, 'yyyy')); const m = Number(Utilities.formatDate(now, tz, 'MM')); const d = Number(Utilities.formatDate(now, tz, 'dd')); let cur = new Date(y, m-1, d); // dăm înapoi 1 zi până găsim weekday cur.setDate(cur.getDate() - 1); while (true){ const dow = Utilities.formatDate(cur, tz, 'EEE'); // Mon..Sun if (dow !== 'Sat' && dow !== 'Sun') break; cur.setDate(cur.getDate() - 1); } return { date: cur, iso: Utilities.formatDate(cur, tz, 'yyyy-MM-dd'), dow: Utilities.formatDate(cur, tz, 'EEE') }; } function manychat_addTagByNameSafe_(subscriberId, tagName){ // preferăm helperul tău dacă există; altfel call direct if (typeof manychat_addTagByName_ === 'function'){ return manychat_addTagByName_(subscriberId, tagName); } const token = (typeof manychat_getToken_ === 'function') ? manychat_getToken_() : PropertiesService.getScriptProperties().getProperty(MANYCHAT_API_PROP_AGENT); if (!token) throw new Error('Lipsește MANYCHAT_API_TOKEN în Script Properties (ManyChat).'); const url = MANYCHAT_BASE_URL_AGENT + '/fb/subscriber/addTagByName'; const payload = { subscriber_id: Number(subscriberId), tag_name: String(tagName || '').trim() }; const res = UrlFetchApp.fetch(url, { method: 'post', muteHttpExceptions: true, contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + token }, payload: JSON.stringify(payload) }); const code = res.getResponseCode(); if (code < 200 || code >= 300){ throw new Error('ManyChat addTagByName ' + code + ': ' + (res.getContentText() || '')); } return true; } /** * Creează/actualizează contact, setează CUF agent + CUF tel agent, apoi aplică TAG. * IMPORTANT: CUF-urile se setează ÎNAINTE de TAG, ca mesajul să nu vină cu câmpuri blank. */ function manychatEnsureContactWithTag_NAR1_(name, phoneE164, tagName, agentFullName, agentPhone){ const token = (typeof manychat_getToken_ === 'function') ? manychat_getToken_() : PropertiesService.getScriptProperties().getProperty(MANYCHAT_API_PROP_AGENT); if (!token) throw new Error('Lipsește MANYCHAT_API_TOKEN în Script Properties (ManyChat).'); const url = MANYCHAT_BASE_URL_AGENT + '/fb/subscriber/createSubscriber'; const parts = String(name || '').trim().split(/\s+/); const firstName = parts[0] || ''; const lastName = parts.slice(1).join(' ') || ''; const payload = { first_name: firstName, last_name: lastName || undefined, whatsapp_phone: String(phoneE164 || '').trim(), has_opt_in_sms: false, has_opt_in_email: false }; const res = UrlFetchApp.fetch(url, { method: 'post', muteHttpExceptions: true, contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + token }, payload: JSON.stringify(payload) }); const code = res.getResponseCode(); const body = res.getContentText() || ''; if (code < 200 || code >= 300){ throw new Error('ManyChat createSubscriber ' + code + ': ' + body); } let subscriberId = null; try{ const json = body ? JSON.parse(body) : null; const data = json && json.data; if (data){ if (data.subscriber && data.subscriber.id) subscriberId = data.subscriber.id; else if (data.id) subscriberId = data.id; } }catch(_){} if (!subscriberId){ throw new Error('ManyChat createSubscriber: subscriber_id lipsă în răspuns.'); } const nm = String(agentFullName || '').trim(); const ph = String(agentPhone || '').trim(); if (!nm) throw new Error('Nume agent lipsă (cuf_14023121).'); if (!ph) throw new Error('Telefon agent lipsă (cuf_14023122).'); // 1) CUF-uri (îNAINTE de TAG) manychat_setCustomField_(subscriberId, MANYCHAT_AGENT_FIELD_ID, nm); manychat_setCustomField_(subscriberId, MANYCHAT_AGENT_PHONE_FIELD_ID, ph); // 2) TAG (declanșează flow-ul) manychat_addTagByNameSafe_(subscriberId, tagName); return subscriberId; } /** * Trimite mesaj NAR1 către clienții: * - canal = CAMPANIE SMC * - dua = ziua lucrătoare anterioară * - status_secundar = NAR1 (acceptă și “NAR 1”) * Tag: * - dacă ziua lucrătoare anterioară e Vineri -> "NAR1 – LUNI" * - altfel -> "NAR1 – CS" */ function manychat_notifyNar1PrevWorkingDay(token){ if (token) _getSession(token); const tz = Session.getScriptTimeZone() || 'Europe/Bucharest'; const todayDow = Utilities.formatDate(new Date(), tz, 'EEE'); if (todayDow === 'Sat' || todayDow === 'Sun'){ return { ok:true, skipped:'weekend' }; } const prev = _prevWorkingDayInfo_(tz); const prevIso = prev.iso; // Tag-ul depinde de ziua lucrătoare anterioară (DUA) const tagName = (prev.dow === 'Fri') ? MANYCHAT_TAG_NAR1_LUNI : MANYCHAT_TAG_NAR1_CS; const select = 'id_unic,nume_complet,telefon,agent,data_client,status,dua,canal,status_secundar'; const conds = [ 'canal=eq.' + encodeURIComponent('CAMPANIE SMC'), 'dua=eq.' + encodeURIComponent(prevIso), // ✅ STRICT: doar "NAR 1" 'status_secundar=eq.' + encodeURIComponent('NAR 1') ]; const qs = 'select=' + encodeURIComponent(select) + '&' + conds.join('&'); const fetched = supaSelectAll_(SUPA_TABLE, qs, 1000) || []; const notified = []; const failed = []; const errors = []; for (let i=0; i= azi * ========================= */ const SUPA_REALOCARI = 'bd_realocari'; const RA_ALL_TARGET = '__ALL__'; // Upsert "minimal" (nu returnăm rânduri) — necesar pentru operații mari function supaUpsertMinimal_(table, rows, conflictCol){ const url = SUPA_URL + '/rest/v1/' + table + '?on_conflict=' + encodeURIComponent(conflictCol || 'id_unic'); const headers = supaHeaders_(false); headers['Prefer'] = 'resolution=merge-duplicates,return=minimal'; const res = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', headers: headers, payload: JSON.stringify(rows || []), muteHttpExceptions: true }); const code = res.getResponseCode(); if (code >= 300){ throw new Error('Supabase UPSERT(minimal) ' + table + ': ' + res.getContentText()); } return true; } // Insert "minimal" function supaInsertManyMinimal_(table, rows){ const url = SUPA_URL + '/rest/v1/' + table; const headers = supaHeaders_(false); headers['Prefer'] = 'return=minimal'; const res = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', headers: headers, payload: JSON.stringify(rows || []), muteHttpExceptions: true }); const code = res.getResponseCode(); if (code >= 300){ throw new Error('Supabase INSERT(minimal) ' + table + ': ' + res.getContentText()); } return true; } function ra__ensureAccess_(sess){ const role = String((sess && sess.role) || '').toLowerCase(); if (!['administrator','manager','controlor'].includes(role)){ throw new Error('Acces interzis (doar Administrator / Manager / Controlor).'); } } function ra__listTargets_(){ const tipCol = encodeURIComponent('"TIP ANGAJAT"'); const hrCol = encodeURIComponent('"STATUS HR"'); // CONSILIER activ + RMA (ca opțiune individuală) const qs = 'select=' + encodeURIComponent('USER,"TIP ANGAJAT","STATUS HR"') + '&' + hrCol + '=eq.' + encodeURIComponent('ACTIV') + '&or=(' + tipCol + '.eq.' + encodeURIComponent('CONSILIER') + ',USER.eq.' + encodeURIComponent('RMA') + ')' + '&order=' + encodeURIComponent('USER.asc'); const rows = supaGet_(SUPA_USERS_TABLE, qs) || []; const targets = []; const allAgents = []; // doar CONSILIER activ, fără RMA rows.forEach(r => { const u = String(r.USER || '').trim(); if (!u) return; targets.push(u); const tip = String(r['TIP ANGAJAT'] || '').trim().toUpperCase(); if (tip === 'CONSILIER' && u.toUpperCase() !== 'RMA'){ allAgents.push(u); } }); const uniqSort = arr => Array.from(new Set(arr.filter(Boolean))).sort((a,b)=>String(a).localeCompare(String(b),'ro')); return { targets: uniqSort(targets), allAgents: uniqSort(allAgents) }; } /** Frontend: populate dropdown target */ function ra_getTargets(token){ const sess = _getSession(token); ra__ensureAccess_(sess); const o = ra__listTargets_(); return { ok:true, agents:o.targets, allAgents:o.allAgents, allValue:RA_ALL_TARGET }; } /** * Realocare: * @param {string} token * @param {Array} idList * @param {string} target - '__ALL__' sau cod agent (ex: ABC / RMA) * @param {boolean} includeProgramari * @param {string} reason */ function ra_realoca(token, idList, target, includeProgramari, reason){ const sess = _getSession(token); ra__ensureAccess_(sess); reason = String(reason || '').trim(); if (!reason) throw new Error('Motiv realocare obligatoriu.'); includeProgramari = (includeProgramari !== false); // normalizează id-uri (doar cifre) + dedup const uniq = []; const seen = Object.create(null); (idList || []).forEach(x => { const id = String(x || '').replace(/\D+/g,'').trim(); if (!id) return; if (!seen[id]){ seen[id]=1; uniq.push(id); } }); if (!uniq.length) throw new Error('Nu există clienți de realocat.'); const t = String(target || '').trim(); if (!t) throw new Error('Selectează agentul țintă.'); const mode = (t === RA_ALL_TARGET) ? 'TO_ALL' : 'SINGLE'; const targets = ra__listTargets_(); const allowedSingle = targets.targets; // CONSILIER activ + RMA const allAgents = targets.allAgents; // CONSILIER activ fără RMA if (mode === 'SINGLE'){ if (t.toUpperCase() !== 'RMA' && allowedSingle.indexOf(t) === -1){ throw new Error('Agent țintă invalid sau INACTIV.'); } } else { if (!allAgents.length){ throw new Error('Nu există agenți ACTIVI pentru redistribuire.'); } } // asignare (ordine fixă; nu rotim startul) const assign = Object.create(null); if (mode === 'SINGLE'){ uniq.forEach(id => { assign[id] = t; }); } else { for (let i=0; i { const id = String(r.id_unic || '').replace(/\D+/g,'').trim(); if (id) oldMap[id] = String(r.agent || '').trim(); }); // 2) build patches pentru clienți const patchesCli = []; chunkIds.forEach(id => { if (!Object.prototype.hasOwnProperty.call(oldMap, id)){ skippedNotFound++; logs.push({ batch_id: batchId, created_by_user: createdByUser, created_by_role: createdByRole, mode: mode, reason: reason, include_programari: includeProgramari, id_unic_client: Number(id), old_agent: null, new_agent: assign[id] || null, programari_updated: 0, result: 'SKIPPED', error_msg: 'NOT_FOUND' }); return; } patchesCli.push({ id_unic: id, agent: assign[id] }); }); if (patchesCli.length){ supaUpsertMinimal_(SUPA_TABLE, patchesCli, 'id_unic'); clientsUpdated += patchesCli.length; } // 3) update programări viitoare (>= azi) const progCountMap = Object.create(null); if (includeProgramari && patchesCli.length){ const idsForProg = patchesCli.map(p => String(p.id_unic)).join(','); const qsP = 'select=' + encodeURIComponent('id,id_unic_client') + '&id_unic_client=in.(' + idsForProg + ')' + '&data_programare=gte.' + encodeURIComponent(todayIso); const progRows = supaSelectAll_(SUPA_PROGRAMARI, qsP, 1000) || []; if (progRows.length){ const patchesProg = []; progRows.forEach(pr => { const pid = Number(pr.id || 0); const cid = String(pr.id_unic_client || '').replace(/\D+/g,'').trim(); if (!pid || !cid) return; const ag = assign[cid]; if (!ag) return; patchesProg.push({ id: pid, agent: ag }); progCountMap[cid] = (progCountMap[cid] || 0) + 1; }); if (patchesProg.length){ const CH_P = 500; for (let j=0; j { const id = String(p.id_unic).replace(/\D+/g,'').trim(); logs.push({ batch_id: batchId, created_by_user: createdByUser, created_by_role: createdByRole, mode: mode, reason: reason, include_programari: includeProgramari, id_unic_client: Number(id), old_agent: oldMap[id] || null, new_agent: assign[id] || null, programari_updated: Number(progCountMap[id] || 0), result: 'UPDATED', error_msg: null }); }); } // 5) insert logs (chunk) if (logs.length){ const CH_L = 500; for (let i=0; i'mr:'+st+':'+t,q=t=>'select=text_mesaj&status_secundar=eq.'+encodeURIComponent(st)+'&tip_client=eq.'+encodeURIComponent(t); const get=t=>{const k=key(t),h=c.get(k); if(h!=null) return h===MR_NULL?'':h; const a=supaGet_(SUPA_MESAJE_RAPIDE_FO,q(t)); const txt=(a&&a[0]&&a[0].text_mesaj!=null)?String(a[0].text_mesaj):''; c.put(k,txt||MR_NULL,MR_TTL); return txt;}; let txt=get(tc); if(!txt&&tc!=='ANY') txt=get('ANY'); return txt?{ok:true,text:txt}:{ok:false,msg:'Nu există mesaj pentru '+st+' / '+tc+'.'}; } function bc_pickNextActiveAsistent_(){ const colU=encodeURIComponent('"USER"'),colTA=encodeURIComponent('"TIP ANGAJAT"'),colSZ=encodeURIComponent('"STATUS ZILNIC"'); const qs='select='+encodeURIComponent('"USER","E-MAIL"')+'&'+colTA+'=eq.'+encodeURIComponent('ASISTENT')+'&'+colSZ+'=eq.'+encodeURIComponent('ACTIV')+'&order='+colU+'.asc'; const rows=supaGet_(SUPA_USERS_TABLE,qs)||[]; if(!rows.length) return null; const sp=PropertiesService.getScriptProperties(),last=sp.getProperty(BC_RR_KEY)||''; let i=rows.findIndex(x=>String(x['USER']||'')===last); i=(i+1)%rows.length; const pick=rows[i]||{}; sp.setProperty(BC_RR_KEY,String(pick['USER']||'')); return {user:String(pick['USER']||'').trim(),email:String(pick['E-MAIL']||'').trim()}; } function bc_userEmail_(u){ const colU=encodeURIComponent('"USER"'); const qs='select='+encodeURIComponent('"E-MAIL"')+'&'+colU+'=eq.'+encodeURIComponent(String(u||'').trim())+'&limit=1'; const arr=supaGet_(SUPA_USERS_TABLE,qs)||[]; return (arr[0]&&arr[0]['E-MAIL'])?String(arr[0]['E-MAIL']).trim():''; } // ===== SURSE TEHNICE / EXTRAGERI BC (Supabase) ===== const SUPA_EXTR_INFO='bd_diverse_info_extrageri',SUPA_IFN_BUNE='bd_ifnuri_bune',SUPA_LISTA_CREDITORI='bd_lista_creditori'; function exi_getInfo(token){ const sess=_getSession(token);_ensureAdmin_(sess); let a=[];try{a=supaGet_(SUPA_EXTR_INFO,'id=eq.1&select=dobanda_np,dobanda_ipotecar,coeficient_np,curs_eur,curs_usd,curs_chf');}catch(_){} if(!a||!a.length){try{supaUpsert_(SUPA_EXTR_INFO,[{id:1}],'id');}catch(_){} return{ok:true,row:{dobanda_np:'',dobanda_ipotecar:'',coeficient_np:'',curs_eur:'',curs_usd:'',curs_chf:''}};} const r=a[0]||{}; return{ok:true,row:{dobanda_np:r.dobanda_np??'',dobanda_ipotecar:r.dobanda_ipotecar??'',coeficient_np:r.coeficient_np??'',curs_eur:r.curs_eur??'',curs_usd:r.curs_usd??'',curs_chf:r.curs_chf??''}}; } function exi_setInfo(token,patch){ const sess=_getSession(token);_ensureAdmin_(sess); patch=patch||{};patch.id=1; supaUpsert_(SUPA_EXTR_INFO,[patch],'id'); return{ok:true}; } function exi_ifn_list(token){ const sess=_getSession(token);_ensureAdmin_(sess); const col=encodeURIComponent('"DENUMIRE COMERCIALA"'); const a=supaSelectAll_(SUPA_LISTA_CREDITORI,'select='+col+'&order='+col+'.asc',2000)||[]; const options=Array.from(new Set(a.map(r=>String(r['DENUMIRE COMERCIALA']||'').trim()).filter(Boolean))).sort((x,y)=>x.localeCompare(y,'ro')); const rows=supaSelectAll_(SUPA_IFN_BUNE,'select=id,denumire_creditor&order=denumire_creditor.asc',1000)||[]; return{ok:true,options,rows:rows.map(r=>({id:r.id,denumire_creditor:r.denumire_creditor||''}))}; } function exi_ifn_add(token,den){ const sess=_getSession(token);_ensureAdmin_(sess); den=String(den||'').trim();if(!den) return{ok:false,msg:'Alege creditor.'}; supaUpsert_(SUPA_IFN_BUNE,[{denumire_creditor:den}],'denumire_creditor'); return{ok:true}; } function exi_ifn_delete(token,id){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0);if(!id) return{ok:false,msg:'ID invalid.'}; const url=`${SUPA_URL}/rest/v1/${SUPA_IFN_BUNE}?id=eq.${encodeURIComponent(id)}`; const res=UrlFetchApp.fetch(url,{method:'delete',headers:supaHeaders_(false),muteHttpExceptions:true}); if(res.getResponseCode()>=300) throw new Error('Supabase DELETE: '+res.getContentText()); return{ok:true}; } function exi_ifn_update(token,id,den){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0);den=String(den||'').trim();if(!id)return{ok:false,msg:'ID invalid.'};if(!den)return{ok:false,msg:'Alege creditor.'}; const a=supaGet_(SUPA_IFN_BUNE,'select=id&denumire_creditor=eq.'+encodeURIComponent(den)+'&limit=1')||[]; if(a.length&&Number(a[0].id||0)!==id) return{ok:false,msg:'Creditor existent deja.'}; const url=`${SUPA_URL}/rest/v1/${SUPA_IFN_BUNE}?id=eq.${encodeURIComponent(id)}`; const r=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify({denumire_creditor:den}),muteHttpExceptions:true}); if(r.getResponseCode()>=300) throw new Error('Supabase UPDATE: '+r.getContentText()); return{ok:true}; } function exi_cred_list(token){ const sess=_getSession(token);_ensureAdmin_(sess); // corectăm atât select cât și order (coloana reală = "NR CRT") const sel=encodeURIComponent('"NR CRT",CREDITOR,"DENUMIRE COMERCIALA","TIP CREDITOR"'); const qs='select='+sel+'&order='+encodeURIComponent('"NR CRT"')+'.asc'; const a=supaSelectAll_(SUPA_LISTA_CREDITORI,qs,2000)||[]; return{ok:true,rows:a.map(r=>({ nr:r['NR CRT'], creditor:r.CREDITOR||'', denumire:r['DENUMIRE COMERCIALA']||'', tip:r['TIP CREDITOR']||'' }))}; } function q3_cred_list(token){ _getSession(token); // doar autentificare, FĂRĂ _ensureAdmin_ const sel = encodeURIComponent('"DENUMIRE COMERCIALA"'); const qs = 'select=' + sel + '&order=' + sel + '.asc'; const a = supaSelectAll_(SUPA_LISTA_CREDITORI, qs, 2000) || []; return { ok: true, rows: a.map(r => ({ denumire: r['DENUMIRE COMERCIALA'] || '' })) }; } function exi_cred_add(token,p){ const sess=_getSession(token);_ensureAdmin_(sess); p=p||{}; const cred=String(p.creditor||'').trim(), den =String(p.denumire||'').trim(), tip =String(p.tip||'').trim().toUpperCase(); if(!cred||!den||!tip) return{ok:false,msg:'Completează toate câmpurile.'}; if(tip!=='BANCA'&&tip!=='IFN') return{ok:false,msg:'Tip invalid (BANCA/IFN).'}; supaInsertOne_(SUPA_LISTA_CREDITORI,{ CREDITOR:cred, 'DENUMIRE COMERCIALA':den, 'TIP CREDITOR':tip }); return{ok:true}; } function exi_cred_update(token,nr,p){ const sess=_getSession(token);_ensureAdmin_(sess); nr=Number(nr||0);if(!nr) return{ok:false,msg:'NR invalid.'}; p=p||{}; const cred=String(p.creditor||'').trim(), den =String(p.denumire||'').trim(), tip =String(p.tip||'').trim().toUpperCase(); if(!cred||!den||!tip) return{ok:false,msg:'Completează toate câmpurile.'}; if(tip!=='BANCA'&&tip!=='IFN') return{ok:false,msg:'Tip invalid (BANCA/IFN).'}; const url=`${SUPA_URL}/rest/v1/${SUPA_LISTA_CREDITORI}?${encodeURIComponent('"NR CRT"')}=eq.${encodeURIComponent(nr)}`; const body={CREDITOR:cred,'DENUMIRE COMERCIALA':den,'TIP CREDITOR':tip}; const res=UrlFetchApp.fetch(url,{ method:'patch', contentType:'application/json', headers:supaHeaders_(false), payload:JSON.stringify(body), muteHttpExceptions:true }); if(res.getResponseCode()>=300) return{ok:false,msg:'Supabase UPDATE: '+res.getContentText()}; return{ok:true}; } function exi_cred_delete(token,nr){ const sess=_getSession(token);_ensureAdmin_(sess); nr=Number(nr||0);if(!nr) return{ok:false,msg:'NR invalid.'}; const url=`${SUPA_URL}/rest/v1/${SUPA_LISTA_CREDITORI}?${encodeURIComponent('"NR CRT"')}=eq.${encodeURIComponent(nr)}`; const res=UrlFetchApp.fetch(url,{ method:'delete', headers:supaHeaders_(false), muteHttpExceptions:true }); if(res.getResponseCode()>=300) return{ok:false,msg:'Supabase DELETE: '+res.getContentText()}; return{ok:true}; } function st_ss_printPDF(token, rows){ _getSession(token); rows = Array.isArray(rows) ? rows : []; if (!rows.length) return { ok:false, msg:'Lipsă date pentru PDF.' }; const pick = (r, keys) => { for (var i=0;i ({ den: pick(r, ['den','creditor','CREDITOR','DENUMIRE','DENUMIRE INTERNA SS','DENUMIRE INTERNA']).trim(), cont: pick(r, ['cont','conturi','NUMAR/TIPURI CONTURI','NUMAR_TIPURI_CONTURI']).trim(), pret: pick(r, ['pret','pretInt','pret_intern','PRET CERUT INTERN','PRET (INTERN)']).trim(), exp: pick(r, ['exp','explicatii','EXPLICATII']).trim() }); // grupare pe creditor const groups = {}; rows.forEach(r=>{ const x = normRow(r); if (!x.den) return; (groups[x.den] = groups[x.den] || []).push(x); }); const keys = Object.keys(groups).sort((a,b)=>a.localeCompare(b,'ro')); if (!keys.length) return { ok:false, msg:'Nu există denumiri de creditor pentru PDF.' }; const doc = DocumentApp.create('Lista Internă – Ștergeri Speciale'); const body = doc.getBody(); body.clear(); // A4 landscape în puncte: 841.89 × 595.28 try{ if (typeof body.setPageWidth === 'function') body.setPageWidth(841.89); if (typeof body.setPageHeight === 'function') body.setPageHeight(595.28); if (typeof body.setMarginTop === 'function') body.setMarginTop(28); if (typeof body.setMarginBottom === 'function') body.setMarginBottom(28); if (typeof body.setMarginLeft === 'function') body.setMarginLeft(28); if (typeof body.setMarginRight === 'function') body.setMarginRight(28); }catch(_){} const t = body.appendParagraph('LISTA INTERNĂ – ȘTERGERI SPECIALE'); t.setHeading(DocumentApp.ParagraphHeading.HEADING1); body.appendParagraph(''); keys.forEach(k=>{ const h = body.appendParagraph(k); h.setHeading(DocumentApp.ParagraphHeading.HEADING2); const tbl = body.appendTable([['NUMAR/TIPURI CONTURI','PRET (INTERN)','EXPLICAȚII']]); const hr = tbl.getRow(0); hr.editAsText().setBold(true); try{ for (var i=0;i<3;i++) hr.getCell(i).setBackgroundColor('#dbe3f3'); }catch(_){} (groups[k]||[]).forEach(r=>{ const tr = tbl.appendTableRow(); tr.appendTableCell(String(r.cont||'')); tr.appendTableCell(String(r.pret||'')); tr.appendTableCell(String(r.exp||'')); }); body.appendParagraph(''); }); // IMPORTANT: fără asta PDF-ul poate ieși BLANK doc.saveAndClose(); Utilities.sleep(250); const fileId = doc.getId(); const pdfBlob = DriveApp.getFileById(fileId).getBlob().getAs(MimeType.PDF); const content = Utilities.base64Encode(pdfBlob.getBytes()); try{ DriveApp.getFileById(fileId).setTrashed(true); }catch(_){} return { ok:true, content }; } function gest_getInfoSuplimentare(token,idUnic){ _getSession(token);idUnic=String(idUnic||'').trim();if(!idUnic)return{ok:false,msg:'ID UNIC lipsă.'}; const r=supaOneById_(idUnic,'info_suplimentare');return r?{ok:true,info:String(r.info_suplimentare||'')}:{ok:false,msg:'Client inexistent.'}; } function _sc_isoTs_(s) { s = String(s || '').trim(); const m = s.match(/^(\d{2})-(\d{2})-(\d{4})\s+(\d{2}):(\d{2}):(\d{2})$/); return m ? `${m[3]}-${m[2]}-${m[1]}T${m[4]}:${m[5]}:${m[6]}` : ''; } function _sc_dmyDashToIso_(s){const m=String(s||'').trim().match(/^(\d{2})-(\d{2})-(\d{4})$/);return m?`${m[3]}-${m[2]}-${m[1]}`:'';} function _sc_toMs_(s){ s=String(s||'').trim(); if(!s) return 0; if(/^\d{4}-\d{2}-\d{2}T/.test(s)||/^\d{4}-\d{2}-\d{2}$/.test(s)){const t=Date.parse(s); return isNaN(t)?0:t;} let m=s.match(/^(\d{2})-(\d{2})-(\d{4})(?:\s+(\d{2}):(\d{2}):(\d{2}))?$/); if(m) return new Date(+m[3],+m[2]-1,+m[1],+(m[4]||0),+(m[5]||0),+(m[6]||0)).getTime(); m=s.match(/^(\d{2})\.(\d{2})\.(\d{4})(?:\s+(\d{2}):(\d{2}):(\d{2}))?$/); if(m) return new Date(+m[3],+m[2]-1,+m[1],+(m[4]||0),+(m[5]||0),+(m[6]||0)).getTime(); const t2=Date.parse(s); return isNaN(t2)?0:t2; } function ca_bc_people_list(token){ _getSession(token); const col=encodeURIComponent('"TIP ANGAJAT"'); const sel=encodeURIComponent('USER,"NUME COMPLET USER","TIP ANGAJAT"'); const qs='select='+sel+'&'+col+'=in.('+['CONSILIER','ASISTENT'].map(encodeURIComponent).join(',')+')&order='+encodeURIComponent('USER.asc'); const a=supaSelectAll_(SUPA_USERS_TABLE,qs,1000)||[]; return{ok:true,rows:a.map(r=>({user:r.USER||'',nume:r['NUME COMPLET USER']||'',tip:r['TIP ANGAJAT']||''}))}; } // --- claim/release slot (lock doar pe alocare) --- function _bc_claimWs_(who,waitMs){ const props=PropertiesService.getScriptProperties(); const end=Date.now()+Math.max(0,Number(waitMs||12000)); for(;;){ const lock=LockService.getScriptLock(); lock.waitLock(5000); try{ const now=Date.now(); for(const name of BC_WS){ const k=BC_WS_KEY+name; let o=null; try{o=JSON.parse(props.getProperty(k)||'');}catch(_){} const exp=o&&Number(o.exp)||0; if(!exp||expend) return ''; Utilities.sleep(350); } } function _bc_releaseWs_(name){ if(!name) return; const lock=LockService.getScriptLock(); lock.waitLock(5000); try{ PropertiesService.getScriptProperties().deleteProperty(BC_WS_KEY+name); } finally{ try{lock.releaseLock();}catch(_){ } } } // --- upload XLSX (din UI) -> fișier temporar -> conversie Google Sheets --- function _bc_tmpFromUpload_(fd){ const base64=String(fd.data||'').split(',').pop(); const bytes=Utilities.base64Decode(base64); const mime=fd.mimeType||'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; const name=String(fd.filename||'bc.xlsx').replace(/[\\/:*?"<>|]+/g,'_'); const xFile=DriveApp.createFile(Utilities.newBlob(bytes,mime,name)); const gs=Drive.Files.copy({title:name.replace(/\.(xlsx|xls)$/i,'')+' [tmp]',mimeType:MimeType.GOOGLE_SHEETS},xFile.getId(),{supportsAllDrives:true}); return {xlsxId:xFile.getId(),gsId:gs.id}; } // --- citește exact 2999x51 din primul sheet --- function _bc_readMatrix_(sh){ if(sh.getMaxRows()String(v||'').trim()!=='')) break; if(Date.now()-t0>12000) break; Utilities.sleep(400); } const row={}; for(let i=0;i=300) throw new Error('BC_SUPA|UPDATE|'+r.getContentText()); return {ok:true,action:'UPDATE',cnp}; } else { const r=UrlFetchApp.fetch(SUPA_URL+'/rest/v1/'+BC_TBL,{method:'post',contentType:'application/json',headers:h,payload:JSON.stringify([rowObj]),muteHttpExceptions:true}); if(r.getResponseCode()>=300) throw new Error('BC_SUPA|INSERT|'+r.getContentText()); return {ok:true,action:'INSERT',cnp}; } } // --- cleanup workspace --- function _bc_wsCleanup_(sh,aux){ try{ sh.getRange(BC_PASTE_R,1,BC_SRC_R,BC_SRC_C).clearContent(); }catch(_){} try{ if(aux&&aux.r&&aux.c) sh.getRange(BC_AUX_PASTE_R,BC_AUX_PASTE_C,aux.r,aux.c).clearContent(); }catch(_){} SpreadsheetApp.flush(); } /** * PUBLIC: Procesare XLSX (buton "PROCESEAZĂ BC") * - alege workspace liber (Extrageri BC 1..5) * - convertește XLSX -> GS * - paste A1:AY2999 în A2.. din workspace * - citește A3003:AY3003 (prin headerele din 3002) * - UPSERT în bd_interogari_bc */ function bc_processXlsxStandard(token,idInterogare,meta,fd){ const sess=_getSession(token); idInterogare=Number(idInterogare||0); if(!idInterogare) throw new Error('BC_PROC|ID interogare invalid.'); if(!fd||!fd.data||!fd.filename) throw new Error('BC_PROC|Fișier XLSX lipsă.'); const ws=_bc_claimWs_(sess.user,12000); if(!ws) throw new Error('BC_POOL|Toate sloturile de procesare sunt ocupate. Reîncearcă în 1-2 minute.'); const ext=SpreadsheetApp.openById(BC_EXT_SS_ID); const sh=ext.getSheetByName(ws); if(!sh){ _bc_releaseWs_(ws); throw new Error('BC_POOL|Nu găsesc sheet-ul: '+ws); } let tmp={xlsxId:'',gsId:''},aux={r:0,c:0}; try{ tmp=_bc_tmpFromUpload_(fd); const tmpSs=SpreadsheetApp.openById(tmp.gsId),src=tmpSs.getSheets()[0]; const values=_bc_readMatrix_(src); try{ _bc_wsPaste_(sh,values); } catch(e){ throw new Error('BC_PASTE|dst_write|'+((e&&e.message)||String(e))); } aux=_bc_wsAuxCopy_(sh); const row=_bc_wsReadResult_(sh); const up=_bc_upsertInterogari_(row); return {ok:true,cnp:up.cnp,action:up.action,ws:ws}; } finally { try{ _bc_wsCleanup_(sh,aux); }catch(_){} try{ if(tmp.gsId) DriveApp.getFileById(tmp.gsId).setTrashed(true); }catch(_){} try{ if(tmp.xlsxId) DriveApp.getFileById(tmp.xlsxId).setTrashed(true); }catch(_){} try{ _bc_releaseWs_(ws); }catch(_){} } } function bc_findOpenSolicitare_(cnp, tip){ cnp=String(cnp||'').replace(/\D+/g,''); tip=String(tip||'').trim().toUpperCase(); if(!cnp) return null; let qs='select=id_interogare,status,asistent,agent,client,data,tip_bc&cnp=eq.'+encodeURIComponent(cnp)+ '&order='+encodeURIComponent('id_interogare')+'.desc&limit=200'; if(tip) qs+='&tip_bc=eq.'+encodeURIComponent(tip); const a=supaGet_(SUPA_SOLICITARI_BC,qs)||[]; for(const r of a){ const s=String(r.status||'').trim().toUpperCase(); if(!s || (s!=='REZOLVAT' && s!=='ESUAT')) return r; } return null; } function sf_list(token){ const sess=_getSession(token);_ensureAdmin_(sess); const a=supaSelectAll_(SUPA_FUNCTII_SUPABASE,'select=id,tip,nume,structura,functionare&order=id.asc',2000)||[]; return{ok:true,rows:a.map(r=>({id:r.id,tip:r.tip||'',nume:r.nume||'',structura:r.structura||'',functionare:r.functionare||''}))}; } function sf_add(token,p){ const sess=_getSession(token);_ensureAdmin_(sess);p=p||{}; const tip=String(p.tip||'').trim().toUpperCase(),nume=String(p.nume||'').trim(); if(!['FUNCTIE','TRIGGER','CRONJOB'].includes(tip)) return{ok:false,msg:'TIP invalid (FUNCTIE/TRIGGER/CRONJOB).'}; if(!nume) return{ok:false,msg:'Completează NUME.'}; const ins=supaInsertOne_(SUPA_FUNCTII_SUPABASE,{ tip:tip,nume:nume, structura:String(p.structura||'').trim()||null, functionare:String(p.functionare||'').trim()||null }); return{ok:true,id:ins.id||null}; } function sf_update(token,id,p){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0);if(!id) return{ok:false,msg:'ID invalid.'}; p=p||{};const body={}; if(p.hasOwnProperty('tip')){ const t=String(p.tip||'').trim().toUpperCase(); if(!['FUNCTIE','TRIGGER','CRONJOB'].includes(t)) return{ok:false,msg:'TIP invalid (FUNCTIE/TRIGGER/CRONJOB).'}; body.tip=t; } if(p.hasOwnProperty('nume')){ const n=String(p.nume||'').trim(); if(!n) return{ok:false,msg:'NUME nu poate fi gol.'}; body.nume=n; } if(p.hasOwnProperty('structura')) body.structura=String(p.structura||'').trim()||null; if(p.hasOwnProperty('functionare')) body.functionare=String(p.functionare||'').trim()||null; if(!Object.keys(body).length) return{ok:true}; const url=`${SUPA_URL}/rest/v1/${SUPA_FUNCTII_SUPABASE}?id=eq.${encodeURIComponent(id)}`; const res=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify(body),muteHttpExceptions:true}); if(res.getResponseCode()>=300) return{ok:false,msg:'Supabase UPDATE: '+res.getContentText()}; return{ok:true}; } function sf_delete(token,id){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0);if(!id) return{ok:false,msg:'ID invalid.'}; const url=`${SUPA_URL}/rest/v1/${SUPA_FUNCTII_SUPABASE}?id=eq.${encodeURIComponent(id)}`; const res=UrlFetchApp.fetch(url,{method:'delete',headers:supaHeaders_(false),muteHttpExceptions:true}); if(res.getResponseCode()>=300) return{ok:false,msg:'Supabase DELETE: '+res.getContentText()}; return{ok:true}; } function fi_list(token){ const sess=_getSession(token);_ensureAdmin_(sess); const qs='select='+encodeURIComponent('id,denumire_flux,mod_de_functionare,legaturi_cu')+'&order=id.desc&limit=2000'; const a=supaGet_(SUPA_FLUXURI_INTERNE,qs)||[]; return{ok:true,rows:a.map(r=>({id:r.id,denumire:r.denumire_flux||'',mod:r.mod_de_functionare||'',legaturi:r.legaturi_cu||''}))}; } function fi_add(token,p){ const sess=_getSession(token);_ensureAdmin_(sess); p=p||{}; const den=String(p.denumire||'').trim(),mod=String(p.mod||'').trim(),leg=String(p.legaturi||'').trim(); if(!den||!mod||!leg) return{ok:false,msg:'Completeaza toate campurile.'}; const ins=supaInsertOne_(SUPA_FLUXURI_INTERNE,{denumire_flux:den,mod_de_functionare:mod,legaturi_cu:leg}); return{ok:true,id:(ins&&ins.id)||null}; } function fi_update(token,id,p){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0); if(!id) return{ok:false,msg:'ID invalid.'}; p=p||{}; const patch={ denumire_flux:String(p.denumire||'').trim(), mod_de_functionare:String(p.mod||'').trim(), legaturi_cu:String(p.legaturi||'').trim() }; const url=SUPA_URL+'/rest/v1/'+SUPA_FLUXURI_INTERNE+'?id=eq.'+encodeURIComponent(String(id)); const r=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify(patch),muteHttpExceptions:true}); if(r.getResponseCode()>=300) return{ok:false,msg:r.getContentText()}; return{ok:true}; } function fi_delete(token,id){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0); if(!id) return{ok:false,msg:'ID invalid.'}; const url=SUPA_URL+'/rest/v1/'+SUPA_FLUXURI_INTERNE+'?id=eq.'+encodeURIComponent(String(id)); const r=UrlFetchApp.fetch(url,{method:'delete',headers:supaHeaders_(false),muteHttpExceptions:true}); if(r.getResponseCode()>=300) return{ok:false,msg:r.getContentText()}; return{ok:true}; } function ts_list(token){ const sess=_getSession(token);_ensureAdmin_(sess); const qs='select='+encodeURIComponent('id,denumire_tabel,utilizare,structura')+'&order=id.desc&limit=2000'; const a=supaGet_(SUPA_TABELE_SUPABASE,qs)||[]; return{ok:true,rows:a.map(r=>({id:r.id,denumire:r.denumire_tabel||'',utilizare:r.utilizare||'',structura:r.structura||''}))}; } function ts_add(token,p){ const sess=_getSession(token);_ensureAdmin_(sess);p=p||{}; const den=String(p.denumire||'').trim(); if(!den) return{ok:false,msg:'Completeaza DENUMIRE TABEL.'}; const ins=supaInsertOne_(SUPA_TABELE_SUPABASE,{denumire_tabel:den,utilizare:String(p.utilizare||'').trim()||null,structura:String(p.structura||'').trim()||null}); return{ok:true,id:(ins&&ins.id)||null}; } function ts_update(token,id,p){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0); if(!id) return{ok:false,msg:'ID invalid.'}; p=p||{}; const body={}; if(p.hasOwnProperty('denumire')) body.denumire_tabel=String(p.denumire||'').trim()||null; if(p.hasOwnProperty('utilizare')) body.utilizare=String(p.utilizare||'').trim()||null; if(p.hasOwnProperty('structura')) body.structura=String(p.structura||'').trim()||null; if(!Object.keys(body).length) return{ok:true}; const url=SUPA_URL+'/rest/v1/'+SUPA_TABELE_SUPABASE+'?id=eq.'+encodeURIComponent(String(id)); const r=UrlFetchApp.fetch(url,{method:'patch',contentType:'application/json',headers:supaHeaders_(false),payload:JSON.stringify(body),muteHttpExceptions:true}); if(r.getResponseCode()>=300) return{ok:false,msg:'Supabase UPDATE: '+r.getContentText()}; return{ok:true}; } function ts_delete(token,id){ const sess=_getSession(token);_ensureAdmin_(sess); id=Number(id||0); if(!id) return{ok:false,msg:'ID invalid.'}; const url=SUPA_URL+'/rest/v1/'+SUPA_TABELE_SUPABASE+'?id=eq.'+encodeURIComponent(String(id)); const r=UrlFetchApp.fetch(url,{method:'delete',headers:supaHeaders_(false),muteHttpExceptions:true}); if(r.getResponseCode()>=300) return{ok:false,msg:'Supabase DELETE: '+r.getContentText()}; return{ok:true}; } // ================== CLIENTI PIERDUTI (ADMIN, paginare 1000) ================== function supaCountExact_(table, qs){ const url=SUPA_URL+'/rest/v1/'+table+(qs?('?'+qs):''); const opt={method:'get',headers:supaHeaders_(true),muteHttpExceptions:true}; opt.headers['Range-Unit']='items'; opt.headers['Range']='0-0'; const res=UrlFetchApp.fetch(url,opt); const code=res.getResponseCode(); if(code>=300 && code!==416) throw new Error('Supabase COUNT: '+res.getContentText()); const h=res.getAllHeaders()||{}; const cr=h['Content-Range']||h['content-range']||''; const m=String(cr).match(/\/(\d+)\s*$/); return m?Number(m[1]):0; } function pierduti_list(token,F,page,limit,wantCount){ const sess=_getSession(token); if(String(sess.role||'').toLowerCase()!=='administrator') return{ok:false,msg:'Doar Administratorul poate accesa Clienți Pierduți.'}; F=F||{}; page=Math.max(1,Number(page||1)); limit=Math.min(1000,Math.max(1,Number(limit||1000))); const T=SUPA_TABLE_PIERDUTI,tz=Session.getScriptTimeZone()||'Europe/Bucharest'; const dmy=v=>{v=String(v||'').trim(); if(!v) return ''; if(/^\d{2}\.\d{2}\.\d{4}$/.test(v)) return v; if(/^\d{4}-\d{2}-\d{2}$/.test(v)){const m=v.split('-'); return m[2]+'.'+m[1]+'.'+m[0];} const d=new Date(v); return isNaN(d)?v:Utilities.formatDate(d,tz,'dd.MM.yyyy');}; const isoFromDmy=v=>{const m=String(v||'').trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/); return m?`${m[3]}-${m[2]}-${m[1]}`:(_toIsoDateFromRo_(v)||'');}; const dig=s=>String(s||'').replace(/\D+/g,''); const age=cnp=>{cnp=dig(cnp); if(cnp.length!==13) return ''; const s=+cnp[0],yy=+cnp.slice(1,3),mm=+cnp.slice(3,5),dd=+cnp.slice(5,7); let y=1900+yy; if(s===3||s===4) y=1800+yy; else if(s===5||s===6) y=2000+yy; const b=new Date(y,mm-1,dd); if(isNaN(b)) return ''; const t=new Date(); let a=t.getFullYear()-b.getFullYear(); const m=t.getMonth()-b.getMonth(); if(m<0||(m===0&&t.getDate()=0&&a<=120)?String(a):'';}; const conds=[]; const inList=(col,arr,conv)=>{arr=(arr||[]).map(x=>String(x||'').trim()).filter(Boolean); if(!arr.length) return; const v=arr.map(x=>conv?conv(x):x).filter(Boolean); if(!v.length) return; conds.push(col+'=in.('+encodeURIComponent(v.join(','))+')');}; const ilike=(col,txt)=>{txt=String(txt||'').trim(); if(txt) conds.push(col+'=ilike.*'+encodeURIComponent(txt)+'*');}; const eq=(col,txt)=>{txt=String(txt||'').trim(); if(txt) conds.push(col+'=eq.'+encodeURIComponent(txt));}; // toolbar const cnp=dig(F.cnp),tel=dig(F.tel),mail=String(F.mail||'').trim().toLowerCase(),name=String(F.name||'').trim(); if(cnp) conds.push('cnp=like.*'+encodeURIComponent(cnp)+'*'); if(tel) conds.push('telefon=like.*'+encodeURIComponent(tel)+'*'); if(mail) conds.push('or='+encodeURIComponent(`(e_mail.ilike.*${mail}*,e_mail_creat.ilike.*${mail}*)`)); if(name){ const toks=name.split(/\s+/).map(x=>x.trim()).filter(Boolean).slice(0,6); if(toks.length) conds.push('and='+encodeURIComponent('('+toks.map(t=>`nume_complet.ilike.*${t.toLowerCase()}*`).join(',')+')')); } // header text filters ilike('nume_complet',F.text0); ilike('e_mail_creat',F.text2); ilike('observatii_aratate',F.textObs); // VARSTA (text1) – filtrare pe data_nasterii (exact age) const av=dig(F.text1); if(av&&/^\d{1,3}$/.test(av)){ const A=Number(av); if(A>=0&&A<=120){ const t0=new Date(); t0.setHours(0,0,0,0); const end=new Date(t0); end.setFullYear(end.getFullYear()-A); const start=new Date(t0); start.setFullYear(start.getFullYear()-A-1); start.setDate(start.getDate()+1); const ymd=d=>Utilities.formatDate(d,tz,'yyyy-MM-dd'); conds.push('data_nasterii=gte.'+ymd(start)); conds.push('data_nasterii=lte.'+ymd(end)); } } // multi filters inList('agent',F.agentList); inList('judet_ci_munca',F.judetList); inList('status',F.statusList); inList('status_secundar',F.status2List); // date filters (UI trimite dd.MM.yyyy) inList('data_client',F.dcList,isoFromDmy); inList('dua',F.duaList,isoFromDmy); inList('dva',F.dvaList,isoFromDmy); // single selects eq('raport_bc',F.raport); eq('status_bc',F.sbc); eq('potential_refin',F.prefin); eq('tipologie_client',F.tip); // venit bucket (dacă e numeric în Supabase) const vb=String(F.venitBucket||'').trim(); if(vb){ const map={lt3000:'valoare_venit=lt.3000',b3000_5000:'and=(valoare_venit=gte.3000,valoare_venit=lt.5000)',b5000_7500:'and=(valoare_venit=gte.5000,valoare_venit=lt.7500)',b7500_10000:'and=(valoare_venit=gte.7500,valoare_venit=lte.10000)',gt10000:'valoare_venit=gt.10000'}; if(map[vb]) conds.push(map[vb]); } // delay const dl=String(F.delay||'').trim(); if(dl){ if(dl==='ge5') conds.push('delay=gte.5'); else if(/^\d+$/.test(dl)) conds.push('delay=eq.'+encodeURIComponent(dl)); } const select=encodeURIComponent('id_unic,agent,data_client,nume_complet,cnp,telefon,e_mail,e_mail_creat,judet_ci_munca,valoare_venit,status,dua,status_secundar,dva,dva_maxim,delay,raport_bc,status_bc,potential_refin,tipologie_client,observatii_aratate'); const qs='select='+select+(conds.length?'&'+conds.join('&'):'')+'&order='+encodeURIComponent('data_client')+'.desc,'+encodeURIComponent('id_unic')+'.desc'; // count exact (doar când vrei pager total) let total=null,pages=null; if(wantCount){ const url=SUPA_URL+'/rest/v1/'+T+'?'+qs+'&limit=1'; const h=supaHeaders_(true); h['Range-Unit']='items'; h['Range']='0-0'; const r=UrlFetchApp.fetch(url,{method:'get',headers:h,muteHttpExceptions:true}); if(r.getResponseCode()>=300) throw new Error('Supabase COUNT: '+r.getContentText()); const cr=(r.getHeaders()['Content-Range']||''); // ex: 0-0/58889 const m=String(cr).match(/\/(\d+)$/); total=m?Number(m[1]):0; pages=total?Math.max(1,Math.ceil(total/limit)):0; } // page fetch const from=(page-1)*limit,to=from+limit-1; const url2=SUPA_URL+'/rest/v1/'+T+'?'+qs; const h2=supaHeaders_(false); h2['Range-Unit']='items'; h2['Range']=from+'-'+to; const r2=UrlFetchApp.fetch(url2,{method:'get',headers:h2,muteHttpExceptions:true}); if(r2.getResponseCode()>=300) throw new Error('Supabase LIST: '+r2.getContentText()); const arr=JSON.parse(r2.getContentText()||'[]'); const rows=(arr||[]).map(x=>{ const out=new Array(22).fill(''); out[0]=x.nume_complet||''; out[1]=age(x.cnp); out[2]=x.e_mail_creat||''; out[3]=x.judet_ci_munca||''; out[4]=(x.valoare_venit!=null?String(x.valoare_venit):''); out[5]=x.status||''; out[6]=dmy(x.dua); out[7]=x.status_secundar||''; out[8]=dmy(x.dva); out[9]=dmy(x.dva_maxim); out[10]=(x.delay!=null?String(x.delay):''); out[11]=x.raport_bc||''; out[12]=x.status_bc||''; out[13]=x.potential_refin||''; out[14]=x.tipologie_client||''; out[15]=x.observatii_aratate||''; out[16]=x.cnp||''; out[17]=x.telefon||''; out[18]=String(x.id_unic||''); out[19]=x.agent||''; out[20]=x.e_mail||''; out[21]=dmy(x.data_client); return out; }); return{ok:true,page:page,limit:limit,rows:rows,total:total,pages:pages}; } function _pOne_(id,sel){const qs='id_unic=eq.'+encodeURIComponent(String(id||'').replace(/\D+/g,''))+'&select='+encodeURIComponent(sel||'*')+'&limit=1';const a=supaGet_(SUPA_TABLE_PIERDUTI,qs);return a&&a[0]||null;} function cp_gest_getBasic(token,id){_getSession(token);id=String(id||'').replace(/\D+/g,'');if(!id) return{ok:false,msg:'ID UNIC lipsă.'};const r=_pOne_(id,'id_unic,agent,data_client,nume_complet,cnp,telefon,judet_ci_munca,status,dua,status_secundar,dva,dva_maxim,raport_bc,observatii_aratate');if(!r) return{ok:false,msg:'ID UNIC inexistent.'};return{ok:true,data:{id:id,agentOfRow:r.agent||'',nume:r.nume_complet||'',tel:r.telefon||'',cnp:r.cnp||'',judetCIMunca:r.judet_ci_munca||'',status:r.status||'',status2:r.status_secundar||'',dua:r.dua||'',dva:r.dva||'',dvaMax:r.dva_maxim||'',raport:r.raport_bc||'',obsFromList:r.observatii_aratate||''}};} function cp_gest_getDataClient(token,id){_getSession(token);const r=_pOne_(id,'data_client');const txt=r&&r.data_client?String(r.data_client).substr(0,10):'';return{ok:true,data_client:txt,data_client_iso:txt};} function cp_gest_getInfoSuplimentare(token,id){_getSession(token);const r=_pOne_(id,'info_suplimentare');return r?{ok:true,info:String(r.info_suplimentare||'')}:{ok:false,msg:'Client inexistent.'};} function cp_gest_getMoreObs(token,id){_getSession(token);const r=_pOne_(id,'observatii');return r?{ok:true,obs:String(r.observatii||'')}:{ok:false,msg:'Client inexistent.'};} function _lastLines_(t,n){const a=String(t||'').split(/\r?\n/).map(x=>String(x).trim()).filter(Boolean);return a.slice(Math.max(0,a.length-Number(n||5))).join('\n');} function editor_saveAllPierduti(token,id,rowHint,payload){ const sess=_getSession(token); id=String(id||'').replace(/\D+/g,''); if(!id) throw new Error('ID UNIC lipsă.'); const cur=_pOne_(id,'observatii'); if(!cur) throw new Error('ID UNIC inexistent.'); const g=(payload&&payload.gest)||{}; const tz=Session.getScriptTimeZone()||'Europe/Bucharest'; const stamp=Utilities.formatDate(new Date(),tz,'dd.MM.yyyy'); const who=String(sess.user||'').trim()||'USER'; const add=String(g.observatiiNoi||'').trim(); let full=String(cur.observatii||''); if(add){ if(full && !/\n$/.test(full)) full+='\n'; full+=stamp+' - '+who+' - '+add; } const p={id_unic:id, status:String(g.status||'').trim()||null, dua:String(g.dua||'').trim()||null, status_secundar:String(g.statusSecundar||'').trim()||null, dva:String(g.dva||'').trim()||null, dva_maxim:String(g.dvaMaxim||'').trim()||null, raport_bc:String(g.raportBC||'').trim()||null, observatii:full||null, observatii_aratate:_lastLines_(full,5)||null }; supaUpsert_(SUPA_TABLE_PIERDUTI,[p],'id_unic'); return{ok:true}; } function getEditorUrlPierduti(token,id){ _getSession(token); id=String(id||'').replace(/\D+/g,''); if(!id)throw new Error('ID UNIC lipsă.'); if(!_pOne_(id,'id_unic'))throw new Error('ID UNIC inexistent.'); return getEditorUrl(token,id)+'&src=cp'; } function pierduti_facets(token,F){ const sess=_getSession(token); // ✅ guard real if(String(sess.role||'').toLowerCase()!=='administrator') return {ok:false,msg:'Doar Administratorul poate accesa Clienți Pierduți.'}; F=F||{}; const url=SUPA_URL+'/rest/v1/rpc/cp_pierduti_facets'; const opt={method:'post',muteHttpExceptions:true,headers:{apikey:SUPA_KEY,Authorization:'Bearer '+SUPA_KEY,'Content-Type':'application/json'},payload:JSON.stringify({p:F})}; const r=UrlFetchApp.fetch(url,opt),code=r.getResponseCode(),txt=r.getContentText(); if(code<200||code>=300) return {ok:false,msg:'RPC facets failed',code,txt}; let x=null; try{x=JSON.parse(txt);}catch(e){return {ok:false,msg:'RPC JSON parse failed',code,txt,err:String(e)};} if(Array.isArray(x)) x=x[0]||{}; if(x&&typeof x==='object'){ if(x.cp_pierduti_facets!=null) x=x.cp_pierduti_facets; else if(x.result!=null) x=x.result; } return {ok:true,facets:x||{}}; } /** ================== BC HELPERS (STANDARD + SCORERISE) ================== **/ function bc__today_(){return Utilities.formatDate(new Date(),Session.getScriptTimeZone()||'Europe/Bucharest','yyyy-MM-dd');} function bc__sess_(token){ token=String(token||'').trim(); if(!token) return null; const pref=(typeof SESSION_PREFIX!=='undefined')?SESSION_PREFIX:'sess:'; const cache=CacheService.getScriptCache(); const raw=cache.get(pref+token)||cache.get(token)||''; if(!raw) return null; try{return JSON.parse(raw);}catch(_){return {user:raw,role:''};} } function bc__who_(token){ const s=bc__sess_(token); if(!s) return null; const user=String(s.user||s.username||s.u||'').trim(); const role=String(s.role||s.r||'').trim(); return {user,role}; } function bc__supaFetch_(method,path,qsObj,payload,extraH){ const urlBase=(typeof SUPA_URL!=='undefined')?SUPA_URL:PropertiesService.getScriptProperties().getProperty('SUPA_URL'); const key=(typeof SUPA_KEY!=='undefined')?SUPA_KEY:PropertiesService.getScriptProperties().getProperty('SUPA_KEY'); if(!urlBase||!key) throw new Error('SUPA_URL/SUPA_KEY lipsă.'); const qs=qsObj?Object.keys(qsObj).filter(k=>qsObj[k]!=null&&qsObj[k]!=='' ).map(k=>encodeURIComponent(k)+'='+encodeURIComponent(qsObj[k])).join('&'):''; const url=urlBase.replace(/\/$/,'')+path+(qs?('?'+qs):''); const h={apikey:key,authorization:'Bearer '+key}; if(extraH) Object.keys(extraH).forEach(k=>h[k]=extraH[k]); const opt={method:method,headers:h,muteHttpExceptions:true}; if(payload!=null){opt.contentType='application/json';opt.payload=JSON.stringify(payload);} const r=UrlFetchApp.fetch(url,opt); const code=r.getResponseCode(),txt=r.getContentText()||''; let j=null; try{j=txt?JSON.parse(txt):null;}catch(_){} if(code<200||code>=300) throw new Error('SUPA '+code+' '+txt); return j; } function bc__supaSelect_(table,filters,select,limit,order){ const q={select:select||'*'}; if(order) q.order=order; if(limit) q.limit=String(limit); if(filters) Object.keys(filters).forEach(k=>{ if(filters[k]!=null&&filters[k]!=='') q[k]=filters[k]; }); return bc__supaFetch_('get',`/rest/v1/${encodeURIComponent(table)}`,q,null,null); } // STANDARD: există azi solicitare NE-REZOLVATĂ? function bc__hasOpenStandardToday_(cnp,today){ const rows=bc__supaSelect_('bd_solicitari_bc',{cnp:'eq.'+cnp,data:'eq.'+today,status:'neq.REZOLVAT'},'id_interogare,status',1,'id_interogare.desc'); return Array.isArray(rows)&&rows.length>0; } // SCORERISE: există azi ORICE solicitare? (indiferent de status) function bc__hasAnyToday_(cnp,today){ const rows=bc__supaSelect_('bd_solicitari_bc',{cnp:'eq.'+cnp,data:'eq.'+today},'id_interogare,status',1,'id_interogare.desc'); return Array.isArray(rows)&&rows.length>0; } // Cel mai recent CI JPEG (doar bd_documente_clienti, fără storage-check) function bc__latestCiJpeg_(cnp){ const rows=bc__supaSelect_('bd_documente_clienti',{ cnp:'eq.'+cnp, tip_document:'eq.CARTE DE IDENTITATE', mime_type:'eq.image/jpeg' },'path,filename,data_incarcare,mime_type',1,'data_incarcare.desc'); return (Array.isArray(rows)&&rows[0])?rows[0]:null; } // Apel Edge (server-side) – interogari_scorerise function bc__callEdgeScorerise_(payload){ const urlBase=(typeof SUPA_URL!=='undefined')?SUPA_URL:PropertiesService.getScriptProperties().getProperty('SUPA_URL'); const key=(typeof SUPA_KEY!=='undefined')?SUPA_KEY:PropertiesService.getScriptProperties().getProperty('SUPA_KEY'); const url=urlBase.replace(/\/$/,'')+'/functions/v1/interogari_scorerise'; const r=UrlFetchApp.fetch(url,{ method:'post', contentType:'application/json', payload:JSON.stringify(payload), muteHttpExceptions:true, headers:{apikey:key,authorization:'Bearer '+key} }); const code=r.getResponseCode(),txt=r.getContentText()||''; let j=null; try{j=txt?JSON.parse(txt):null;}catch(_){} if(code<200||code>=300) throw new Error('EDGE '+code+' '+txt); return j; } function anaf_scorerise_start(token,p){ const sess=_sessGet_(token); const cnp=String((p&&p.cnp)||'').replace(/\D+/g,''); const numeClient=String((p&&p.numeClient)||'').trim()||'CLIENT'; if(cnp.length!==13) return {ok:false,msg:'CNP invalid.'}; const ciPath=_anaf_getLatestCiJpgPath_(cnp); const url=_supaUrl_()+'/functions/v1/interogari_anaf_scorerise'; const body={cnp,numeClient,ci_path:ciPath,sursa:(sess.user||sess.username||'CRM')}; const r=UrlFetchApp.fetch(url,{ method:'post', contentType:'application/json', muteHttpExceptions:true, headers:{Authorization:'Bearer '+_supaKey_(),apikey:_supaKey_()}, payload:JSON.stringify(body) }); const code=r.getResponseCode(),txt=r.getContentText(); let j=null; try{j=JSON.parse(txt);}catch(_){} if(code<200||code>=300||!j||j.ok!==true) return {ok:false,msg:(j&&j.error)||txt||('HTTP '+code)}; return j; } function _anaf_getLatestCiJpgPath_(cnp){ const base=_supaUrl_()+'/rest/v1/bd_documente_clienti'; const tip=encodeURIComponent('CARTE DE IDENTITATE'); const q1=base+'?select=path,filename,data_incarcare,mime_type&cnp=eq.'+encodeURIComponent(cnp)+'&tip_document=eq.'+tip+'&mime_type=eq.image/jpeg&order=data_incarcare.desc&limit=1'; const a1=_supaGet_(q1); if(a1&&a1.length&&a1[0]&&a1[0].path) return String(a1[0].path); const q2=base+'?select=path,filename,data_incarcare,mime_type&cnp=eq.'+encodeURIComponent(cnp)+'&tip_document=eq.'+tip+'&order=data_incarcare.desc&limit=1'; const a2=_supaGet_(q2); if(a2&&a2.length&&a2[0]&&a2[0].path) return String(a2[0].path); throw new Error('Nu există „CARTE DE IDENTITATE” în documente_clienti pentru acest CNP.'); } function _supaGet_(url){ const r=UrlFetchApp.fetch(url,{method:'get',muteHttpExceptions:true,headers:{Authorization:'Bearer '+_supaKey_(),apikey:_supaKey_()}}); const code=r.getResponseCode(),txt=r.getContentText(); if(code<200||code>=300) throw new Error('SUPA_GET_FAIL '+code+' '+txt); try{return JSON.parse(txt);}catch(_){return [];} } function _supaUrl_(){ const sp=PropertiesService.getScriptProperties(); return (typeof SUPA_URL!=='undefined'&&SUPA_URL)||sp.getProperty('SUPA_URL')||''; } function _supaKey_(){ const sp=PropertiesService.getScriptProperties(); return (typeof SUPA_KEY!=='undefined'&&SUPA_KEY)||sp.getProperty('SUPA_KEY')||''; } function _sessGet_(token){ const pref=(typeof SESSION_PREFIX!=='undefined'&&SESSION_PREFIX)||'sess:'; const raw=CacheService.getScriptCache().get(pref+token); if(!raw) throw new Error('Sesiune invalidă.'); try{return JSON.parse(raw);}catch(_){return {user:''};} }