289 lines
9.1 KiB
TypeScript
289 lines
9.1 KiB
TypeScript
/**
|
|
* 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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
SELECT ticketid, subject, priority
|
|
FROM tbltickets
|
|
WHERE status = 1
|
|
`)
|
|
return rows
|
|
}
|
|
|
|
// Leads
|
|
export async function getContactarLeads() {
|
|
const [rows] = await db.query<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
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<RowDataPacket[]>(`
|
|
SELECT COUNT(*) as qtd, COALESCE(SUM(subtotal), 0) as valor
|
|
FROM tblproposals
|
|
WHERE status = 4
|
|
`)
|
|
|
|
const [total] = await db.query<RowDataPacket[]>(`
|
|
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)
|
|
}
|
|
}
|