/** * Dashboard Queries Service * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import db from '../db.js' import type { RowDataPacket } from 'mysql2' // Helper: Get week range (Monday-Sunday) function getWeekRange() { const now = new Date() const dow = now.getDay() const diff = dow === 0 ? -6 : 1 - dow const monday = new Date(now) monday.setDate(now.getDate() + diff) monday.setHours(0, 0, 0, 0) const sunday = new Date(monday) sunday.setDate(monday.getDate() + 6) sunday.setHours(23, 59, 59, 999) return { inicio_semana: monday.toISOString().split('T')[0], fim_semana: sunday.toISOString().split('T')[0], inicio_unix: Math.floor(monday.getTime() / 1000) } } // Tasks export async function getUrgenteTasks() { const [rows] = await db.query(` SELECT t.id, t.name, t.status, t.duedate, p.name as projeto FROM tbltasks t LEFT JOIN tblprojects p ON t.rel_id = p.id AND t.rel_type = 'project' WHERE t.priority = 4 AND t.status IN (1,4) `) return rows } export async function getAltaTasks() { const [rows] = await db.query(` SELECT t.id, t.name, t.status, t.duedate, p.name as projeto, DATEDIFF(t.duedate, CURDATE()) as dias FROM tbltasks t LEFT JOIN tblprojects p ON t.rel_id = p.id AND t.rel_type = 'project' WHERE t.priority = 3 AND t.status IN (1,4) `) return rows } export async function getVencidasTasks() { const [rows] = await db.query(` SELECT t.id, t.name, t.duedate, p.name as projeto, DATEDIFF(CURDATE(), t.duedate) as dias_atraso FROM tbltasks t LEFT JOIN tblprojects p ON t.rel_id = p.id AND t.rel_type = 'project' WHERE t.duedate < CURDATE() AND t.status IN (1,4) AND (t.description NOT LIKE '%[TAGS: ongoing]%' OR t.description IS NULL) `) return rows } export async function getEmTestesTasks() { const [rows] = await db.query(` SELECT t.id, t.name, t.duedate, p.name as projeto FROM tbltasks t LEFT JOIN tblprojects p ON t.rel_id = p.id AND t.rel_type = 'project' WHERE t.status = 3 ORDER BY t.duedate ASC `) return rows } export async function getEstaSemana() { const { inicio_semana, fim_semana } = getWeekRange() const [rows] = await db.query(` SELECT t.id, t.name, t.duedate, p.name as projeto, DATEDIFF(t.duedate, CURDATE()) as dias FROM tbltasks t LEFT JOIN tblprojects p ON t.rel_id = p.id AND t.rel_type = 'project' WHERE t.duedate BETWEEN ? AND ? AND t.status IN (1,3,4) `, [inicio_semana, fim_semana]) return rows } export async function getMondayMood() { const now = new Date() if (now.getDay() !== 1) return [] const [rows] = await db.query(` SELECT t.id, t.name, t.status, p.name as projeto FROM tbltasks t LEFT JOIN tblprojects p ON t.rel_id = p.id AND t.rel_type = 'project' INNER JOIN tbltaggables tg ON t.id = tg.rel_id AND tg.rel_type = 'task' WHERE tg.tag_id = 67 AND t.status IN (1,4) ORDER BY t.name `) return rows } // Tickets export async function getTickets() { const [rows] = await db.query(` SELECT ticketid, subject, priority FROM tbltickets WHERE status = 1 `) return rows } // Leads export async function getContactarLeads() { const [rows] = await db.query(` SELECT l.id, l.name, l.company, DATEDIFF(CURDATE(), l.dateadded) as dias, (SELECT name FROM tblleads_sources WHERE id = l.source) as source FROM tblleads l WHERE l.lost = 0 AND l.junk = 0 AND ( l.status = 14 OR (l.status = 12 AND ( (SELECT MAX(date) FROM tbllead_activity_log WHERE leadid = l.id) IS NULL OR (SELECT MAX(date) FROM tbllead_activity_log WHERE leadid = l.id) < DATE_SUB(CURDATE(), INTERVAL 60 DAY) )) ) AND NOT EXISTS ( SELECT 1 FROM tblreminders r WHERE r.rel_id = l.id AND r.rel_type = 'lead' AND r.date > NOW() AND r.isnotified = 0 ) ORDER BY l.dateadded DESC `) return rows } export async function getFollowupLeads() { const [rows] = await db.query(` SELECT l.id, l.name, l.company, DATEDIFF(CURDATE(), l.dateadded) as dias, (SELECT name FROM tblleads_sources WHERE id = l.source) as source, DATEDIFF(CURDATE(), (SELECT MAX(date) FROM tbllead_activity_log WHERE leadid = l.id)) as dias_sem_contacto FROM tblleads l WHERE l.status = 12 AND l.lost = 0 AND l.junk = 0 AND (SELECT MAX(date) FROM tbllead_activity_log WHERE leadid = l.id) >= DATE_SUB(CURDATE(), INTERVAL 60 DAY) AND NOT EXISTS ( SELECT 1 FROM tblreminders r WHERE r.rel_id = l.id AND r.rel_type = 'lead' AND r.date > NOW() AND r.isnotified = 0 ) ORDER BY (SELECT MAX(date) FROM tbllead_activity_log WHERE leadid = l.id) ASC `) return rows } export async function getPropostaLeads() { const [rows] = await db.query(` SELECT id, name, company, DATEDIFF(CURDATE(), dateadded) as dias, (SELECT name FROM tblleads_sources WHERE id = tblleads.source) as source FROM tblleads WHERE status = 4 AND lost = 0 AND junk = 0 `) return rows } // Projects export async function getProjectos() { const [rows] = await db.query(` SELECT p.id, p.name, c.company as cliente, (SELECT COUNT(*) FROM tbltasks WHERE rel_id = p.id AND rel_type = 'project') as total, (SELECT COUNT(*) FROM tbltasks WHERE rel_id = p.id AND rel_type = 'project' AND status = 5) as concluidas FROM tblprojects p LEFT JOIN tblclients c ON p.clientid = c.userid WHERE p.status IN (2, 53) `) return rows } // Timesheet export async function getTimesheet() { const { inicio_semana, inicio_unix } = getWeekRange() const [rows] = await db.query(` SELECT SUM( CASE WHEN LENGTH(start_time) < 15 THEN (CAST(end_time AS UNSIGNED) - CAST(start_time AS UNSIGNED)) / 3600 ELSE TIMESTAMPDIFF(SECOND, start_time, end_time) / 3600 END ) as horas FROM tbltaskstimers WHERE staff_id = 1 AND end_time IS NOT NULL AND ((LENGTH(start_time) < 15 AND CAST(start_time AS UNSIGNED) >= ?) OR (LENGTH(start_time) >= 15 AND start_time >= ?)) `, [inicio_unix, inicio_semana]) const horas = Math.round((rows[0]?.horas || 0) * 10) / 10 const horas_pct = Math.min(100, Math.round((horas / 40) * 100)) return { horas, horas_pct } } // Billing 360 export async function getBilling360() { const [rows] = await db.query(` SELECT c.client_name, c.desk_project_id, c.monthly_hours, COALESCE(SUM(i.hours_invoiced), 0) as total_invoiced, (SELECT COALESCE(SUM( CASE WHEN LENGTH(tt.start_time) < 15 THEN (CAST(tt.end_time AS UNSIGNED) - CAST(tt.start_time AS UNSIGNED)) / 3600 ELSE TIMESTAMPDIFF(SECOND, tt.start_time, tt.end_time) / 3600 END ), 0) FROM tbltaskstimers tt INNER JOIN tbltasks t ON tt.task_id = t.id WHERE t.rel_id = c.desk_project_id AND t.rel_type = 'project' AND tt.end_time IS NOT NULL ) as total_delivered FROM billing_360_clients c LEFT JOIN billing_360_invoices i ON c.id = i.client_id WHERE c.active = 1 GROUP BY c.id ORDER BY c.client_name `) return rows.map(client => { const total_delivered = Math.round(client.total_delivered * 10) / 10 const balance = Math.round((client.total_invoiced - total_delivered) * 10) / 10 let status: 'credit' | 'debt' | 'ok' if (balance > 5) status = 'credit' else if (balance < -5) status = 'debt' else status = 'ok' return { ...client, total_delivered, balance, status } }) } // Pipeline export async function getPipeline() { const [leads] = await db.query(` SELECT ls.name as fase, COUNT(l.id) as qtd, COALESCE(SUM(l.lead_value), 0) as valor FROM tblleads l LEFT JOIN tblleads_status ls ON l.status = ls.id WHERE l.lost = 0 AND l.junk = 0 AND l.status NOT IN (1) GROUP BY l.status, ls.name, ls.statusorder ORDER BY ls.statusorder `) const [estimates] = await db.query(` SELECT CASE status WHEN 2 THEN 'Enviado' WHEN 3 THEN 'Visto' END as fase, COUNT(*) as qtd, SUM(subtotal) as valor FROM tblestimates WHERE status IN (2,3) GROUP BY status `) const [proposals] = await db.query(` SELECT COUNT(*) as qtd, COALESCE(SUM(subtotal), 0) as valor FROM tblproposals WHERE status = 4 `) const [total] = await db.query(` SELECT ( (SELECT COALESCE(SUM(lead_value), 0) FROM tblleads WHERE lost=0 AND junk=0 AND status NOT IN (1)) + (SELECT COALESCE(SUM(subtotal), 0) FROM tblestimates WHERE status IN (2,3)) + (SELECT COALESCE(SUM(subtotal), 0) FROM tblproposals WHERE status = 4) ) as total `) return { pipeline_leads: leads, pipeline_estimates: estimates, pipeline_proposals: proposals[0] || { qtd: 0, valor: 0 }, pipeline_valor: Math.round(total[0]?.total || 0) } }