Hi-Tech Tracker

Sign in with your work account

Hi-Tech Activity Tracker

Team Activity

Activity by Consultant

Bar chart

Activity breakdown

Donut chart

Team Pipeline — Closest to Closing

Placements this month

Recent Activity

Today's Snapshot

Monthly billing by consultant

/* ════════════════════════════════════════════════════════════ PLACEMENT DETAILS MODAL ════════════════════════════════════════════════════════════ */ const PIPE_STAGES = { 'Sourcing':10,'Screening':25,'Submitted':40,'1st Interview':55,'2nd Interview':70,'3rd Interview':80,'Offer':90 }; const PIPE_COLORS = { 'Sourcing':'#888780','Screening':'#378ADD','Submitted':'#7F77DD','1st Interview':'#EF9F27','2nd Interview':'#D4537E','3rd Interview':'#993556','Offer':'#1D9E75' }; function openPlDetailModal(clientOwner, candidateOwner, candName, company) { document.getElementById('plDetailOwners').textContent = `Client owner: ${clientOwner} · Candidate owner: ${candidateOwner}`; document.getElementById('plRole').value = company || ''; document.getElementById('plClient').value = company || ''; document.getElementById('plCand').value = candName || ''; document.getElementById('plFee').value = ''; document.getElementById('plDate').value = todayStr(); document.getElementById('plNotes').value = ''; document.getElementById('plDetailModal').classList.add('open'); } function closePlDetail() { document.getElementById('plDetailModal').classList.remove('open'); pendingPlOwners = null; } async function savePlDetail() { if (!pendingPlOwners) return; const role = document.getElementById('plRole').value.trim(); const date = document.getElementById('plDate').value; if (!role || !date) { alert('Role and date are required.'); return; } const { clientOwner, candidateOwner } = pendingPlOwners; const fee = parseFloat(document.getElementById('plFee').value) || 0; const cand = document.getElementById('plCand').value.trim(); const client= document.getElementById('plClient').value.trim(); const notes = document.getElementById('plNotes').value.trim(); try { // Save placement record await db.collection('placements').add({ loggedBy: currentUserName, uid: currentUser.uid, clientOwner, candidateOwner, role, client, candidateName: cand, fee, date, notes, timestamp: firebase.firestore.FieldValue.serverTimestamp(), }); // Also log the activity count await logActivity('placement', clientOwner, candidateOwner, cand, client); closePlDetail(); } catch(e) { console.error('Save placement error:', e); alert('Failed to save placement. Please try again.'); } } document.getElementById('plDetailModal').addEventListener('click', e => { if (e.target.id === 'plDetailModal') closePlDetail(); }); /* ════════════════════════════════════════════════════════════ PIPELINE MODAL ════════════════════════════════════════════════════════════ */ function openAddPipeline() { editPipeId = null; ppClientOwner = currentUserName; ppCandOwner = currentUserName; document.getElementById('pipeModalTitle').textContent = 'Add to pipeline'; ['ppRole','ppClient','ppCand','ppFee'].forEach(id => document.getElementById(id).value = ''); document.getElementById('ppStage').value = '1st Interview'; renderPipeTags(); document.getElementById('pipeModal').classList.add('open'); } function closePipeModal() { document.getElementById('pipeModal').classList.remove('open'); editPipeId = null; ppClientOwner = ''; ppCandOwner = ''; } function renderPipeTags() { document.getElementById('ppClientOwnerTags').innerHTML = CONSULTANTS.map(n => `` ).join(''); document.getElementById('ppCandOwnerTags').innerHTML = CONSULTANTS.map(n => `` ).join(''); } async function savePipeline() { const role = document.getElementById('ppRole').value.trim(); const client= document.getElementById('ppClient').value.trim(); if (!role) { alert('Role title is required.'); return; } const data = { role, client, candidateName: document.getElementById('ppCand').value.trim(), fee: parseFloat(document.getElementById('ppFee').value) || 0, stage: document.getElementById('ppStage').value, clientOwner: ppClientOwner || currentUserName, candidateOwner: ppCandOwner || currentUserName, addedBy: currentUserName, uid: currentUser.uid, updatedAt: firebase.firestore.FieldValue.serverTimestamp(), }; try { if (editPipeId) { await db.collection('pipeline').doc(editPipeId).update(data); } else { data.createdAt = firebase.firestore.FieldValue.serverTimestamp(); await db.collection('pipeline').add(data); } closePipeModal(); } catch(e) { console.error('Pipeline save error:', e); alert('Failed to save. Please try again.'); } } function editPipelineRow(id) { const row = allPipeline.find(r => r.id === id); if (!row) return; editPipeId = id; ppClientOwner = row.clientOwner || ''; ppCandOwner = row.candidateOwner || ''; document.getElementById('pipeModalTitle').textContent = 'Edit pipeline'; document.getElementById('ppRole').value = row.role || ''; document.getElementById('ppClient').value = row.client || ''; document.getElementById('ppCand').value = row.candidateName || ''; document.getElementById('ppFee').value = row.fee || ''; document.getElementById('ppStage').value = row.stage || '1st Interview'; renderPipeTags(); document.getElementById('pipeModal').classList.add('open'); } async function advancePipeStage(id) { const row = allPipeline.find(r => r.id === id); if (!row) return; const stages = Object.keys(PIPE_STAGES); const idx = stages.indexOf(row.stage); if (idx < stages.length - 1) { try { await db.collection('pipeline').doc(id).update({ stage: stages[idx + 1], updatedAt: firebase.firestore.FieldValue.serverTimestamp() }); } catch(e) { console.error(e); } } } async function deletePipelineRow(id) { if (!confirm('Remove from pipeline?')) return; try { await db.collection('pipeline').doc(id).delete(); } catch(e) { console.error(e); alert('Could not remove. Please try again.'); } } document.getElementById('pipeModal').addEventListener('click', e => { if (e.target.id === 'pipeModal') closePipeModal(); }); /* ════════════════════════════════════════════════════════════ RENDER PIPELINE ════════════════════════════════════════════════════════════ */ function renderPipeline() { const filterName = document.getElementById('cFilter')?.value || ''; let rows = allPipeline.filter(r => !filterName || r.clientOwner === filterName || r.candidateOwner === filterName || r.addedBy === filterName ); rows = [...rows].sort((a, b) => (PIPE_STAGES[b.stage]||0) - (PIPE_STAGES[a.stage]||0)); if (!rows.length) { document.getElementById('pipelineList').innerHTML = '

No pipeline yet — click "+ Add to pipeline" to start tracking

'; return; } document.getElementById('pipelineList').innerHTML = rows.map(r => { const pct = PIPE_STAGES[r.stage] || 10; const col = PIPE_COLORS[r.stage] || '#888'; const pctCol= pct>=85?'#639922':pct>=65?'#1D9E75':pct>=45?'#EF9F27':'#E24B4A'; const canEdit = r.addedBy === currentUserName || isAdmin; return `
${r.role} ${r.stage}
${r.client ? `🏢 ${r.client}` : ''} ${r.candidateName? `👤 ${r.candidateName}` : ''} Client: ${r.clientOwner||'—'} · Cand: ${r.candidateOwner||'—'}
${pct}%
${r.fee ? `¥${r.fee}M` : ''} ${canEdit ? ` ` : ''}
`; }).join(''); } /* ════════════════════════════════════════════════════════════ RENDER PLACEMENTS PANEL ════════════════════════════════════════════════════════════ */ function renderPlacementsPanel() { const now = new Date(); const mon = now.toLocaleDateString('en-GB', { month:'long', year:'numeric' }); document.getElementById('plMonthLabel').textContent = mon; if (!allPlacements.length) { document.getElementById('plPanel').innerHTML = `

No placements logged this month

`; return; } const total = allPlacements.reduce((s, p) => s + (p.fee||0), 0); document.getElementById('plPanel').innerHTML = `
${allPlacements.length} placements ¥${total.toFixed(1)}M billed
${allPlacements.sort((a,b) => b.date > a.date ? 1:-1).map(p => `

${p.role||'—'}

${p.client||''} · ${p.candidateName||''} · ${p.date}

Client: ${p.clientOwner||'—'} · Cand: ${p.candidateOwner||'—'}

¥${p.fee||0}M ${p.uid === currentUser?.uid || isAdmin ? `` : ''}
`).join('')} `; } async function deletePlacement(id) { if (!confirm('Remove this placement?')) return; try { await db.collection('placements').doc(id).delete(); } catch(e) { console.error(e); alert('Could not remove. Please try again.'); } } function promptPlacement() { // Start the placement flow: open joint modal with placement type pendingLog = 'placement'; selectedOwner = ''; myRole = ''; openModal('placement'); } /* ════════════════════════════════════════════════════════════ RENDER BILLING PANEL ════════════════════════════════════════════════════════════ */ function renderBillingPanel() { const now = new Date(); const mon = now.toLocaleDateString('en-GB', { month:'long', year:'numeric' }); document.getElementById('billingTitle').textContent = `Billing by consultant — ${mon}`; const bilMap = {}; CONSULTANTS.forEach(n => bilMap[n] = { fees:0, count:0 }); allPlacements.forEach(p => { if (bilMap[p.clientOwner]) { bilMap[p.clientOwner].fees += p.fee||0; bilMap[p.clientOwner].count++; } if (bilMap[p.candidateOwner] && p.candidateOwner !== p.clientOwner) { bilMap[p.candidateOwner].fees += p.fee||0; bilMap[p.candidateOwner].count++; } }); const maxFee = Math.max(...Object.values(bilMap).map(v => v.fees), 0.1); const rows = CONSULTANTS.filter(n => bilMap[n].count > 0) .sort((a,b) => bilMap[b].fees - bilMap[a].fees); if (!rows.length) { document.getElementById('billingList').innerHTML = '

No billing data this month yet

'; return; } document.getElementById('billingList').innerHTML = rows.map(name => { const { fees, count } = bilMap[name]; const pct = Math.round(fees / maxFee * 100); return `
${name}
${count} pl. ¥${fees.toFixed(1)}M
`; }).join(''); } document.getElementById('jointModal').addEventListener('click', e => { if (e.target.id === 'jointModal') closeModal(); });