529 lines
22 KiB
JavaScript
529 lines
22 KiB
JavaScript
'use strict'
|
|
|
|
/**
|
|
* equipeController.js — CRUD para Equipe Virtual (Fase 12)
|
|
*
|
|
* Recursos:
|
|
* accounts — colaboradores internos
|
|
* sectors — setores operacionais
|
|
* team_members — vínculo conta ↔ setor
|
|
* sector_brains — cérebro de conhecimento por setor
|
|
* inbox_interna — mensagens internas (Human API, escalações, colaboração)
|
|
*/
|
|
|
|
const bcrypt = require('bcryptjs')
|
|
const { query, queryOne, execute } = require('../database/postgres')
|
|
|
|
// ─── helpers ───────────────────────────────────────────────────────────────
|
|
|
|
function tid(req) { return req.tenantId || 1 }
|
|
|
|
function ok(res, data) { return res.json(data) }
|
|
function err(res, status, msg) { return res.status(status).json({ error: msg }) }
|
|
function serverErr(res, e, tag) {
|
|
console.error(`[equipe] ${tag}:`, e)
|
|
return res.status(500).json({ error: 'Erro interno' })
|
|
}
|
|
|
|
// ─── ACCOUNTS ───────────────────────────────────────────────────────────────
|
|
|
|
async function listAccounts(req, res) {
|
|
try {
|
|
const { role, active } = req.query
|
|
let sql = `
|
|
SELECT a.id, a.name, a.email, a.role, a.phone, a.phone_connected,
|
|
a.availability, a.avatar_url, a.active, a.created_at,
|
|
a.nw_team_id, a.nw_sector_id,
|
|
COALESCE(
|
|
json_agg(
|
|
json_build_object(
|
|
'sector_id', tm.sector_id,
|
|
'sector_name', s.name,
|
|
'function', tm.function,
|
|
'is_primary', tm.is_primary,
|
|
'on_duty', tm.on_duty
|
|
)
|
|
) FILTER (WHERE tm.id IS NOT NULL), '[]'
|
|
) AS sectors
|
|
FROM accounts a
|
|
LEFT JOIN team_members tm ON tm.account_id = a.id
|
|
LEFT JOIN sectors s ON s.id = tm.sector_id
|
|
WHERE a.tenant_id = $1
|
|
`
|
|
const params = [tid(req)]
|
|
if (role) { params.push(role); sql += ` AND a.role = $${params.length}` }
|
|
if (active !== undefined) { params.push(active === 'true'); sql += ` AND a.active = $${params.length}` }
|
|
sql += ` GROUP BY a.id ORDER BY a.name`
|
|
const rows = await query(sql, params)
|
|
ok(res, rows)
|
|
} catch (e) { serverErr(res, e, 'listAccounts') }
|
|
}
|
|
|
|
async function getAccount(req, res) {
|
|
try {
|
|
const row = await queryOne(
|
|
`SELECT id, name, email, role, phone, phone_connected, availability, avatar_url,
|
|
active, created_at, nw_team_id, nw_sector_id
|
|
FROM accounts WHERE id = $1 AND tenant_id = $2`,
|
|
[req.params.id, tid(req)]
|
|
)
|
|
if (!row) return err(res, 404, 'Conta não encontrada')
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'getAccount') }
|
|
}
|
|
|
|
async function createAccount(req, res) {
|
|
try {
|
|
const { name, email, password, role, phone, avatar_url } = req.body
|
|
if (!name) return err(res, 400, 'name obrigatório')
|
|
const validRoles = ['owner','manager','supervisor','secretary','agent','viewer']
|
|
if (role && !validRoles.includes(role)) return err(res, 400, 'role inválido')
|
|
|
|
let hash = null
|
|
if (password) hash = await bcrypt.hash(password, 10)
|
|
|
|
const row = await queryOne(
|
|
`INSERT INTO accounts (tenant_id, name, email, password_hash, role, phone, avatar_url)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id, name, email, role, phone, availability, active`,
|
|
[tid(req), name, email||null, hash, role||'agent', phone||null, avatar_url||null]
|
|
)
|
|
ok(res, row)
|
|
} catch (e) {
|
|
if (e.code === '23505') return err(res, 409, 'E-mail já cadastrado neste tenant')
|
|
serverErr(res, e, 'createAccount')
|
|
}
|
|
}
|
|
|
|
async function updateAccount(req, res) {
|
|
try {
|
|
const { name, email, password, role, phone, availability, avatar_url, active,
|
|
phone_connected, nw_team_id, nw_sector_id } = req.body
|
|
const id = req.params.id
|
|
|
|
const existing = await queryOne('SELECT id FROM accounts WHERE id=$1 AND tenant_id=$2', [id, tid(req)])
|
|
if (!existing) return err(res, 404, 'Conta não encontrada')
|
|
|
|
const sets = []
|
|
const params = []
|
|
|
|
if (name !== undefined) { params.push(name); sets.push(`name=$${params.length}`) }
|
|
if (email !== undefined) { params.push(email); sets.push(`email=$${params.length}`) }
|
|
if (role !== undefined) { params.push(role); sets.push(`role=$${params.length}`) }
|
|
if (phone !== undefined) { params.push(phone); sets.push(`phone=$${params.length}`) }
|
|
if (availability !== undefined) { params.push(availability); sets.push(`availability=$${params.length}`) }
|
|
if (avatar_url !== undefined) { params.push(avatar_url); sets.push(`avatar_url=$${params.length}`) }
|
|
if (active !== undefined) { params.push(active); sets.push(`active=$${params.length}`) }
|
|
if (phone_connected !== undefined) { params.push(phone_connected); sets.push(`phone_connected=$${params.length}`) }
|
|
if (nw_team_id !== undefined) { params.push(nw_team_id); sets.push(`nw_team_id=$${params.length}`) }
|
|
if (nw_sector_id !== undefined) { params.push(nw_sector_id); sets.push(`nw_sector_id=$${params.length}`) }
|
|
if (password) {
|
|
const hash = await bcrypt.hash(password, 10)
|
|
params.push(hash); sets.push(`password_hash=$${params.length}`)
|
|
}
|
|
|
|
if (sets.length === 0) return err(res, 400, 'Nenhum campo para atualizar')
|
|
|
|
params.push(id, tid(req))
|
|
const row = await queryOne(
|
|
`UPDATE accounts SET ${sets.join(', ')} WHERE id=$${params.length-1} AND tenant_id=$${params.length}
|
|
RETURNING id, name, email, role, phone, phone_connected, availability, active`,
|
|
params
|
|
)
|
|
ok(res, row)
|
|
} catch (e) {
|
|
if (e.code === '23505') return err(res, 409, 'E-mail já cadastrado neste tenant')
|
|
serverErr(res, e, 'updateAccount')
|
|
}
|
|
}
|
|
|
|
async function deleteAccount(req, res) {
|
|
try {
|
|
const result = await execute(
|
|
'DELETE FROM accounts WHERE id=$1 AND tenant_id=$2',
|
|
[req.params.id, tid(req)]
|
|
)
|
|
if (result.rowCount === 0) return err(res, 404, 'Conta não encontrada')
|
|
ok(res, { deleted: true })
|
|
} catch (e) { serverErr(res, e, 'deleteAccount') }
|
|
}
|
|
|
|
// ─── SECTORS ────────────────────────────────────────────────────────────────
|
|
|
|
async function listSectors(req, res) {
|
|
try {
|
|
const rows = await query(
|
|
`SELECT s.id, s.name, s.description, s.icon, s.color, s.sla_minutes,
|
|
s.sort_order, s.active, s.created_at,
|
|
COUNT(tm.id)::int AS member_count
|
|
FROM sectors s
|
|
LEFT JOIN team_members tm ON tm.sector_id = s.id
|
|
WHERE s.tenant_id = $1
|
|
GROUP BY s.id
|
|
ORDER BY s.sort_order, s.name`,
|
|
[tid(req)]
|
|
)
|
|
ok(res, rows)
|
|
} catch (e) { serverErr(res, e, 'listSectors') }
|
|
}
|
|
|
|
async function getSector(req, res) {
|
|
try {
|
|
const sector = await queryOne(
|
|
'SELECT * FROM sectors WHERE id=$1 AND tenant_id=$2',
|
|
[req.params.id, tid(req)]
|
|
)
|
|
if (!sector) return err(res, 404, 'Setor não encontrado')
|
|
|
|
const members = await query(
|
|
`SELECT a.id, a.name, a.role, a.availability, a.phone_connected,
|
|
tm.function, tm.is_primary, tm.on_duty, tm.notify_escalation, tm.notify_human_api
|
|
FROM team_members tm
|
|
JOIN accounts a ON a.id = tm.account_id
|
|
WHERE tm.sector_id = $1`,
|
|
[req.params.id]
|
|
)
|
|
ok(res, { ...sector, members })
|
|
} catch (e) { serverErr(res, e, 'getSector') }
|
|
}
|
|
|
|
async function createSector(req, res) {
|
|
try {
|
|
const { name, description, icon, color, sla_minutes, sort_order } = req.body
|
|
if (!name) return err(res, 400, 'name obrigatório')
|
|
const row = await queryOne(
|
|
`INSERT INTO sectors (tenant_id, name, description, icon, color, sla_minutes, sort_order)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
|
|
[tid(req), name, description||null, icon||'Briefcase', color||'#6366f1',
|
|
sla_minutes||60, sort_order||0]
|
|
)
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'createSector') }
|
|
}
|
|
|
|
async function updateSector(req, res) {
|
|
try {
|
|
const { name, description, icon, color, sla_minutes, sort_order, active } = req.body
|
|
const id = req.params.id
|
|
const existing = await queryOne('SELECT id FROM sectors WHERE id=$1 AND tenant_id=$2', [id, tid(req)])
|
|
if (!existing) return err(res, 404, 'Setor não encontrado')
|
|
|
|
const sets = []; const params = []
|
|
if (name !== undefined) { params.push(name); sets.push(`name=$${params.length}`) }
|
|
if (description !== undefined) { params.push(description); sets.push(`description=$${params.length}`) }
|
|
if (icon !== undefined) { params.push(icon); sets.push(`icon=$${params.length}`) }
|
|
if (color !== undefined) { params.push(color); sets.push(`color=$${params.length}`) }
|
|
if (sla_minutes !== undefined) { params.push(sla_minutes); sets.push(`sla_minutes=$${params.length}`) }
|
|
if (sort_order !== undefined) { params.push(sort_order); sets.push(`sort_order=$${params.length}`) }
|
|
if (active !== undefined) { params.push(active); sets.push(`active=$${params.length}`) }
|
|
if (sets.length === 0) return err(res, 400, 'Nenhum campo para atualizar')
|
|
|
|
params.push(id, tid(req))
|
|
const row = await queryOne(
|
|
`UPDATE sectors SET ${sets.join(', ')} WHERE id=$${params.length-1} AND tenant_id=$${params.length} RETURNING *`,
|
|
params
|
|
)
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'updateSector') }
|
|
}
|
|
|
|
async function deleteSector(req, res) {
|
|
try {
|
|
const result = await execute('DELETE FROM sectors WHERE id=$1 AND tenant_id=$2', [req.params.id, tid(req)])
|
|
if (result.rowCount === 0) return err(res, 404, 'Setor não encontrado')
|
|
ok(res, { deleted: true })
|
|
} catch (e) { serverErr(res, e, 'deleteSector') }
|
|
}
|
|
|
|
// ─── TEAM MEMBERS ────────────────────────────────────────────────────────────
|
|
|
|
async function listTeamMembers(req, res) {
|
|
try {
|
|
const { sector_id } = req.query
|
|
let sql = `
|
|
SELECT tm.id, tm.account_id, tm.sector_id, tm.function, tm.is_primary,
|
|
tm.on_duty, tm.notify_escalation, tm.notify_human_api, tm.created_at,
|
|
a.name AS account_name, a.email AS account_email, a.role AS account_role,
|
|
a.availability, a.phone_connected,
|
|
s.name AS sector_name, s.color AS sector_color, s.icon AS sector_icon
|
|
FROM team_members tm
|
|
JOIN accounts a ON a.id = tm.account_id
|
|
JOIN sectors s ON s.id = tm.sector_id
|
|
WHERE a.tenant_id = $1
|
|
`
|
|
const params = [tid(req)]
|
|
if (sector_id) { params.push(sector_id); sql += ` AND tm.sector_id = $${params.length}` }
|
|
sql += ' ORDER BY s.name, a.name'
|
|
const rows = await query(sql, params)
|
|
ok(res, rows)
|
|
} catch (e) { serverErr(res, e, 'listTeamMembers') }
|
|
}
|
|
|
|
async function addTeamMember(req, res) {
|
|
try {
|
|
const { account_id, sector_id, function: fn, is_primary, notify_escalation, notify_human_api } = req.body
|
|
if (!account_id || !sector_id) return err(res, 400, 'account_id e sector_id obrigatórios')
|
|
|
|
// verifica que a conta pertence ao tenant
|
|
const acc = await queryOne('SELECT id FROM accounts WHERE id=$1 AND tenant_id=$2', [account_id, tid(req)])
|
|
if (!acc) return err(res, 404, 'Conta não encontrada')
|
|
const sec = await queryOne('SELECT id FROM sectors WHERE id=$1 AND tenant_id=$2', [sector_id, tid(req)])
|
|
if (!sec) return err(res, 404, 'Setor não encontrado')
|
|
|
|
const row = await queryOne(
|
|
`INSERT INTO team_members (account_id, sector_id, function, is_primary, notify_escalation, notify_human_api)
|
|
VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
|
|
[account_id, sector_id, fn||null,
|
|
is_primary !== undefined ? is_primary : false,
|
|
notify_escalation !== undefined ? notify_escalation : true,
|
|
notify_human_api !== undefined ? notify_human_api : true]
|
|
)
|
|
ok(res, row)
|
|
} catch (e) {
|
|
if (e.code === '23505') return err(res, 409, 'Membro já vinculado a este setor')
|
|
serverErr(res, e, 'addTeamMember')
|
|
}
|
|
}
|
|
|
|
async function updateTeamMember(req, res) {
|
|
try {
|
|
const { function: fn, is_primary, on_duty, notify_escalation, notify_human_api } = req.body
|
|
const id = req.params.id
|
|
const sets = []; const params = []
|
|
if (fn !== undefined) { params.push(fn); sets.push(`function=$${params.length}`) }
|
|
if (is_primary !== undefined) { params.push(is_primary); sets.push(`is_primary=$${params.length}`) }
|
|
if (on_duty !== undefined) { params.push(on_duty); sets.push(`on_duty=$${params.length}`) }
|
|
if (notify_escalation !== undefined) { params.push(notify_escalation); sets.push(`notify_escalation=$${params.length}`) }
|
|
if (notify_human_api !== undefined) { params.push(notify_human_api); sets.push(`notify_human_api=$${params.length}`) }
|
|
if (sets.length === 0) return err(res, 400, 'Nenhum campo para atualizar')
|
|
params.push(id)
|
|
const row = await queryOne(`UPDATE team_members SET ${sets.join(', ')} WHERE id=$${params.length} RETURNING *`, params)
|
|
if (!row) return err(res, 404, 'Membro não encontrado')
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'updateTeamMember') }
|
|
}
|
|
|
|
async function removeTeamMember(req, res) {
|
|
try {
|
|
const result = await execute('DELETE FROM team_members WHERE id=$1', [req.params.id])
|
|
if (result.rowCount === 0) return err(res, 404, 'Membro não encontrado')
|
|
ok(res, { deleted: true })
|
|
} catch (e) { serverErr(res, e, 'removeTeamMember') }
|
|
}
|
|
|
|
// ─── SECTOR BRAIN NODES ─────────────────────────────────────────────────────
|
|
|
|
async function listSectorBrains(req, res) {
|
|
try {
|
|
const rows = await query(
|
|
`SELECT * FROM sector_brain_nodes WHERE sector_id=$1 ORDER BY sort_order, id`,
|
|
[req.params.sectorId]
|
|
)
|
|
ok(res, rows)
|
|
} catch (e) { serverErr(res, e, 'listSectorBrains') }
|
|
}
|
|
|
|
async function createSectorBrain(req, res) {
|
|
try {
|
|
const { type, content, sort_order } = req.body
|
|
if (!content) return err(res, 400, 'content obrigatório')
|
|
const row = await queryOne(
|
|
`INSERT INTO sector_brain_nodes (sector_id, type, content, sort_order)
|
|
VALUES ($1,$2,$3,$4) RETURNING *`,
|
|
[req.params.sectorId, type||'knowledge', content, sort_order||0]
|
|
)
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'createSectorBrain') }
|
|
}
|
|
|
|
async function updateSectorBrain(req, res) {
|
|
try {
|
|
const { type, content, sort_order, active } = req.body
|
|
const sets = []; const params = []
|
|
if (type !== undefined) { params.push(type); sets.push(`type=$${params.length}`) }
|
|
if (content !== undefined) { params.push(content); sets.push(`content=$${params.length}`) }
|
|
if (sort_order !== undefined) { params.push(sort_order); sets.push(`sort_order=$${params.length}`) }
|
|
if (active !== undefined) { params.push(active); sets.push(`active=$${params.length}`) }
|
|
if (sets.length === 0) return err(res, 400, 'Nenhum campo para atualizar')
|
|
params.push(req.params.id)
|
|
const row = await queryOne(`UPDATE sector_brain_nodes SET ${sets.join(', ')} WHERE id=$${params.length} RETURNING *`, params)
|
|
if (!row) return err(res, 404, 'Node não encontrado')
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'updateSectorBrain') }
|
|
}
|
|
|
|
async function deleteSectorBrain(req, res) {
|
|
try {
|
|
const result = await execute('DELETE FROM sector_brain_nodes WHERE id=$1', [req.params.id])
|
|
if (result.rowCount === 0) return err(res, 404, 'Node não encontrado')
|
|
ok(res, { deleted: true })
|
|
} catch (e) { serverErr(res, e, 'deleteSectorBrain') }
|
|
}
|
|
|
|
// ─── PERFIL DO COLABORADOR LOGADO ────────────────────────────────────────────
|
|
|
|
async function me(req, res) {
|
|
try {
|
|
const accountId = req.teamAccount?.accountId || req.admin?.id
|
|
const tenantId = req.teamAccount?.tenantId || req.tenantId || 1
|
|
if (!accountId) return err(res, 401, 'Não autenticado')
|
|
|
|
const row = await queryOne(`
|
|
SELECT a.id, a.name, a.email, a.role, a.phone, a.phone_connected,
|
|
a.availability, a.avatar_url, a.active, a.nw_team_id, a.nw_sector_id,
|
|
COALESCE(
|
|
json_agg(json_build_object(
|
|
'sector_id', s.id, 'sector_name', s.name,
|
|
'function', tm.function, 'is_primary', tm.is_primary, 'on_duty', tm.on_duty
|
|
)) FILTER (WHERE tm.id IS NOT NULL), '[]'
|
|
) AS sectors
|
|
FROM accounts a
|
|
LEFT JOIN team_members tm ON tm.account_id = a.id
|
|
LEFT JOIN sectors s ON s.id = tm.sector_id
|
|
WHERE a.id = $1 AND a.tenant_id = $2
|
|
GROUP BY a.id
|
|
`, [accountId, tenantId])
|
|
|
|
if (!row) return err(res, 404, 'Conta não encontrada')
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'me') }
|
|
}
|
|
|
|
// ─── INBOX INTERNA ───────────────────────────────────────────────────────────
|
|
|
|
/** Lista threads únicas com última mensagem + contador de não lidas */
|
|
async function listThreads(req, res) {
|
|
try {
|
|
// Segurança: colaborador só pode ver sua própria inbox.
|
|
// Admin pode passar account_id na query para inspecionar qualquer conta.
|
|
const isAdmin = !!req.admin
|
|
// queriedId pode ser NaN se não for passado — trata como null
|
|
const queriedId = req.query.account_id ? parseInt(req.query.account_id) : null
|
|
const ownId = req.teamAccount?.accountId
|
|
|
|
if (!isAdmin && queriedId && queriedId !== ownId) {
|
|
return err(res, 403, 'Acesso negado: você só pode ver sua própria inbox')
|
|
}
|
|
|
|
// Colaborador sem account_id na query usa o próprio id do token
|
|
const account_id = isAdmin ? (queriedId || null) : (queriedId || ownId)
|
|
if (!account_id) return ok(res, [])
|
|
|
|
const rows = await query(`
|
|
SELECT DISTINCT ON (im.thread_id)
|
|
im.thread_id,
|
|
im.type,
|
|
im.sector_id,
|
|
s.name AS sector_name,
|
|
im.content AS last_message,
|
|
im.created_at AS last_at,
|
|
im.ref_id,
|
|
im.chat_id,
|
|
COUNT(im2.id) FILTER (WHERE im2.read_at IS NULL AND im2.to_account = $2)::int AS unread
|
|
FROM internal_messages im
|
|
LEFT JOIN sectors s ON s.id = im.sector_id
|
|
LEFT JOIN internal_messages im2 ON im2.thread_id = im.thread_id
|
|
WHERE im.tenant_id = $1
|
|
AND (im.to_account = $2 OR im.sector_id IN (
|
|
SELECT sector_id FROM team_members WHERE account_id = $2
|
|
))
|
|
GROUP BY im.thread_id, im.type, im.sector_id, s.name, im.content, im.created_at, im.ref_id, im.chat_id
|
|
ORDER BY im.thread_id, im.created_at DESC
|
|
`, [tid(req), account_id])
|
|
ok(res, rows)
|
|
} catch (e) { serverErr(res, e, 'listThreads') }
|
|
}
|
|
|
|
/** Lista mensagens de uma thread */
|
|
async function getThread(req, res) {
|
|
try {
|
|
const rows = await query(
|
|
`SELECT im.*, a.name AS from_name, a.avatar_url AS from_avatar
|
|
FROM internal_messages im
|
|
LEFT JOIN accounts a ON a.id = im.from_account
|
|
WHERE im.thread_id = $1 AND im.tenant_id = $2
|
|
ORDER BY im.created_at`,
|
|
[req.params.threadId, tid(req)]
|
|
)
|
|
ok(res, rows)
|
|
} catch (e) { serverErr(res, e, 'getThread') }
|
|
}
|
|
|
|
/** Envia mensagem em uma thread (ou cria nova thread) */
|
|
async function postMessage(req, res) {
|
|
try {
|
|
const { thread_id, from_account, to_account, sector_id, type, content, ref_id } = req.body
|
|
if (!content) return err(res, 400, 'content obrigatório')
|
|
|
|
const threadId = thread_id || `t-${Date.now()}-${Math.random().toString(36).slice(2,8)}`
|
|
const row = await queryOne(
|
|
`INSERT INTO internal_messages (tenant_id, thread_id, from_account, to_account, sector_id, type, content, ref_id)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
|
|
[tid(req), threadId, from_account||null, to_account||null, sector_id||null,
|
|
type||'collaboration', content, ref_id||null]
|
|
)
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'postMessage') }
|
|
}
|
|
|
|
/** Marca mensagens de uma thread como lidas */
|
|
async function markRead(req, res) {
|
|
try {
|
|
// Colaborador só pode marcar suas próprias mensagens como lidas
|
|
const isAdmin = !!req.admin
|
|
const ownId = req.teamAccount?.accountId
|
|
const bodyId = parseInt(req.body.account_id)
|
|
const account_id = isAdmin ? bodyId : ownId
|
|
|
|
if (!isAdmin && bodyId && bodyId !== ownId) {
|
|
return err(res, 403, 'Acesso negado')
|
|
}
|
|
if (!account_id) return ok(res, { ok: true })
|
|
|
|
await execute(
|
|
`UPDATE internal_messages SET read_at = NOW()
|
|
WHERE thread_id = $1 AND to_account = $2 AND read_at IS NULL`,
|
|
[req.params.threadId, account_id]
|
|
)
|
|
ok(res, { ok: true })
|
|
} catch (e) { serverErr(res, e, 'markRead') }
|
|
}
|
|
|
|
/** Conta não lidas de uma conta */
|
|
async function unreadCount(req, res) {
|
|
try {
|
|
const isAdmin = !!req.admin
|
|
const queriedId = parseInt(req.query.account_id)
|
|
const ownId = req.teamAccount?.accountId
|
|
|
|
if (!isAdmin && queriedId && queriedId !== ownId) {
|
|
return err(res, 403, 'Acesso negado')
|
|
}
|
|
const account_id = isAdmin ? queriedId : ownId
|
|
if (!account_id) return ok(res, { count: 0 })
|
|
|
|
const row = await queryOne(
|
|
`SELECT COUNT(*)::int AS count FROM internal_messages
|
|
WHERE tenant_id=$1 AND to_account=$2 AND read_at IS NULL`,
|
|
[tid(req), account_id]
|
|
)
|
|
ok(res, row)
|
|
} catch (e) { serverErr(res, e, 'unreadCount') }
|
|
}
|
|
|
|
// ─── EXPORTS ─────────────────────────────────────────────────────────────────
|
|
|
|
module.exports = {
|
|
// perfil do colaborador logado
|
|
me,
|
|
// accounts
|
|
listAccounts, getAccount, createAccount, updateAccount, deleteAccount,
|
|
// sectors
|
|
listSectors, getSector, createSector, updateSector, deleteSector,
|
|
// team members
|
|
listTeamMembers, addTeamMember, updateTeamMember, removeTeamMember,
|
|
// sector brains
|
|
listSectorBrains, createSectorBrain, updateSectorBrain, deleteSectorBrain,
|
|
// inbox interna
|
|
listThreads, getThread, postMessage, markRead, unreadCount,
|
|
}
|