diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index 0d9d14a..05a30de 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -521,6 +521,24 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => { // ── POST notify ─────────────────────────────────────────────────────────── + // ── GET /chat/members — list hotel members (accessible to all roles) ───── + + fastify.get( + '/api/hotels/:slug/chat/members', + { onRequest: [fastify.authenticate] }, + async (request, reply) => { + const { slug } = request.params + if (!canAccess(request.user.hotelSlug, request.user.role, slug)) return reply.code(403).send({ error: 'Forbidden' }) + const hotelId = await getHotelId(slug) + if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) + const { rows } = await db.query( + `SELECT id, name, role FROM users WHERE hotel_id = $1 AND active = true ORDER BY name`, + [hotelId], + ) + return rows + }, + ) + fastify.post( '/api/hotels/:slug/chat/notify', { onRequest: [fastify.authenticate] }, diff --git a/src/components/chat/ChatWidget.tsx b/src/components/chat/ChatWidget.tsx index a610781..d23fad6 100644 --- a/src/components/chat/ChatWidget.tsx +++ b/src/components/chat/ChatWidget.tsx @@ -163,6 +163,7 @@ export function ChatWidget() { const [searchTab, setSearchTab] = useState<'people' | 'messages'>('people') const [loadingSearch, setLoadingSearch] = useState(false) const [allUsers, setAllUsers] = useState([]) + const [chatMembers, setChatMembers] = useState<{ id: string; name: string; role: string }[]>([]) // Settings + pins + muted const [settings, setSettings] = useState(loadSettings) @@ -309,13 +310,19 @@ export function ChatWidget() { // Clear typing names when leaving room useEffect(() => { if (view !== 'messages') setTypingNames([]) }, [view]) - // Load users list (for search view + @mentions in general chat + create-group) + // Load full users (manager+ only) for search view + useEffect(() => { + if (!slug || view !== 'search') return + if (allUsers.length === 0) api.users.list(slug).then(setAllUsers).catch(() => {/**/}) + }, [view, slug, allUsers.length]) + + // Load chat members (all roles) for @mentions and create-group useEffect(() => { if (!slug) return - if (view === 'search' || view === 'create-group' || (view === 'messages' && activeRoom?.type === 'general')) { - if (allUsers.length === 0) api.users.list(slug).then(setAllUsers).catch(() => {/**/}) + if (view === 'create-group' || (view === 'messages' && activeRoom?.type === 'general')) { + if (chatMembers.length === 0) api.chat.listMembers(slug).then(setChatMembers).catch(() => {/**/}) } - }, [view, slug, activeRoom?.type, allUsers.length]) + }, [view, slug, activeRoom?.type, chatMembers.length]) // Search effect useEffect(() => { @@ -370,13 +377,13 @@ export function ChatWidget() { typingTimerRef.current = setTimeout(() => sendTyping(false), 4000) } - const insertMention = (mentionUser: User) => { + const insertMention = (mentionUser: { id: string; name: string; role: string }) => { const cursor = textareaRef.current?.selectionStart ?? text.length const before = text.slice(0, cursor) const match = before.match(/@([^\s@]*)$/) if (!match) return const start = cursor - match[0].length - const newText = text.slice(0, start) + `@${mentionUser.name} ` + text.slice(cursor) + const newText = text.slice(0, start) + `@${mentionUser.name.split(' ')[0]} ` + text.slice(cursor) setText(newText) setMentionQuery(null) setTimeout(() => textareaRef.current?.focus(), 0) @@ -630,7 +637,7 @@ export function ChatWidget() { {view === 'rooms' && (
- +
@@ -767,10 +774,10 @@ export function ChatWidget() { )}
- {allUsers.length === 0 ? ( + {chatMembers.length === 0 ? (
) : ( - allUsers.filter(u => u.id !== user?.id).map(u => { + chatMembers.filter(u => u.id !== user?.id).map(u => { const selected = groupMemberIds.includes(u.id) const roleLabels: Record = { hotel_admin: 'Администратор', manager: 'Менеджер', housekeeper: 'Горничная', @@ -903,7 +910,7 @@ export function ChatWidget() { {/* @mention dropdown */} {mentionQuery !== null && (() => { const q = mentionQuery.toLowerCase() - const candidates = allUsers + const candidates = chatMembers .filter(u => u.id !== user?.id && u.name.toLowerCase().includes(q)) .slice(0, 6) if (candidates.length === 0) return null diff --git a/src/lib/api.ts b/src/lib/api.ts index f2f7910..1a81ecd 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -356,6 +356,8 @@ export const api = { req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/read`), openDirect: (slug: string, otherUserId: string) => req<{ roomId: string }>('POST', `/api/hotels/${slug}/chat/direct/${otherUserId}`), + listMembers: (slug: string) => + req<{ id: string; name: string; role: string }[]>('GET', `/api/hotels/${slug}/chat/members`), createGroup: (slug: string, name: string, memberIds: string[]) => req<{ roomId: string }>('POST', `/api/hotels/${slug}/chat/group`, { name, memberIds }), editMessage: (slug: string, roomId: string, msgId: string, text: string) =>