fix: chat — group members endpoint (all roles), mention first-name only, restore direct-chat button
This commit is contained in:
@@ -521,6 +521,24 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// ── POST notify ───────────────────────────────────────────────────────────
|
||||
|
||||
// ── GET /chat/members — list hotel members (accessible to all roles) ─────
|
||||
|
||||
fastify.get<SlugParam>(
|
||||
'/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<SlugParam & { Body: { text: string; system_name?: string } }>(
|
||||
'/api/hotels/:slug/chat/notify',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
|
||||
@@ -163,6 +163,7 @@ export function ChatWidget() {
|
||||
const [searchTab, setSearchTab] = useState<'people' | 'messages'>('people')
|
||||
const [loadingSearch, setLoadingSearch] = useState(false)
|
||||
const [allUsers, setAllUsers] = useState<User[]>([])
|
||||
const [chatMembers, setChatMembers] = useState<{ id: string; name: string; role: string }[]>([])
|
||||
|
||||
// Settings + pins + muted
|
||||
const [settings, setSettings] = useState<ChatSettings>(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() {
|
||||
</div>
|
||||
{view === 'rooms' && (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setView('search')} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" title="Поиск"><Search size={15} /></button>
|
||||
<button onClick={() => setView('search')} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" title="Поиск и новый чат"><PenSquare size={15} /></button>
|
||||
<button onClick={() => { setGroupName(''); setGroupMemberIds([]); setView('create-group') }} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" title="Новая группа"><UserPlus size={15} /></button>
|
||||
<button onClick={() => setView('settings')} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" title="Настройки"><Settings size={15} /></button>
|
||||
</div>
|
||||
@@ -767,10 +774,10 @@ export function ChatWidget() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{allUsers.length === 0 ? (
|
||||
{chatMembers.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8"><Loader2 size={18} className="animate-spin text-slate-400" /></div>
|
||||
) : (
|
||||
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<string, string> = {
|
||||
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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user