Connecting to your workspace…
$ $ $
Your projects, reports and invoices — brought to light in one place.

Customer sign in

Access your client workspace
'); w.document.close(); } function payInvoiceFromView(id){ var all=TA.getInvoices(); all.forEach(function(x){ if(x.id===id){ x.status='paid'; x.notes='Paid online via portal'; } }); TA.saveInvoices(all); _closeModal(); if(typeof toast==='function') toast('Payment successful. Thank you!'); if(typeof renderInvoices==='function') renderInvoices(); if(typeof renderOv==='function') renderOv(); } /* ---------- USER DETAIL POPUP (admin) ---------- */ function viewUser(id){ var u = TA.getUserById(id); if(!u) return; var isEmp = u.role==='employee'; var avCls = isEmp ? 'avb' : 'avp'; var rows = []; function row(k,v){ if(v!=null&&v!=='') rows.push('
'+k+'
'+_esc(v)+'
'); } if(isEmp){ row('Department', u.department); row('Designation', u.designation); row('Email', u.email); row('Joining date', u.joining?TA.date(u.joining):''); // work stats var tasks = TA.getTasks().filter(function(t){return t.assigneeId===u.id;}); var reps = TA.getReports().filter(function(r){return r.employeeId===u.id;}); var att = TA.getAttendance().filter(function(a){return a.employeeId===u.id;}); var hrs = att.reduce(function(s,a){return s+(a.hours||0);},0); var doneN = tasks.filter(function(t){return t.status==='done';}).length; var stat = '
'+ '
'+tasks.length+'
Tasks ('+doneN+' done)
'+ '
'+reps.length+'
Reports
'+ '
'+(Math.round(hrs*10)/10)+'h
Hours logged
'+ '
'; var body = '
'+_esc(u.avatar)+'
'+ '
'+_esc(u.name)+'
'+ '
'+_esc(u.designation||'Employee')+' · '+(u.active?'Active':'Inactive')+'
'+ stat + '
'+rows.join('')+'
'+ '
'+ '
'; _modal('
Employee details
'+body); } else { row('Company', u.company); row('Industry', u.industry); row('Email', u.email); row('Phone', u.phone); row('GSTIN', u.gstin); // client stats var projs = TA.getProjects().filter(function(p){return p.clientId===u.id;}); var invs = TA.getInvoices().filter(function(i){return i.clientId===u.id;}); var tks = TA.getTickets().filter(function(t){return t.clientId===u.id;}); var billed = invs.reduce(function(s,i){return s+i.total;},0); var due = invs.filter(function(i){return i.status!=='paid';}).reduce(function(s,i){return s+i.total;},0); var stat = '
'+ '
'+projs.length+'
Projects
'+ '
'+TA.inr(billed)+'
Total billed
'+ '
'+TA.inr(due)+'
Outstanding
'+ '
'; var plist=''; projs.forEach(function(p){ plist+='
'+_esc(p.name)+'
'+p.progress+'% · '+_esc(p.status)+'
'; }); var body = '
'+_esc(u.avatar)+'
'+ '
'+_esc(u.company||u.name)+'
'+ '
'+_esc(u.industry||'Customer')+' · '+(u.active?'Active':'Inactive')+'
'+ stat + '
'+rows.join('')+'
'+ (plist?'
Projects
'+plist+'
':'')+ '
Tickets
Support tickets
'+tks.length+' total, '+tks.filter(function(t){return t.status!=='closed'&&t.status!=='resolved';}).length+' open
'+ '
'+ '
'; _modal('
Customer details
'+body); } } /* ============================================================ Report rollup engine Employees submit DAILY reports only. This module generates weekly / monthly / yearly summaries on the fly from those. Requires TA. Exposes ROLLUP.build(dailyReports, period). ============================================================ */ var ROLLUP = { // ISO-ish week key: YYYY-Www (week starting Monday) weekKey: function(dateStr){ var d=new Date(dateStr+'T00:00:00'); var day=(d.getDay()+6)%7; // Mon=0..Sun=6 var mon=new Date(d); mon.setDate(d.getDate()-day); // week number within the year var jan1=new Date(mon.getFullYear(),0,1); var wk=Math.ceil((((mon-jan1)/86400000)+jan1.getDay()+1)/7); return mon.getFullYear()+'-W'+String(wk).padStart(2,'0'); }, weekRange: function(dateStr){ var d=new Date(dateStr+'T00:00:00'); var day=(d.getDay()+6)%7; var mon=new Date(d); mon.setDate(d.getDate()-day); var sun=new Date(mon); sun.setDate(mon.getDate()+6); return {from:mon.toISOString().slice(0,10), to:sun.toISOString().slice(0,10)}; }, monthKey: function(dateStr){ return dateStr.slice(0,7); }, yearKey: function(dateStr){ return dateStr.slice(0,4); }, periodLabel: function(period, key, sample){ if(period==='weekly'){ var r=this.weekRange(sample); return 'Week of '+TA.date(r.from); } if(period==='monthly'){ var d=new Date(key+'-01'); return d.toLocaleString('en-US',{month:'long',year:'numeric'}); } if(period==='yearly'){ return 'Year '+key; } return key; }, // group daily reports by period key, then aggregate each group build: function(dailies, period){ var self=this; var groups={}; dailies.forEach(function(r){ var d=r.date||r.submitted; if(!d) return; var key = period==='weekly'? self.weekKey(d) : period==='monthly'? self.monthKey(d) : self.yearKey(d); // also split by client so a rollup is per-client var gkey = key+'::'+(r.clientId||''); (groups[gkey]=groups[gkey]||[]).push(r); }); var out=[]; Object.keys(groups).forEach(function(gkey){ var items=groups[gkey].slice().sort(function(a,b){return (a.date||a.submitted)<(b.date||b.submitted)?-1:1;}); var key=gkey.split('::')[0]; var clientId=gkey.split('::')[1]; var sample=items[0].date||items[0].submitted; var backlinks=[], socialLinks=[], keywords={}, changes=[], traffic=[], images=0, time=0, attachments=[], campaignUpdates=[]; var empSet={}; items.forEach(function(r){ (r.backlinks||[]).forEach(function(b){backlinks.push(b);}); (r.socialLinks||[]).forEach(function(s){socialLinks.push(s);}); (r.keywords||'').split(',').forEach(function(k){k=k.trim();if(k)keywords[k]=true;}); if(r.websiteChanges) changes.push('• '+r.websiteChanges); if(r.trafficUpdate) traffic.push('• ('+TA.date(r.date||r.submitted)+') '+r.trafficUpdate); images+=(r.images||0); time+=(r.timeSpent||0); (r.attachments||[]).forEach(function(a){attachments.push(a);}); (r.campaignUpdates||[]).forEach(function(c){campaignUpdates.push(c);}); if(r.employeeId) empSet[r.employeeId]=true; }); var empIds=Object.keys(empSet); out.push({ id:'roll-'+period+'-'+gkey, rollup:true, period:period, type:period, periodKey:key, clientId:clientId, employeeId: empIds.length===1?empIds[0]:null, employeeIds: empIds, projectId: items[0].projectId, date: items[items.length-1].date||items[items.length-1].submitted, // latest day in period title: self.periodLabel(period, key, sample), dayCount: items.length, backlinks: backlinks, socialLinks: socialLinks, keywords: Object.keys(keywords).join(', '), websiteChanges: changes.join('\n'), trafficUpdate: traffic.join('\n'), images: images, imageNotes:'', attachments: attachments, campaignUpdates: campaignUpdates, weeklyProgress:'', timeSpent: Math.round(time*10)/10, submitted: items[items.length-1].submitted, reviewed: items.every(function(r){return r.reviewed;}) }); }); // newest period first out.sort(function(a,b){return b.dateto) return false; return true; }); }, // default range for a period around today (or an anchor) defaultRange: function(period, anchor){ var d=anchor?new Date(anchor+'T00:00:00'):new Date(); var from, to=d.toISOString().slice(0,10); if(period==='weekly'){ var w=new Date(d); w.setDate(d.getDate()-27); from=w.toISOString().slice(0,10); } // ~4 weeks else if(period==='monthly'){ var m=new Date(d); m.setMonth(d.getMonth()-5); m.setDate(1); from=m.toISOString().slice(0,10); } // ~6 months else { from=(d.getFullYear()-1)+'-01-01'; } // 2 years return {from:from, to:to}; }, // ---- ANALYTICS: keyword frequency + traffic trend across dailies ---- analytics: function(dailies){ var kw={}, totalBacklinks=0, totalImages=0, totalHours=0, totalSocial=0, days=0; var trafficPoints=[]; // {date, note} var numbers=[]; // extracted numeric signals (percentages) dailies.forEach(function(r){ days++; (r.keywords||'').split(',').forEach(function(k){ k=k.trim(); if(k){ kw[k]=(kw[k]||0)+1; } }); totalBacklinks+=(r.backlinks||[]).length; totalImages+=(r.images||0); totalHours+=(r.timeSpent||0); totalSocial+=(r.socialLinks||[]).length; if(r.trafficUpdate){ trafficPoints.push({date:r.date||r.submitted, note:r.trafficUpdate}); // pull first percentage mentioned, if any, for a trend line var m=(r.trafficUpdate.match(/(-?\d+(?:\.\d+)?)\s*%/)); if(m) numbers.push({date:r.date||r.submitted, val:parseFloat(m[1])}); } }); // top keywords sorted by frequency var kwList=Object.keys(kw).map(function(k){return {kw:k, n:kw[k]};}) .sort(function(a,b){return b.n-a.n;}); numbers.sort(function(a,b){return a.dateNo numeric traffic signals in this period.'; var vals=nums.map(function(x){return x.val;}); var min=Math.min.apply(null, vals.concat([0])), max=Math.max.apply(null, vals); var range=(max-min)||1; var bw=w/nums.length; var bars=''; nums.forEach(function(x,i){ var hh=Math.max(3,((x.val-min)/range)*(hgt-16)); var col=x.val>=0?'var(--ok)':'var(--er)'; bars+=''+ ''+x.val+''; }); return ''+bars+''; } }; /* ============================================================ Shared reports view (admin + customer) Renders: view toggle, date-range picker, analytics panel, and a fixed-column rollup/daily table. Config passed in via REPVIEW.mount(cfg). cfg = { scope: 'admin' | 'customer', getDailies: function(){...}, // returns daily reports in scope containers: {toggle, range, analytics, strip, tbody}, onOpenDaily: function(id){...}, onOpenRollup: function(rollupObj){...} } ============================================================ */ var REPVIEW = { state: {}, // keyed by scope mount: function(cfg){ this.state[cfg.scope] = this.state[cfg.scope] || {view:'daily', preset:'', from:'', to:'', client:''}; this.cfg = this.cfg || {}; this.cfg[cfg.scope] = cfg; this.render(cfg.scope); }, esc: function(s){var d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}, setClient: function(scope, val){ this.state[scope].client=val; this.render(scope); }, // range presets → concrete from/to. '' = all time, 'custom' = manual calendars presetRange: function(preset){ var to=new Date().toISOString().slice(0,10), d=new Date(), from=''; if(preset==='7d'){ d.setDate(d.getDate()-6); from=d.toISOString().slice(0,10); } else if(preset==='4w'){ d.setDate(d.getDate()-27); from=d.toISOString().slice(0,10); } else if(preset==='6m'){ d.setMonth(d.getMonth()-5); d.setDate(1); from=d.toISOString().slice(0,10); } else if(preset==='12m'){ d.setMonth(d.getMonth()-11); d.setDate(1); from=d.toISOString().slice(0,10); } else if(preset==='ytd'){ from=new Date().getFullYear()+'-01-01'; } else { return {from:'', to:''}; } // all time return {from:from, to:to}; }, setPreset: function(scope, preset){ var st=this.state[scope]; st.preset=preset; if(preset==='custom'){ /* keep existing from/to, show calendars */ } else { var r=this.presetRange(preset); st.from=r.from; st.to=r.to; } this.render(scope); }, setView: function(scope, v){ var st=this.state[scope]; st.view=v; this.render(scope); }, setFrom: function(scope, val){ this.state[scope].from=val; this.render(scope); }, setTo: function(scope, val){ this.state[scope].to=val; this.render(scope); }, clearRange: function(scope){ var st=this.state[scope]; st.preset=''; st.from=''; st.to=''; st.client=''; this.render(scope); }, render: function(scope){ var cfg=this.cfg[scope], st=this.state[scope], self=this; var dailies=cfg.getDailies().filter(function(r){return (r.period||r.type||'daily')==='daily';}); // toggle var toggle='
'+ ['daily','weekly','monthly','yearly'].map(function(v){ return ''; }).join('')+'
'; document.getElementById(cfg.containers.toggle).innerHTML=toggle; // date-range picker (all views) + optional client filter (admin) var clientFilterHtml=''; if(cfg.clientFilter){ var clients=TA.getUsers().filter(function(u){return u.role==='customer';}); var copts=''+clients.map(function(c){ return ''; }).join(''); clientFilterHtml='
'+ '
'; } // single range dropdown; calendars only when Custom is picked var presets=[ {v:'',l:'All time'}, {v:'7d',l:'Last 7 days'}, {v:'4w',l:'Last 4 weeks'}, {v:'6m',l:'Last 6 months'}, {v:'12m',l:'Last 12 months'}, {v:'ytd',l:'Year to date'}, {v:'custom',l:'Custom range…'} ]; var popts=presets.map(function(x){return '';}).join(''); var customHtml=''; if(st.preset==='custom'){ customHtml='
'+ '
'; } var rangeHtml='
'+ clientFilterHtml+ '
'+ '
'+ customHtml+ '
'; document.getElementById(cfg.containers.range).innerHTML=rangeHtml; // apply client filter, then range if(st.client){ dailies=dailies.filter(function(r){return r.clientId===st.client;}); } var scoped=ROLLUP.inRange(dailies, st.from, st.to); // ANALYTICS panel (only for rollup views) var anEl=document.getElementById(cfg.containers.analytics); if(st.view==='daily'){ anEl.innerHTML=''; } else { var a=ROLLUP.analytics(scoped); var topkw=a.keywords.slice(0,8).map(function(k){ var max=a.keywords[0]?a.keywords[0].n:1; var pct=Math.round((k.n/max)*100); return '
'+self.esc(k.kw)+''+ ''+ ''+k.n+'d
'; }).join('') || '
No keywords tracked in this range.
'; var trafficList=a.trafficPoints.slice(-5).reverse().map(function(t){ return '
'+TA.date(t.date)+''+self.esc(t.note)+'
'; }).join('') || '
No traffic updates in this range.
'; anEl.innerHTML= '
'+st.view.charAt(0).toUpperCase()+st.view.slice(1)+' analytics
'+ ''+a.days+' daily report(s)
'+ '
'+ '
Keywords worked
'+a.uniqueKeywords+'
'+ '
Backlinks
'+a.totalBacklinks+'
'+ '
Creatives
'+a.totalImages+'
'+ '
Hours
'+a.totalHours+'h
'+ '
'+ '
'+ '
Top keywords (by active days)
'+topkw+'
'+ '
Traffic trend
'+ROLLUP.trafficSparkline(a.trafficNumbers, 260, 64)+ '
Recent updates
'+trafficList+'
'+ '
'; } // TABLE var tbody=document.getElementById(cfg.containers.tbody); var stripEl=cfg.containers.strip?document.getElementById(cfg.containers.strip):null; if(st.view==='daily'){ var reps=scoped.slice().sort(function(a,b){return (b.date||b.submitted)<(a.date||a.submitted)?-1:1;}); var h=''; reps.forEach(function(r){ var emp=TA.getUserById(r.employeeId), cl=TA.getUserById(r.clientId); var pj=TA.getProjects().filter(function(p){return p.id===r.projectId;})[0]; var bl=(r.backlinks||[]).length; var who = scope==='admin' ? (emp?emp.name:'—') : (emp?emp.name:'Team'); h+=''+ '
'+self.esc(r.title)+'
'+self.esc(pj?pj.name:'')+'
'+ ''+self.esc(who)+''+ (scope==='admin'?''+self.esc(cl?(cl.company||cl.name):'—')+'':'')+ 'daily'+ ''+bl+' links · '+(r.images||0)+' img · '+(r.timeSpent||0)+'h'+ ''+TA.date(r.date||r.submitted)+''+ ''+(r.reviewed?'Reviewed':'Pending')+''+ ''; }); tbody.innerHTML=h||self.emptyRow(scope,'No daily reports in this range.'); if(stripEl){ var tb=0,ti=0,th=0; reps.forEach(function(r){tb+=(r.backlinks||[]).length;ti+=(r.images||0);th+=(r.timeSpent||0);}); stripEl.innerHTML=self.strip(reps.length,'Reports',tb,Math.round(th*10)/10); } } else { var rolls=ROLLUP.build(scoped, st.view); var h=''; rolls.forEach(function(r){ var cl=TA.getUserById(r.clientId); var pj=TA.getProjects().filter(function(p){return p.id===r.projectId;})[0]; var bl=(r.backlinks||[]).length; var who; if(scope==='admin'){ who=(r.employeeIds||[]).map(function(id){var u=TA.getUserById(id);return u?u.name.split(' ')[0]:'';}).filter(Boolean).join(', ')||'—'; } else { who='—'; } h+=''+ '
'+self.esc(r.title)+'
'+self.esc(pj?pj.name:'')+' · '+r.dayCount+' day(s)
'+ ''+self.esc(who)+''+ (scope==='admin'?''+self.esc(cl?(cl.company||cl.name):'—')+'':'')+ ''+r.period+''+ ''+bl+' links · '+(r.images||0)+' img · '+(r.timeSpent||0)+'h'+ ''+TA.date(r.date||r.submitted)+''+ ''+(r.reviewed?'All reviewed':'Pending')+''+ ''; }); tbody.innerHTML=h||self.emptyRow(scope,'No '+st.view+' summary in this range.'); if(stripEl){ var tb=0,th=0; rolls.forEach(function(r){tb+=(r.backlinks||[]).length;th+=(r.timeSpent||0);}); stripEl.innerHTML=self.strip(rolls.length, st.view.charAt(0).toUpperCase()+st.view.slice(1)+' summaries', tb, Math.round(th*10)/10); } } // fire optional render hook (e.g. keyword panel) with the scoped daily set + current view if(cfg.onRender){ cfg.onRender(scoped, st.view, st); } }, emptyRow: function(scope,msg){ var cols = scope==='admin'?8:7; return ''+msg+''; }, strip: function(count,label,backlinks,hours){ return '
'+ '
'+label+'
'+count+'
'+ '
Total backlinks
'+backlinks+'
'+ '
Total hours
'+hours+'h
'; }, openDaily: function(scope,id){ this.cfg[scope].onOpenDaily(id); }, openRollup: function(scope,id){ var st=this.state[scope], cfg=this.cfg[scope]; var dailies=cfg.getDailies().filter(function(r){return (r.period||r.type||'daily')==='daily';}); if(st.client){ dailies=dailies.filter(function(r){return r.clientId===st.client;}); } var scoped=ROLLUP.inRange(dailies, st.from, st.to); var r=ROLLUP.build(scoped, st.view).filter(function(x){return x.id===id;})[0]; if(r) cfg.onOpenRollup(r); } }; /* ============================================================ CHARTS — tiny self-contained SVG chart helpers. No external libs. Uses CSS vars for theming. ============================================================ */ var CHARTS = { palette: ['#316699','#f5b400','#15803d','#8b5cf6','#dc2626','#0ea5e9','#e08a00','#64748b'], esc:function(s){var d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}, // vertical bar chart: data=[{label, value}], opts{height, unit, color} bars:function(data, opts){ opts=opts||{}; var W=opts.width||520, H=opts.height||180, pad=28, bottom=26; if(!data.length) return '
No data for this period.
'; var max=Math.max.apply(null, data.map(function(d){return d.value;}).concat([1])); var bw=(W-pad)/data.length; var color=opts.color||'var(--brand)'; var bars='',labels='',grid=''; // gridlines (3) for(var g=1;g<=3;g++){ var gy=(H-bottom)-((H-bottom-10)*(g/3)); grid+=''+ ''+Math.round(max*g/3)+''; } data.forEach(function(d,i){ var hh=Math.max(2, ((d.value/max)*(H-bottom-12))); var x=pad+i*bw+bw*0.18, y=(H-bottom)-hh, w=bw*0.64; bars+=''+CHARTS.esc(d.label)+': '+d.value+(opts.unit||'')+''; if(d.value>0) bars+=''+d.value+''; labels+=''+CHARTS.esc(d.label)+''; }); return ''+grid+bars+labels+''; }, // grouped/stacked-ish multi-series as separate colored bars per period multiBars:function(periods, series, opts){ opts=opts||{}; var W=opts.width||540, H=opts.height||190, pad=30, bottom=28; if(!periods.length) return '
No data for this period.
'; var allVals=[]; periods.forEach(function(p){ series.forEach(function(s){ allVals.push(p.values[s.key]||0); }); }); var max=Math.max.apply(null, allVals.concat([1])); var groupW=(W-pad)/periods.length; var barW=(groupW*0.7)/series.length; var out='',labels='',grid=''; for(var g=1;g<=3;g++){ var gy=(H-bottom)-((H-bottom-10)*(g/3)); grid+=''+ ''+Math.round(max*g/3)+''; } periods.forEach(function(p,i){ var gx=pad+i*groupW+groupW*0.15; series.forEach(function(s,j){ var v=p.values[s.key]||0; var hh=Math.max(1,((v/max)*(H-bottom-12))); var x=gx+j*barW, y=(H-bottom)-hh; out+=''+CHARTS.esc(p.label)+' · '+s.label+': '+v+''; }); labels+=''+CHARTS.esc(p.label)+''; }); var legend=series.map(function(s){return ''+CHARTS.esc(s.label)+'';}).join(''); return ''+grid+out+labels+''+ '
'+legend+'
'; }, // donut chart: data=[{label,value}], opts{size,thickness,centerLabel,centerValue} donut:function(data, opts){ opts=opts||{}; var size=opts.size||150, th=opts.thickness||26, r=(size-th)/2, cx=size/2, cy=size/2; var total=data.reduce(function(s,d){return s+d.value;},0); if(total<=0) return '
No data.
'; var circ=2*Math.PI*r, off=0, segs=''; data.forEach(function(d,i){ if(d.value<=0) return; var frac=d.value/total, len=frac*circ; var col=d.color||CHARTS.palette[i%CHARTS.palette.length]; segs+=''+CHARTS.esc(d.label)+': '+d.value+''; off+=len; }); var center=''; if(opts.centerValue!=null){ center=''+CHARTS.esc(opts.centerValue)+''+ (opts.centerLabel?''+CHARTS.esc(opts.centerLabel)+'':''); } var legend=data.filter(function(d){return d.value>0;}).map(function(d,i){ var col=d.color||CHARTS.palette[i%CHARTS.palette.length]; var pct=Math.round((d.value/total)*100); return '
'+CHARTS.esc(d.label)+''+d.value+' · '+pct+'%
'; }).join(''); return '
'+segs+center+''+ '
'+legend+'
'; }, // line/area sparkline: points=[{label,value}] line:function(points, opts){ opts=opts||{}; var W=opts.width||520, H=opts.height||150, pad=28, bottom=24; if(points.length<2) return '
Not enough data for a trend.
'; var vals=points.map(function(p){return p.value;}); var max=Math.max.apply(null,vals), min=Math.min.apply(null,vals.concat([0])); var range=(max-min)||1; var stepX=(W-pad)/(points.length-1); var pts=points.map(function(p,i){ var x=pad+i*stepX, y=(H-bottom)-((p.value-min)/range)*(H-bottom-12); return [x,y]; }); var poly=pts.map(function(p){return p[0]+','+p[1];}).join(' '); var area='M'+pad+','+(H-bottom)+' L'+pts.map(function(p){return p[0]+','+p[1];}).join(' L')+' L'+(pad+(points.length-1)*stepX)+','+(H-bottom)+' Z'; var dots='',labels=''; pts.forEach(function(p,i){ dots+=''+CHARTS.esc(points[i].label)+': '+points[i].value+''; labels+=''+CHARTS.esc(points[i].label)+''; }); return ''+ ''+ ''+ dots+labels+''; }, // horizontal progress ring ring:function(pct, opts){ opts=opts||{}; var size=opts.size||110, th=opts.thickness||12, r=(size-th)/2, cx=size/2, cy=size/2, circ=2*Math.PI*r; var len=(Math.max(0,Math.min(100,pct))/100)*circ; var col=opts.color||(pct>=75?'var(--ok)':pct>=40?'var(--brand)':'var(--wn)'); return ''+ ''+ ''+ ''+Math.round(pct)+'%'+ (opts.label?''+CHARTS.esc(opts.label)+'':'')+ ''; } }; /* ============================================================ KEYWORDS — rank-tracking engine. Employees log daily {kw, rank}. This builds per-keyword timelines and period (daily/weekly/monthly/yearly) rollups showing rank progress. Lower rank number = better position. Requires TA, ROLLUP (for week/month keys), CHARTS (for viz). ============================================================ */ var KW = { // collect all {date, kw, rank} points from daily reports (optionally one client) points: function(dailies){ var pts=[]; dailies.forEach(function(r){ var d=r.date||r.submitted; if(!d) return; (r.keywordRanks||[]).forEach(function(k){ if(k && k.kw && (k.rank||k.rank===0)) pts.push({date:d, kw:k.kw.trim(), rank:k.rank, clientId:r.clientId}); }); }); return pts; }, // unique keyword list with their latest rank + first rank + delta summary: function(dailies){ var pts=this.points(dailies); var byKw={}; pts.forEach(function(p){ (byKw[p.kw]=byKw[p.kw]||[]).push(p); }); var out=Object.keys(byKw).map(function(kw){ var arr=byKw[kw].slice().sort(function(a,b){return a.date0) return '▲ '+improvement+''; if(improvement<0) return '▼ '+Math.abs(improvement)+''; return ''; }, /* ---- rendered blocks ---- */ // full keyword-progress panel (used on customer reports + admin) progressPanel: function(dailies, period){ period=period||'weekly'; var sum=this.summary(dailies); if(!sum.length) return '
No keyword rankings tracked yet. Your team adds keywords and daily positions in their reports.
'; // KPIs var improved=sum.filter(function(k){return k.improvement>0;}).length; var onPage1=sum.filter(function(k){return k.latest<=10;}).length; var avgLatest=Math.round(sum.reduce(function(s,k){return s+k.latest;},0)/sum.length*10)/10; var h='
'+ '
'+sum.length+'
Keywords tracked
'+ '
'+onPage1+'
On page 1 (top 10)
'+ '
'+improved+'
Improved
'+ '
'+avgLatest+'
Avg. position
'+ '
'; // overall visibility trend (avg rank over periods) — inverted so up = better var series=this.avgTimeline(dailies, period); if(series.length>=2){ var maxRank=Math.max.apply(null, series.map(function(s){return s.rank;})); // invert: show as "visibility" so higher on chart = better rank var pts=series.map(function(s){return {label:s.label, value:Math.round((maxRank-s.rank+1)*10)/10, rank:s.rank};}); h+='
Overall ranking trend (higher = better positions)
'+ CHARTS.line(pts,{height:150}); } // per-keyword table with mini sparkline h+='
Keyword positions
'; h+='
'; sum.forEach(function(k){ var tl=KW.timeline(dailies, k.kw, period); var spark=KW.miniSpark(tl); var curCol=k.latest<=10?'var(--ok)':k.latest<=20?'var(--brand)':'var(--ink2)'; h+=''+ ''+ ''+ ''+ ''+ ''; }); h+='
KeywordStartBestCurrentChangeTrend
'+CHARTS.esc(k.kw)+'#'+k.first+'#'+k.best+'#'+k.latest+''+KW.deltaBadge(k.improvement)+''+spark+'
'; return h; }, // tiny rank sparkline (inverted: lower rank plotted higher) miniSpark: function(timeline){ if(!timeline || timeline.length<2) return ''; var W=90,H=26; var ranks=timeline.map(function(t){return t.rank;}); var min=Math.min.apply(null,ranks), max=Math.max.apply(null,ranks); var range=(max-min)||1; var step=W/(timeline.length-1); var pts=timeline.map(function(t,i){ // invert: better rank (smaller) => higher on chart (smaller y) var y=4+((t.rank-min)/range)*(H-8); return [i*step, y]; }); var poly=pts.map(function(p){return p[0].toFixed(1)+','+p[1].toFixed(1);}).join(' '); var last=pts[pts.length-1]; var improving=timeline[timeline.length-1].rank<=timeline[0].rank; var col=improving?'var(--ok)':'var(--er)'; return ''; }, // compact dashboard card content (top movers) dashCard: function(dailies){ var sum=this.summary(dailies); if(!sum.length) return '
No keyword rankings yet.
'; var onPage1=sum.filter(function(k){return k.latest<=10;}).length; var improved=sum.filter(function(k){return k.improvement>0;}).length; var h='
'+ '
'+sum.length+'
Tracked
'+ '
'+onPage1+'
Page 1
'+ '
'+improved+'
Improved
'+ '
'; // top 5 by current rank h+='
'; sum.slice(0,5).forEach(function(k){ var curCol=k.latest<=10?'var(--ok)':k.latest<=20?'var(--brand)':'var(--ink2)'; h+=''+ ''+ ''; }); h+='
'+CHARTS.esc(k.kw)+'#'+k.latest+''+KW.deltaBadge(k.improvement)+'
'; return h; } }; /* ============================================================ LEADS — lead management engine. Employees create leads for a customer; customers & employees update status. Shared across admin / employee / customer. Requires TA, CHARTS, and a modal system (openM/closeM or #mo+cm). Config via LEADS.mount(cfg): scope, getLeads(), canAdd, canEditStatus, containers{filters,kpis,charts,tbody}, clientId ============================================================ */ var LEADS = { TYPES: [ {v:'cold', l:'Cold', color:'#0ea5e9'}, {v:'warm', l:'Warm', color:'#f5b400'}, {v:'hot', l:'Hot', color:'#e0512b'}, {v:'won', l:'Won', color:'#15803d'}, {v:'dead', l:'Dead', color:'#94a3b8'} ], typeMeta: function(v){ for(var i=0;i0?Math.round((counts.won/closed)*100):0; return {counts:counts, total:total, active:active, won:counts.won, winRate:winRate}; }, render: function(scope){ var cfg=this.cfg[scope], st=this.state[scope], self=this; var leads=this.scoped(scope); // filter bar if(cfg.containers.filters){ var clients=TA.getUsers().filter(function(u){return u.role==='customer';}); var emps=TA.getUsers().filter(function(u){return u.role==='employee';}); function sel(k,label,list,cur,allLabel){ var o=''+list.map(function(x){return '';}).join(''); return '
'; } var typeList=this.TYPES.map(function(t){return {v:t.v,l:t.l};}); var anyActive=st.type||st.client||st.owner||st.q; var bar='
'+ '
'+ sel('type','Status',typeList,st.type,'All statuses')+ (cfg.showClient?sel('client','Customer',clients.map(function(c){return {v:c.id,l:(c.company||c.name)};}),st.client,'All customers'):'')+ (cfg.showOwner?sel('owner','Added by',emps.map(function(e){return {v:e.id,l:e.name};}),st.owner,'All employees'):'')+ (anyActive?'
':'')+ (cfg.canAdd?'
':'')+ '
'; document.getElementById(cfg.containers.filters).innerHTML=bar; } // KPIs var a=this.analytics(leads); if(cfg.containers.kpis){ document.getElementById(cfg.containers.kpis).innerHTML= '
'+ '
'+a.total+'
Total leads
'+ '
'+a.counts.hot+'
Hot
'+ '
'+a.active+'
Active pipeline
'+ '
'+a.won+'
Won
'+ '
'+a.winRate+'%
Win rate
'+ '
'; } // charts: funnel donut + status bars if(cfg.containers.charts){ var donutData=this.TYPES.map(function(t){return {label:t.l, value:a.counts[t.v], color:t.color};}); var barData=this.TYPES.map(function(t){return {label:t.l, value:a.counts[t.v]};}); document.getElementById(cfg.containers.charts).innerHTML= '
'+ '

Lead status breakdown

Distribution across the pipeline
'+ CHARTS.donut(donutData,{size:150,thickness:26,centerValue:a.total,centerLabel:'leads'})+'
'+ '

Pipeline funnel

Leads by stage
'+ (a.total?LEADS.funnelBars(a.counts):'
No leads yet.
')+'
'+ '
'; } // table if(cfg.containers.tbody){ var h=''; leads.slice().sort(function(x,y){return (y.date||'')<(x.date||'')?-1:1;}).forEach(function(l){ var cl=TA.getUserById(l.clientId), owner=TA.getUserById(l.ownerId); var m=self.typeMeta(l.type); var li=l.linkedin?'in ↗':'—'; h+=''+ '
'+self.esc(l.name)+'
'+self.esc(l.jobTitle||'')+'
'+ ''+(l.company?self.esc(l.company):'—')+''+ (cfg.showClient?''+(cl?self.esc(cl.company||cl.name):'—')+'':'')+ ''+m.l+''+ ''+(l.source?self.esc(l.source):'—')+''+ (cfg.showOwner?''+(owner?self.esc(owner.name.split(' ')[0]):'—')+'':'')+ ''+li+''+ ''+TA.date(l.date)+''+ ''; }); var cols=8+(cfg.showClient?1:0)+(cfg.showOwner?1:0); document.getElementById(cfg.containers.tbody).innerHTML=h||'No leads'+(st.type||st.client||st.owner||st.q?' match these filters.':' yet.')+''; } if(cfg.onRender) cfg.onRender(leads, a); }, funnelBars: function(counts){ var order=[{k:'cold',l:'Cold'},{k:'warm',l:'Warm'},{k:'hot',l:'Hot'},{k:'won',l:'Won'}]; var max=Math.max.apply(null, order.map(function(o){return counts[o.k];}).concat([1])); var h='
'; order.forEach(function(o){ var m=LEADS.typeMeta(o.k); var pct=Math.round((counts[o.k]/max)*100); h+='
'+o.l+''+counts[o.k]+'
'+ '
'; }); h+='
Dead / lost: '+counts.dead+'
'; return h; }, /* ---- detail + status change ---- */ view: function(scope, id){ var cfg=this.cfg[scope]; var l=cfg.getLeads().filter(function(x){return x.id===id;})[0]; if(!l) return; var cl=TA.getUserById(l.clientId), owner=TA.getUserById(l.ownerId); var m=this.typeMeta(l.type); function row(k,v){ return v?'
'+k+'
'+LEADS.esc(v)+'
':''; } var statusSel=''; if(cfg.canEditStatus){ statusSel='
'+ this.TYPES.map(function(t){ var on=t.v===l.type; return ''; }).join('')+'
'; } var linkedinRow = l.linkedin?'':''; var body='
'+this.esc(l.name)+' '+m.l+'
'+ '
'+ row('Company', l.company)+ row('Job title', l.jobTitle)+ (cfg.showClient?row('For customer', cl?(cl.company||cl.name):''):'')+ row('Email', l.email)+ row('Phone', l.phone)+ linkedinRow+ row('Source', l.source)+ row('Added by', owner?owner.name:'')+ row('Date', TA.date(l.date))+ row('Notes', l.notes)+ '
'+ statusSel+ '
'+ (cfg.canAdd?'':'')+'
'; this.showModal(scope, body, 520); }, setStatus: function(scope, id, type){ var all=TA.getLeads(); var l=all.filter(function(x){return x.id===id;})[0]; if(!l) return; l.type=type; TA.saveLeads(all); if(typeof toast==='function') toast('Lead marked '+this.typeMeta(type).l); this.closeModal(scope); this.render(scope); }, /* ---- add / edit form ---- */ openForm: function(scope, id){ var cfg=this.cfg[scope]; var l=id?cfg.getLeads().filter(function(x){return x.id===id;})[0]:null; var clients=TA.getUsers().filter(function(u){return u.role==='customer';}); var clientPick=''; if(cfg.showClient){ clientPick='
'; } var typeOpts=this.TYPES.map(function(t){return '';}).join(''); function val(k){return l&&l[k]?String(l[k]).replace(/"/g,'"'):'';} var body='
'+(id?'Edit lead':'Add new lead')+'
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ clientPick+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'; this.showModal(scope, body, 600); }, save: function(scope, id){ var cfg=this.cfg[scope]; var name=document.getElementById('ld-name').value.trim(); var company=document.getElementById('ld-company').value.trim(); if(!name || !company){ if(typeof toast==='function') toast('Name and company are required', false); return; } var all=TA.getLeads(); var rec=id?all.filter(function(x){return x.id===id;})[0]:{id:TA.uid(), ownerId:(TA.getSession()||{}).id}; rec.name=name; rec.company=company; rec.jobTitle=document.getElementById('ld-title').value.trim(); rec.type=document.getElementById('ld-type').value; rec.email=document.getElementById('ld-email').value.trim(); rec.phone=document.getElementById('ld-phone').value.trim(); rec.linkedin=document.getElementById('ld-linkedin').value.trim(); rec.source=document.getElementById('ld-source').value.trim(); rec.date=document.getElementById('ld-date').value||new Date().toISOString().slice(0,10); rec.notes=document.getElementById('ld-notes').value.trim(); var csel=document.getElementById('ld-client'); if(csel) rec.clientId=csel.value; else if(cfg.clientId) rec.clientId=cfg.clientId; if(!id) all.push(rec); TA.saveLeads(all); if(typeof toast==='function') toast(id?'Lead updated':'Lead added','s'); this.closeModal(scope); this.render(scope); }, /* modal helpers — detect the portal's modal system */ showModal: function(scope, html, width){ if(typeof openM==='function'){ openM(html); var m=document.querySelector('#mo .modal'); if(m&&width)m.style.maxWidth=width+'px'; return; } var mo=document.getElementById('mo'); if(mo){ mo.innerHTML=''; mo.classList.add('open'); } }, closeModal: function(scope){ if(typeof closeM==='function'){ closeM(); return; } if(typeof cm==='function'){ cm(); return; } var mo=document.getElementById('mo'); if(mo){ mo.classList.remove('open'); mo.innerHTML=''; } }, /* compact dashboard card */ dashCard: function(leads){ var a=this.analytics(leads); if(!a.total) return '
No leads yet.
'; var donutData=this.TYPES.map(function(t){return {label:t.l, value:a.counts[t.v], color:t.color};}); return '
'+ '
'+a.total+'
Total
'+ '
'+a.counts.hot+'
Hot
'+ '
'+a.won+'
Won
'+ '
'+a.winRate+'%
Win rate
'+ '
'+CHARTS.donut(donutData,{size:140,thickness:24,centerValue:a.total,centerLabel:'leads'}); } }; /* ============================================================ CAMPAIGNS — marketing campaign tracking (email, LinkedIn InMail, Google Ads). Employees create campaigns and log daily updates against them; customers & admin can view the timeline. Employees (and admin) can mark a campaign closed once done. Config via CAMPAIGNS.mount(cfg): scope, getCampaigns(), canAdd, canManage, showClient, clientId, containers{filters,kpis,tbody} ============================================================ */ var CAMPAIGNS = { TYPES: [ {v:'email', l:'Email campaign', color:'#0ea5e9'}, {v:'linkedin_inmail', l:'LinkedIn InMail', color:'#0a66c2'}, {v:'google_ads', l:'Google Ads', color:'#f5b400'} ], typeMeta: function(v){ for(var i=0;i'+list.map(function(x){return '';}).join(''); return '
'; } var statusOpts=[{v:'active',l:'Active'},{v:'closed',l:'Closed'}]; var typeList=this.TYPES.map(function(t){return {v:t.v,l:t.l};}); var anyActive=st.status||st.type||st.client||st.q; var bar='
'+ '
'+ sel('status','Status',statusOpts,st.status,'All statuses')+ sel('type','Type',typeList,st.type,'All types')+ (cfg.showClient?sel('client','Customer',clients.map(function(c){return {v:c.id,l:(c.company||c.name)};}),st.client,'All customers'):'')+ (anyActive?'
':'')+ (cfg.canAdd?'
':'')+ '
'; document.getElementById(cfg.containers.filters).innerHTML=bar; } if(cfg.containers.kpis){ var activeN=camps.filter(function(c){return c.status==='active';}).length; var closedN=camps.filter(function(c){return c.status==='closed';}).length; document.getElementById(cfg.containers.kpis).innerHTML= '
'+ '
'+camps.length+'
Total campaigns
'+ '
'+activeN+'
Active
'+ '
'+closedN+'
Closed
'+ '
'; } if(cfg.containers.tbody){ var h=''; camps.slice().sort(function(a,b){return (b.startDate||'')<(a.startDate||'')?-1:1;}).forEach(function(c){ var cl=TA.getUserById(c.clientId); var m=self.typeMeta(c.type); var lastUpdate=(c.updates||[]).length?c.updates[c.updates.length-1]:null; h+=''+ '
'+self.esc(c.name)+'
'+self.esc(c.details||'')+'
'+ ''+m.l+''+ (cfg.showClient?''+(cl?self.esc(cl.company||cl.name):'—')+'':'')+ ''+(c.status==='active'?'Active':'Closed')+''+ ''+TA.date(c.startDate)+''+ ''+(lastUpdate?self.esc(lastUpdate.note):'—')+''+ ''; }); var cols=6+(cfg.showClient?1:0); document.getElementById(cfg.containers.tbody).innerHTML=h||'No campaigns'+(st.status||st.type||st.client||st.q?' match these filters.':' yet.')+''; } }, view: function(scope, id){ var cfg=this.cfg[scope]; var c=cfg.getCampaigns().filter(function(x){return x.id===id;})[0]; if(!c) return; var cl=TA.getUserById(c.clientId), owner=TA.getUserById(c.ownerId); var m=this.typeMeta(c.type); function row(k,v){ return v?'
'+k+'
'+CAMPAIGNS.esc(v)+'
':''; } var updatesHtml=(c.updates||[]).slice().reverse().map(function(u){ var emp=TA.getUserById(u.employeeId); return '
'+TA.date(u.date)+(emp?' · '+CAMPAIGNS.esc(emp.name):'')+'
'+ '
'+CAMPAIGNS.esc(u.note)+'
'; }).join('') || '
No updates logged yet.
'; var canManageOpen = cfg.canManage && c.status==='active'; var body='
'+this.esc(c.name)+' '+m.l+' '+ ''+(c.status==='active'?'Active':'Closed')+'
'+ '
'+ '
'+ row('Details', c.details)+ (cfg.showClient?row('Customer', cl?(cl.company||cl.name):''):'')+ row('Started', TA.date(c.startDate))+ (c.status==='closed'?row('Closed', TA.date(c.closedDate)):'')+ row('Owner', owner?owner.name:'')+ '
'+ '
Update timeline
'+updatesHtml+'
'+ (canManageOpen? '
'+ '
'+ ''+ ''+ '
' : '
'); this.showModal(scope, body, 540); }, addUpdate: function(scope, id){ var note=document.getElementById('camp-update-note').value.trim(); if(!note){ if(typeof toast==='function') toast('Add a note first', false); return; } var all=TA.getCampaigns(); var c=all.filter(function(x){return x.id===id;})[0]; if(!c) return; c.updates=c.updates||[]; c.updates.push({date:new Date().toISOString().slice(0,10), note:note, employeeId:(TA.getSession()||{}).id}); TA.saveCampaigns(all); if(typeof toast==='function') toast('Update added','s'); this.closeModal(scope); this.render(scope); }, markClosed: function(scope, id){ if(!confirm('Mark this campaign as closed? It will move out of the active list, but its history stays visible.')) return; var all=TA.getCampaigns(); var c=all.filter(function(x){return x.id===id;})[0]; if(!c) return; c.status='closed'; c.closedDate=new Date().toISOString().slice(0,10); TA.saveCampaigns(all); if(typeof toast==='function') toast('Campaign marked closed','s'); this.closeModal(scope); this.render(scope); }, openForm: function(scope){ var cfg=this.cfg[scope]; var clients=TA.getUsers().filter(function(u){return u.role==='customer';}); var typeOpts=this.TYPES.map(function(t){return '';}).join(''); var clientPick=cfg.showClient?('
'):''; var body='
New campaign
'+ '
'+ '
'+ clientPick+ '
'+ '
'+ '
'+ '
'; this.showModal(scope, body, 520); }, saveNew: function(scope){ var cfg=this.cfg[scope]; var name=document.getElementById('camp-name').value.trim(); if(!name){ if(typeof toast==='function') toast('Campaign name is required', false); return; } var csel=document.getElementById('camp-client'); var clientId=csel?csel.value:cfg.clientId; var note=document.getElementById('camp-first-note').value.trim(); var rec={id:TA.uid(), clientId:clientId, type:document.getElementById('camp-type').value, name:name, details:document.getElementById('camp-details').value.trim(), status:'active', startDate:new Date().toISOString().slice(0,10), closedDate:'', ownerId:(TA.getSession()||{}).id, updates: note?[{date:new Date().toISOString().slice(0,10), note:note, employeeId:(TA.getSession()||{}).id}]:[] }; var all=TA.getCampaigns(); all.push(rec); TA.saveCampaigns(all); if(typeof toast==='function') toast('Campaign created','s'); this.closeModal(scope); this.render(scope); }, /* returns active campaigns for a client, for use in the daily-report form */ activeForClient: function(clientId){ return TA.getCampaigns().filter(function(c){ return c.clientId===clientId && c.status==='active'; }); }, showModal: function(scope, html, width){ if(typeof openM==='function'){ openM(html); var m=document.querySelector('#mo .modal'); if(m&&width)m.style.maxWidth=width+'px'; return; } var mo=document.getElementById('mo'); if(mo){ mo.innerHTML=''; mo.classList.add('open'); } }, closeModal: function(scope){ if(typeof closeM==='function'){ closeM(); return; } if(typeof cm==='function'){ cm(); return; } var mo=document.getElementById('mo'); if(mo){ mo.classList.remove('open'); mo.innerHTML=''; } } }; var sess=null, CLI=null; function esc(s){var d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;} function today(){return new Date().toISOString().slice(0,10);} function toast(msg,ok){var t=document.getElementById('tst');t.textContent=msg;t.className='tst '+(ok===false?'ts-er':'ts-ok');t.style.display='block';setTimeout(function(){t.style.display='none';},2600);} function openM(html){document.getElementById('mo').innerHTML='';document.getElementById('mo').classList.add('open');} function closeM(){document.getElementById('mo').classList.remove('open');document.getElementById('mo').innerHTML='';} /* ---------- LOGIN ---------- */ function doLogin(){ var em=document.getElementById('lem').value.trim(); var pw=document.getElementById('lpw').value; var err=document.getElementById('lerr'); var btn=document.querySelector('.lbtn'); if(btn){btn.disabled=true;btn.textContent='Signing in…';} TA.loginAsync(em,pw,'customer',function(res){ if(btn){btn.disabled=false;btn.textContent='Sign in to Customer Portal';} if(res.error==='invalid'){err.textContent='Incorrect email or password. Please try again.';err.style.display='block';return;} if(res.error==='wrong-role'){err.textContent='This account is not a customer account. Use the correct portal.';err.style.display='block';return;} if(res.error==='inactive'){err.textContent='This account has been deactivated. Contact your account manager.';err.style.display='block';return;} err.style.display='none'; TA.setSession(res.user); boot(res.user); }); } function doLogout(){TA.clearSession();if(TA._fbAuth)TA._fbAuth.signOut().catch(function(){});location.reload();} /* ---------- NAV ---------- */ function go(id){ var pgs=document.querySelectorAll('.pg'); for(var i=0;i-1;}); } function renderCustomerTasks(){ var tasks=myClientTasks(); var el=document.getElementById('cust-tasks-board'); if(!tasks.length){ el.innerHTML='
No tasks yet.
'; return; } var cols=[{id:'todo',lbl:'TO DO',cls:'kt'},{id:'in-progress',lbl:'IN PROGRESS',cls:'ki'},{id:'review',lbl:'IN REVIEW',cls:'kr'},{id:'done',lbl:'DONE',cls:'kd'}]; var h=''; cols.forEach(function(c){ var ct=tasks.filter(function(t){return t.status===c.id;}); h+='
'+c.lbl+' '+ct.length+'
'; ct.forEach(function(t){ var pj=TA.getProjects().filter(function(p){return p.id===t.projectId;})[0]; var pc={urgent:'br',high:'br',medium:'bw',low:'bn'}[t.priority]||'bn'; var openComments=(t.comments||[]).length; h+='
'+esc(t.title)+'
'+ (pj?'
'+esc(pj.name)+'
':'')+ '
'+esc(t.priority)+''+ (t.due?''+TA.date(t.due)+'':'')+'
'+ (t.rating?'
'+starString(t.rating.stars)+'
':'')+ (openComments&&t.status!=='done'?'
↩ '+openComments+' comment(s)
':'')+ (t.status==='review'?'
'+ ''+ '
':'')+ '
'; }); h+='
'; }); el.innerHTML=h; } function starString(n){ n=n||0; var s=''; for(var i=1;i<=5;i++) s+=(i<=n?'★':'☆'); return s; } function viewClientTask(id){ var t=myClientTasks().filter(function(x){return x.id===id;})[0]; if(!t) return; var pj=TA.getProjects().filter(function(p){return p.id===t.projectId;})[0]; var assignee=TA.getUserById(t.assigneeId); var commentsHtml=(t.comments||[]).slice().reverse().map(function(c){ return '
'+TA.date(c.date)+' · '+(c.byRole==='customer'?'You':(esc((TA.getUserById(c.by)||{}).name||'Team')))+'
'+ '
'+esc(c.text)+'
'; }).join(''); var ratingHtml=t.rating?('
Your rating
'+starString(t.rating.stars)+'
'+ (t.rating.review?'
'+esc(t.rating.review)+'
':'')+'
'):''; var body='
'+esc(t.title)+'
'+ '
'+ (pj?'
Project
'+esc(pj.name)+'
':'')+ '
Assigned to
'+(assignee?esc(assignee.name):'—')+'
'+ '
Priority
'+esc(t.priority)+'
'+ (t.due?'
Due
'+TA.date(t.due)+'
':'')+ '
Status
'+esc(t.status.replace('-',' '))+'
'+ '
'+ ratingHtml+ (commentsHtml?'
Comments
'+commentsHtml+'
':'')+ '
'+ (t.status==='review'?'':'')+ '
'; openM(body); } function openApproveModal(id){ var body='
Approve & rate this work
'+ '
'+ '
'+ [1,2,3,4,5].map(function(n){return '';}).join('')+ '
'+ '
'+ '
'+ '
'; openM(body); window._starPick=0; } function setStarPick(n){ window._starPick=n; document.querySelectorAll('#star-picker span').forEach(function(s){ s.style.opacity = (parseInt(s.getAttribute('data-star'),10)<=n) ? '1' : '.35'; }); } function submitApproval(id){ var stars=window._starPick||0; if(!stars){ toast('Pick a star rating first', false); return; } var mine=myClientTasks().map(function(t){return t.id;}); if(mine.indexOf(id)===-1){ toast('You can only approve your own project tasks', false); return; } var all=TA.getTasks(); var ok=false; all.forEach(function(t){ if(t.id===id && t.status==='review'){ t.status='done'; t.rating={stars:stars, review:document.getElementById('rate-review').value.trim(), date:today(), byClientId:sess.id}; ok=true; } }); if(!ok){ toast('Only tasks in review can be approved', false); return; } TA.saveTasks(all); closeM(); toast('Task approved and rated — the team has been notified','s'); renderCustomerTasks(); if(typeof buildNotifs==='function') buildNotifs(); } function openRejectModal(id){ var body='
Request changes
'+ '
'+ '
'+ '
'; openM(body); } function submitRejection(id){ var note=document.getElementById('reject-note').value.trim(); if(!note){ toast('Add a comment so the team knows what to fix', false); return; } var mine=myClientTasks().map(function(t){return t.id;}); if(mine.indexOf(id)===-1){ toast('You can only manage your own project tasks', false); return; } var all=TA.getTasks(); var ok=false; all.forEach(function(t){ if(t.id===id && t.status==='review'){ t.status='in-progress'; t.comments=t.comments||[]; t.comments.push({date:today(), by:sess.id, byRole:'customer', text:note}); ok=true; } }); if(!ok){ toast('Only tasks in review can be sent back', false); return; } TA.saveTasks(all); closeM(); toast('Sent back to the team with your comment','s'); renderCustomerTasks(); if(typeof buildNotifs==='function') buildNotifs(); } function renderCampaigns(){ CAMPAIGNS.mount({ scope:'customer', getCampaigns:myCampaigns, clientId:sess.id, canAdd:false, // campaigns are set up by the team canManage:false, // customer views progress; doesn't add updates or close showClient:false, containers:{filters:'camp-filters', kpis:'camp-kpis', tbody:'camp-body'} }); } function renderLeads(){ LEADS.mount({ scope:'customer', getLeads:myLeads, clientId:sess.id, canAdd:false, // leads are generated by the team canEditStatus:true, // customer can move a lead along the pipeline showClient:false, showOwner:false, containers:{filters:'lead-filters', kpis:'lead-kpis', charts:'lead-charts', tbody:'lead-body'} }); } /* ---------- OVERVIEW ---------- */ var _dashPeriod='weekly'; function setDashPeriod(v){_dashPeriod=v;renderOv();} function renderOv(){ var pr=myProjects(), inv=myInvoices(), tk=myTickets(); var dailies=myReports().filter(function(r){return (r.period||r.type||'daily')==='daily';}); /* period toggle */ var toggle='
'+ ['daily','weekly','monthly','yearly'].map(function(v){ return ''; }).join('')+'
'; document.getElementById('dash-period').innerHTML=toggle; /* period buckets (last N) */ var buckets=dashBuckets(dailies, _dashPeriod); // [{label, from, to, reports:[...]}] oldest->newest var cur=buckets[buckets.length-1]||{reports:[]}; var prev=buckets[buckets.length-2]||{reports:[]}; function sumB(b,fn){return (b.reports||[]).reduce(function(s,r){return s+fn(r);},0);} var curBL=sumB(cur,function(r){return (r.backlinks||[]).length;}); var prevBL=sumB(prev,function(r){return (r.backlinks||[]).length;}); var curHrs=Math.round(sumB(cur,function(r){return r.timeSpent||0;})*10)/10; var curImg=sumB(cur,function(r){return r.images||0;}); var curSoc=sumB(cur,function(r){return (r.socialLinks||[]).length;}); /* KPI hero */ var due=inv.filter(function(i){return i.status!=='paid';}).reduce(function(a,b){return a+b.total;},0); var avg=pr.length?Math.round(pr.reduce(function(a,b){return a+b.progress;},0)/pr.length):0; function delta(cur,prev){ if(prev===0) return cur>0?{t:'New activity',c:'kpi-up'}:{t:'No change',c:'kpi-nu'}; var d=Math.round(((cur-prev)/prev)*100); return {t:(d>=0?'▲ '+d:'▼ '+Math.abs(d))+'% vs last '+_dashPeriod.replace('ly',''), c:d>0?'kpi-up':d<0?'kpi-dn':'kpi-nu'};} var dBL=delta(curBL,prevBL); var sparkBL=CHARTS.line(buckets.map(function(b){return {label:'', value:(b.reports||[]).reduce(function(s,r){return s+(r.backlinks||[]).length;},0)};}),{width:70,height:26}); var hero= kpi('Backlinks built', curBL, dBL.t, dBL.c, iconLink(), 'var(--brand-l)','var(--brand)')+ kpi('Hours invested', curHrs+'h', 'this '+_dashPeriod.replace('ly',''), 'kpi-nu', iconClock(),'var(--ok-bg)','var(--ok)')+ kpi('Creatives & posts', (curImg+curSoc), curImg+' images · '+curSoc+' posts', 'kpi-nu', iconImage(),'#f3e8ff','#8b5cf6')+ kpi('Amount due', TA.inr(due), inv.filter(function(i){return i.status!=='paid';}).length+' open invoice(s)', due>0?'kpi-dn':'kpi-up', iconRupee(),'var(--wn-bg)','var(--wn)'); document.getElementById('dash-hero').innerHTML=hero; /* work output — multi-bar per period */ var series=[{key:'backlinks',label:'Backlinks',color:'var(--brand)'},{key:'images',label:'Creatives',color:'#8b5cf6'},{key:'hours',label:'Hours',color:'var(--ok)'}]; var periods=buckets.map(function(b){ return {label:b.label, values:{ backlinks:(b.reports||[]).reduce(function(s,r){return s+(r.backlinks||[]).length;},0), images:(b.reports||[]).reduce(function(s,r){return s+(r.images||0);},0), hours:Math.round((b.reports||[]).reduce(function(s,r){return s+(r.timeSpent||0);},0)) }}; }); document.getElementById('dash-output').innerHTML=CHARTS.multiBars(periods, series, {height:190}); document.getElementById('dash-out-sub').textContent='Backlinks, creatives & hours · last '+buckets.length+' '+_dashPeriod.replace('ly','')+'s'; /* donut — work breakdown for current period */ var donutData=[ {label:'Backlinks', value:curBL, color:'var(--brand)'}, {label:'Creatives', value:curImg, color:'#8b5cf6'}, {label:'Social posts', value:curSoc, color:'var(--ok)'} ]; var totalWork=curBL+curImg+curSoc; document.getElementById('dash-donut').innerHTML=CHARTS.donut(donutData,{size:150,thickness:26,centerValue:totalWork,centerLabel:'items'}); /* traffic trend line from analytics over the whole scoped set */ var scopedAll=[]; buckets.forEach(function(b){scopedAll=scopedAll.concat(b.reports||[]);}); var an=ROLLUP.analytics(scopedAll); if(an.trafficNumbers.length>=2){ document.getElementById('dash-traffic').innerHTML=CHARTS.line(an.trafficNumbers.map(function(x){return {label:TA.date(x.date).replace(/ \d{4}$/,''), value:x.val};}),{height:150}); } else { document.getElementById('dash-traffic').innerHTML=ROLLUP.trafficSparkline(an.trafficNumbers,300,90)+ '
'+(an.trafficPoints.length?'Recent: '+esc(an.trafficPoints[an.trafficPoints.length-1].note):'No traffic data yet.')+'
'; } /* keywords bar */ var kwBars=an.keywords.slice(0,6).map(function(k){return {label:k.kw.length>12?k.kw.slice(0,11)+'…':k.kw, value:k.n};}); document.getElementById('dash-keywords').innerHTML=kwBars.length?CHARTS.bars(kwBars,{height:170,unit:'d',color:'var(--brand)'}):'
No keywords tracked yet.
'; /* keyword ranking progress (full panel, respects period) */ var kwPeriod=(_dashPeriod==='daily')?'daily':_dashPeriod; document.getElementById('dash-kwrank').innerHTML=KW.progressPanel(scopedAll.length?scopedAll:dailies, kwPeriod); /* lead pipeline card */ document.getElementById('dash-leads').innerHTML=LEADS.dashCard(myLeads()); /* recent work feed */ var recent=dailies.slice().sort(function(a,b){return (b.date||b.submitted)<(a.date||a.submitted)?-1:1;}).slice(0,5); var rh=''; recent.forEach(function(r){ var emp=TA.getUserById(r.employeeId); var pj=TA.getProjects().filter(function(p){return p.id===r.projectId;})[0]; rh+='
'+iconDoc()+'
'+ '
'+esc(r.title)+'
'+ '
'+esc(emp?emp.name:'Team')+' · '+esc(pj?pj.name:'')+' · '+(r.backlinks||[]).length+' links, '+(r.timeSpent||0)+'h
'+ '
'+TA.date(r.date||r.submitted)+'
'; }); document.getElementById('dash-recent').innerHTML=rh||'
No recent work yet.
'; /* side: invoice donut + project rings */ var paidN=inv.filter(function(i){return i.status==='paid';}).length; var sentN=inv.filter(function(i){return i.status==='sent';}).length; var ovdN=inv.filter(function(i){return i.status==='overdue';}).length; var invDonut=CHARTS.donut([ {label:'Paid', value:paidN, color:'var(--ok)'}, {label:'Sent', value:sentN, color:'var(--brand)'}, {label:'Overdue', value:ovdN, color:'var(--er)'} ],{size:130,thickness:22,centerValue:inv.length,centerLabel:'invoices'}); var projRings=''; pr.slice(0,3).forEach(function(p){ projRings+='
'+esc(p.name.length>16?p.name.slice(0,15)+'…':p.name)+'
'+CHARTS.ring(p.progress,{size:82,thickness:9})+'
'; }); document.getElementById('dash-side').innerHTML= '
'+invDonut+'
'+ (projRings?'
Project delivery
'+projRings+'
':''); } /* group dailies into recent period buckets (oldest -> newest) */ function dashBuckets(dailies, period){ var n = period==='daily'?7 : period==='weekly'?6 : period==='monthly'?6 : 3; // how many buckets var buckets=[]; var now=new Date(); for(var i=n-1;i>=0;i--){ var from,to,label; var d=new Date(now); if(period==='daily'){ d.setDate(now.getDate()-i); from=to=d.toISOString().slice(0,10); label=TA.date(from).replace(/ \d{4}$/,''); } else if(period==='weekly'){ d.setDate(now.getDate()-i*7); var day=(d.getDay()+6)%7; var mon=new Date(d); mon.setDate(d.getDate()-day); var sun=new Date(mon); sun.setDate(mon.getDate()+6); from=mon.toISOString().slice(0,10); to=sun.toISOString().slice(0,10); label='W'+isoWeek(mon); } else if(period==='monthly'){ d.setMonth(now.getMonth()-i); from=d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-01'; var last=new Date(d.getFullYear(),d.getMonth()+1,0); to=last.toISOString().slice(0,10); label=d.toLocaleString('en-US',{month:'short'}); } else { var yr=now.getFullYear()-i; from=yr+'-01-01'; to=yr+'-12-31'; label=String(yr); } var reps=dailies.filter(function(r){var dd=r.date||r.submitted; return dd>=from && dd<=to;}); buckets.push({label:label, from:from, to:to, reports:reps}); } return buckets; } function isoWeek(d){ var jan1=new Date(d.getFullYear(),0,1); return Math.ceil((((d-jan1)/86400000)+jan1.getDay()+1)/7); } /* KPI + icon helpers */ function kpi(label,value,delta,dcls,icon,bg,fg){ return '
'+icon+'
'+ '
'+esc(label)+'
'+value+'
'+ '
'+delta+'
'; } function svgI(p){return ''+p+'';} function iconLink(){return svgI('');} function iconClock(){return svgI('');} function iconImage(){return svgI('');} function iconRupee(){return svgI('');} function iconDoc(){return svgI('');} /* ---------- PROJECTS ---------- */ function renderProjects(){ var pr=myProjects(), h=''; if(!pr.length){document.getElementById('proj-list').innerHTML='
No projects assigned yet.
';return;} pr.forEach(function(p){ var col=p.progress>=90?'var(--ok)':p.progress>=50?'var(--brand)':'var(--wn)'; var av=''; p.team.forEach(function(id){var u=TA.getUserById(id);if(u)av+='
'+esc(u.avatar)+'
';}); var sb=p.status==='active'?'bb':p.status==='done'?'bk':'bw'; h+='
'+esc(p.name)+'
'+ '
'+esc(p.description||'')+'
'+ ''+esc(p.status)+'
'+ '
Progress'+p.progress+'%
'+ '
'+ '
'+ '
Team
'+(av||'Unassigned')+'
'+ '
Timeline
'+ '
'+TA.date(p.start)+' → '+TA.date(p.end)+'
'; }); document.getElementById('proj-list').innerHTML=h; } /* ---------- REPORTS (REPVIEW: toggle + range + analytics) ---------- */ function repDate(r){return r.date||r.submitted;} function campUpdatesHTML(updates){ if(!updates||!updates.length) return ''; return updates.map(function(u){ var m=CAMPAIGNS.typeMeta(u.type); return '
'+m.l+''+ ''+esc(u.campaignName)+''+(u.closed?' Closed':'')+ (u.note?'
'+esc(u.note)+'
':'')+'
'; }).join(''); } var CONTENT_TYPE_LABELS={blog:'Blog post',whitepaper:'Whitepaper',webinar:'Webinar',case_study:'Case study',other:'Content'}; function contentUpdatesHTML(updates){ if(!updates||!updates.length) return ''; return updates.map(function(u){ return '
'+esc(CONTENT_TYPE_LABELS[u.type]||'Content')+''+ ''+esc(u.title)+''+ (u.link?'':'')+'
'; }).join(''); } function attachGallery(atts){ if(!atts||!atts.length) return ''; return '
'+atts.map(function(a){ return ''; }).join('')+'
'; } function openLightbox(src){ var d=document.createElement('div'); d.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.82);z-index:9999;display:flex;align-items:center;justify-content:center;cursor:zoom-out'; d.innerHTML=''; d.onclick=function(){ d.remove(); }; document.body.appendChild(d); } function renderReports(){ REPVIEW.mount({ scope:'customer', getDailies:function(){ return myReports(); }, containers:{toggle:'rep-toggle', range:'rep-range', analytics:'rep-analytics', strip:'rep-strip', tbody:'rep-body'}, onOpenDaily:function(id){ viewReport(id); }, onOpenRollup:function(r){ openM(cRollupDetailHTML(r)+ '
'+ '
'); }, onRender:function(scoped, view, st){ var period=(view==='daily')?'daily':view; var badge=document.getElementById('kw-period-badge'); if(badge) badge.textContent=period; var kwEl=document.getElementById('rep-keywords'); if(kwEl) kwEl.innerHTML=KW.progressPanel(scoped, period); } }); } function cRollupDetailHTML(r){ var pj=TA.getProjects().filter(function(p){return p.id===r.projectId;})[0]; function sec(label,val){ return val?'
'+label+'
'+val+'
':''; } var h='
'+esc(r.title)+'
'; h+='
'+esc(pj?pj.name:'')+' · auto-generated '+esc(r.period)+' summary from '+r.dayCount+' daily report(s)
'; h+='
'+ '
Backlinks
'+(r.backlinks||[]).length+'
'+ '
Creatives
'+(r.images||0)+'
'+ '
Hours
'+(r.timeSpent||0)+'h
'; h+='
'; if((r.backlinks||[]).length){ var bh=''; r.backlinks.forEach(function(b){ bh+='
'+esc(b.url)+' · DA '+(b.da||'?')+' · '+esc(b.type||'link')+'
'; }); h+=sec('All backlinks ('+r.backlinks.length+')', bh); } h+=sec('Website changes (combined)', r.websiteChanges?esc(r.websiteChanges):''); if((r.socialLinks||[]).length){ var sh=''; r.socialLinks.forEach(function(x){ sh+='
'+esc(x.platform)+': '+esc(x.url)+'
'; }); h+=sec('Social posts ('+r.socialLinks.length+')', sh); } if(r.images) h+=sec('Total images / creatives', String(r.images)); if((r.attachments||[]).length) h+=sec('Screenshots ('+r.attachments.length+')', attachGallery(r.attachments)); if((r.contentUpdates||[]).length) h+=sec('Content updates', contentUpdatesHTML(r.contentUpdates)); if((r.campaignUpdates||[]).length) h+=sec('Campaign updates', campUpdatesHTML(r.campaignUpdates)); h+=sec('Keywords worked on', r.keywords?esc(r.keywords):''); h+=sec('Traffic updates (timeline)', r.trafficUpdate?esc(r.trafficUpdate):''); h+=sec('Total time spent', r.timeSpent?(''+r.timeSpent+' hours across '+r.dayCount+' day(s)'):''); h+='
'; return h; } function viewReport(id){ var r=TA.getReports().filter(function(x){return x.id===id;})[0]; if(!r)return; var emp=TA.getUserById(r.employeeId); var pj=TA.getProjects().filter(function(p){return p.id===r.projectId;})[0]; function sec(label,val){ return val?'
'+label+'
'+val+'
':''; } var h='
'+esc(r.title)+'
'; h+='
'+esc(emp?emp.name:'Team')+' · '+esc(pj?pj.name:'')+' · '+TA.date(repDate(r))+' · '+esc(r.period||r.type||'daily')+'
'; h+='
'; if((r.backlinks||[]).length){ var bh=''; r.backlinks.forEach(function(b){ bh+='
'+esc(b.url)+' · DA '+(b.da||'?')+' · '+esc(b.type||'link')+'
'; }); h+=sec('Backlinks ('+r.backlinks.length+')', bh); } h+=sec('Website changes', r.websiteChanges?esc(r.websiteChanges):''); if((r.socialLinks||[]).length){ var sh=''; r.socialLinks.forEach(function(x){ sh+='
'+esc(x.platform)+': '+esc(x.url)+'
'; }); h+=sec('Social media posts', sh); } if(r.images) h+=sec('Images / creatives', r.images+(r.imageNotes?' — '+esc(r.imageNotes):'')); if((r.attachments||[]).length) h+=sec('Screenshots ('+r.attachments.length+')', attachGallery(r.attachments)); if((r.contentUpdates||[]).length) h+=sec('Content updates', contentUpdatesHTML(r.contentUpdates)); if((r.campaignUpdates||[]).length) h+=sec('Campaign updates', campUpdatesHTML(r.campaignUpdates)); h+=sec('Keywords', r.keywords?esc(r.keywords):''); h+=sec('Traffic update', r.trafficUpdate?esc(r.trafficUpdate):''); h+=sec('Weekly progress', r.weeklyProgress?esc(r.weeklyProgress):''); h+=sec('Notes', r.content?esc(r.content):''); h+=sec('Total time spent', r.timeSpent?(''+r.timeSpent+' hours'):''); h+='
'; h+='
'+ '
'; openM(h); } /* ---------- INVOICES ---------- */ function renderInvoices(){ var inv=myInvoices().slice().sort(function(a,b){return b.issued'+esc(i.description)+''+ ''+TA.date(i.issued)+''+TA.date(i.due)+''+ ''+esc(i.status)+''+ ''+TA.inr(i.total)+''+ ''; }); document.getElementById('inv-body').innerHTML=h; } function viewInv(id){ // use the shared professional invoice template (customer can pay) if(typeof viewInvoice==='function'){ viewInvoice(id, true); return; } var i=TA.getInvoices().filter(function(x){return x.id===id;})[0]; if(!i)return; openM('
'+esc(i.number)+'
'+ '
'+esc(i.description)+'
'+TA.inr(i.total)+'
'); } function payInv(id){ var all=TA.getInvoices(); all.forEach(function(x){if(x.id===id){x.status='paid';x.notes='Paid online via portal';}}); TA.saveInvoices(all); closeM();toast('Payment successful. Thank you!');renderInvoices();renderOv(); } /* ---------- TICKETS ---------- */ function renderTickets(){ var tk=myTickets().slice().sort(function(a,b){return b.created
'+ '
'+esc(t.number)+' · '+esc(t.title)+'
'+ '
'+esc(t.desc)+'
'+ '
'+esc(t.priority)+''+esc(t.status)+'
'+ '
'+ 'Assigned to '+esc(asg?asg.name:'Unassigned')+''+(t.replies?t.replies.length:0)+' replies · '+TA.date(t.created)+'
'; }); document.getElementById('tk-list').innerHTML=h; } function viewTk(id){ var t=TA.getTickets().filter(function(x){return x.id===id;})[0]; if(!t)return; var rh=''; (t.replies||[]).forEach(function(r){ var u=TA.getUserById(r.userId); rh+='
'+esc(u?u.avatar:'?')+'
'+ '
'+esc(u?u.name:'User')+'
'+ '
'+esc(r.text)+'
'; }); openM('
'+esc(t.number)+' · '+esc(t.title)+'
'+ '
'+esc(t.desc)+'
'+ (rh?'
Conversation
'+rh:'')+ '
'+ '
'+ '
'); } function sendReply(id){ var txt=document.getElementById('tk-reply').value.trim(); if(!txt){toast('Please type a message first',false);return;} var all=TA.getTickets(); all.forEach(function(t){if(t.id===id){t.replies=t.replies||[];t.replies.push({userId:sess.id,text:txt,at:new Date().toISOString().slice(0,10)});}}); TA.saveTickets(all); closeM();toast('Reply sent to your account manager');renderTickets(); } function newTicket(){ var pr=myProjects(), po=''; pr.forEach(function(p){po+='';}); openM('
Raise a support ticket
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'); } function saveTicket(){ var ti=document.getElementById('nt-title').value.trim(); var de=document.getElementById('nt-desc').value.trim(); if(!ti||!de){toast('Subject and description are required',false);return;} var all=TA.getTickets(); var num='TKT-'+(340+all.length+5); all.push({id:TA.uid(),number:num,title:ti,desc:de,priority:document.getElementById('nt-pri').value,status:'open', clientId:sess.id,assigneeId:'u2',created:new Date().toISOString().slice(0,10),replies:[], projectId:document.getElementById('nt-proj').value}); TA.saveTickets(all); closeM();toast('Ticket '+num+' raised. We will respond shortly.');renderTickets();renderOv(); } /* ---------- FILES ---------- */ function renderFiles(){ var pr=myProjects(); var files=[]; pr.forEach(function(p){ files.push({n:p.name+' — Project Brief.pdf',t:'PDF',s:'1.2 MB',d:p.start,p:p.name}); if(p.progress>40)files.push({n:p.name+' — Milestone Deck.pptx',t:'PPTX',s:'4.8 MB',d:p.start,p:p.name}); if(p.progress>=90)files.push({n:p.name+' — Final Deliverables.zip',t:'ZIP',s:'22.4 MB',d:p.end,p:p.name}); }); myReports().forEach(function(r){files.push({n:r.title+'.pdf',t:'PDF',s:'640 KB',d:r.submitted,p:'Reports'});}); if(!files.length){document.getElementById('fl-body').innerHTML='No files yet.';return;} var cols={PDF:'var(--er)',PPTX:'var(--wn)',ZIP:'var(--pu)'}; var h=''; files.forEach(function(f){ h+='
'+ '
'+f.t+'
'+ ''+esc(f.n)+'
'+ ''+esc(f.p)+''+f.s+''+TA.date(f.d)+''+ ''; }); document.getElementById('fl-body').innerHTML=h; } /* ---------- NOTIFICATIONS ---------- */ function buildNotifs(){ var n=[], inv=myInvoices(), tk=myTickets(), rp=myReports(), pr=myProjects(), tasks=myClientTasks(); tasks.filter(function(t){return t.status==='review';}).forEach(function(t){ n.push({i:'📋',c:'var(--brand-l)',t:'"'+t.title+'" is ready for your review.',m:'Awaiting approval'}); }); inv.forEach(function(i){if(i.status==='overdue')n.push({i:'⚠',c:'var(--er-bg)',t:'Invoice '+i.number+' of '+TA.inr(i.total)+' is overdue.',m:'2 hours ago'});}); rp.slice(0,2).forEach(function(r){n.push({i:'📝',c:'var(--brand-l)',t:'New report available: '+r.title,m:TA.date(r.submitted)});}); tk.forEach(function(t){if(t.status==='in-progress')n.push({i:'💬',c:'var(--wn-bg)',t:'Your ticket '+t.number+' is being worked on.',m:'Yesterday'});}); pr.forEach(function(p){if(p.progress>=90&&p.progress<100)n.push({i:'📈',c:'var(--ok-bg)',t:p.name+' is nearly complete ('+p.progress+'%).',m:'2 days ago'});}); var h=''; n.slice(0,6).forEach(function(x){ h+='
'+x.i+'
'+esc(x.t)+'
'+x.m+'
'; }); document.getElementById('nlist').innerHTML=h||'
No notifications
'; document.getElementById('ndot').style.display=n.length?'block':'none'; } function toggleNotif(){document.getElementById('np').classList.toggle('open');} function markRead(){var u=document.querySelectorAll('.ni.unr');for(var i=0;i