feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
'use strict'
|
||||
|
||||
const { query, queryOne } = require('../database/postgres')
|
||||
|
||||
class GroupChat {
|
||||
/**
|
||||
* Listar grupos em que o colaborador participa (via team_members) com unread_count
|
||||
*/
|
||||
static async listForAccount(tenantId, accountId) {
|
||||
return query(
|
||||
`SELECT gc.id, gc.sector_id, gc.name,
|
||||
s.icon, s.color,
|
||||
(SELECT gm.content
|
||||
FROM group_messages gm
|
||||
WHERE gm.group_chat_id = gc.id
|
||||
ORDER BY gm.created_at DESC LIMIT 1) AS last_message,
|
||||
(SELECT gm.created_at
|
||||
FROM group_messages gm
|
||||
WHERE gm.group_chat_id = gc.id
|
||||
ORDER BY gm.created_at DESC LIMIT 1) AS last_message_at,
|
||||
(SELECT COUNT(*)::int
|
||||
FROM group_messages gm2
|
||||
LEFT JOIN group_last_read glr
|
||||
ON glr.group_chat_id = gm2.group_chat_id AND glr.account_id = $2
|
||||
WHERE gm2.group_chat_id = gc.id
|
||||
AND gm2.from_account != $2
|
||||
AND (glr.last_read_at IS NULL OR gm2.created_at > glr.last_read_at)
|
||||
) AS unread_count
|
||||
FROM group_chats gc
|
||||
JOIN sectors s ON s.id = gc.sector_id
|
||||
JOIN team_members tm ON tm.sector_id = gc.sector_id AND tm.account_id = $2
|
||||
WHERE gc.tenant_id = $1
|
||||
ORDER BY s.sort_order, gc.name`,
|
||||
[tenantId, accountId]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar todos os grupos do tenant (para owners/managers) com unread_count
|
||||
*/
|
||||
static async listAll(tenantId, accountId) {
|
||||
return query(
|
||||
`SELECT gc.id, gc.sector_id, gc.name,
|
||||
s.icon, s.color,
|
||||
(SELECT gm.content
|
||||
FROM group_messages gm
|
||||
WHERE gm.group_chat_id = gc.id
|
||||
ORDER BY gm.created_at DESC LIMIT 1) AS last_message,
|
||||
(SELECT gm.created_at
|
||||
FROM group_messages gm
|
||||
WHERE gm.group_chat_id = gc.id
|
||||
ORDER BY gm.created_at DESC LIMIT 1) AS last_message_at,
|
||||
(SELECT COUNT(*)::int
|
||||
FROM group_messages gm2
|
||||
LEFT JOIN group_last_read glr
|
||||
ON glr.group_chat_id = gm2.group_chat_id AND glr.account_id = $2
|
||||
WHERE gm2.group_chat_id = gc.id
|
||||
AND gm2.from_account != $2
|
||||
AND (glr.last_read_at IS NULL OR gm2.created_at > glr.last_read_at)
|
||||
) AS unread_count
|
||||
FROM group_chats gc
|
||||
JOIN sectors s ON s.id = gc.sector_id
|
||||
WHERE gc.tenant_id = $1
|
||||
ORDER BY s.sort_order, gc.name`,
|
||||
[tenantId, accountId]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar grupo como lido (upsert last_read_at)
|
||||
*/
|
||||
static async markRead(accountId, groupChatId) {
|
||||
await queryOne(
|
||||
`INSERT INTO group_last_read (account_id, group_chat_id, last_read_at)
|
||||
VALUES ($1, $2, NOW())
|
||||
ON CONFLICT (account_id, group_chat_id)
|
||||
DO UPDATE SET last_read_at = NOW()`,
|
||||
[accountId, groupChatId]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar grupo por id, verificando pertença ao tenant
|
||||
*/
|
||||
static async findById(tenantId, groupChatId) {
|
||||
return queryOne(
|
||||
`SELECT gc.*, s.name AS sector_name, s.icon, s.color,
|
||||
(SELECT COUNT(*)::int FROM team_members tm WHERE tm.sector_id = s.id) AS member_count
|
||||
FROM group_chats gc
|
||||
JOIN sectors s ON s.id = gc.sector_id
|
||||
WHERE gc.id = $1 AND gc.tenant_id = $2`,
|
||||
[groupChatId, tenantId]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar mensagens de um grupo (paginação por cursor)
|
||||
*/
|
||||
static async getMessages(tenantId, groupChatId, limit = 50, before = null) {
|
||||
const params = [tenantId, groupChatId, limit]
|
||||
const beforeClause = before ? `AND gm.created_at < $4` : ''
|
||||
if (before) params.push(before)
|
||||
|
||||
const msgs = await query(
|
||||
`SELECT gm.id, gm.from_account, gm.content, gm.created_at,
|
||||
a.name AS sender_name, a.avatar_url AS sender_avatar
|
||||
FROM group_messages gm
|
||||
LEFT JOIN accounts a ON a.id = gm.from_account
|
||||
WHERE gm.tenant_id = $1
|
||||
AND gm.group_chat_id = $2
|
||||
${beforeClause}
|
||||
ORDER BY gm.created_at DESC
|
||||
LIMIT $3`,
|
||||
params
|
||||
)
|
||||
return msgs.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar mensagem em um grupo
|
||||
*/
|
||||
static async sendMessage(tenantId, groupChatId, fromAccount, content) {
|
||||
return queryOne(
|
||||
`INSERT INTO group_messages (tenant_id, group_chat_id, from_account, content)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, group_chat_id, from_account, content, created_at`,
|
||||
[tenantId, groupChatId, fromAccount, content]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listar membros de um grupo (via team_members do setor)
|
||||
*/
|
||||
static async getMembers(tenantId, groupChatId) {
|
||||
return query(
|
||||
`SELECT a.id, a.name, a.avatar_url, a.availability
|
||||
FROM group_chats gc
|
||||
JOIN team_members tm ON tm.sector_id = gc.sector_id
|
||||
JOIN accounts a ON a.id = tm.account_id
|
||||
WHERE gc.id = $1 AND gc.tenant_id = $2 AND a.active = TRUE
|
||||
ORDER BY a.name`,
|
||||
[groupChatId, tenantId]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* IDs dos membros de um grupo (para SSE direcionado)
|
||||
*/
|
||||
static async getMemberIds(groupChatId) {
|
||||
const rows = await query(
|
||||
`SELECT tm.account_id
|
||||
FROM group_chats gc
|
||||
JOIN team_members tm ON tm.sector_id = gc.sector_id
|
||||
WHERE gc.id = $1`,
|
||||
[groupChatId]
|
||||
)
|
||||
return rows.map(r => r.account_id)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GroupChat
|
||||
Reference in New Issue
Block a user