feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker

This commit is contained in:
VPS 4 Builder
2026-05-15 10:39:35 +02:00
commit 3beac0e0f2
288 changed files with 78761 additions and 0 deletions
+161
View File
@@ -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