Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules
dist
.git
.github
.env
.env.local
.env.development
.env.production
npm-debug.log
Dockerfile
docker-compose*.yml
README.md
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
VITE_API_URL=www.apiexample.com
VITE_API_URL=www.apiexample.com/api
VITE_WS_URL=ws://apiexample.com/api/live_chat
VITE_APP_NAME=SyncDesk
3 changes: 3 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_API_URL=http://api.syncdesk.pro/api
VITE_WS_URL=ws://api.syncdesk.pro/api/live_chat
VITE_APP_NAME=SyncDesk
83 changes: 83 additions & 0 deletions .github/workflows/deploy-web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Deploy SyncDesk Web

on:
push:
branches:
- main

permissions:
contents: read
packages: write

env:
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/syncdesk-web

jobs:
build-and-deploy:
name: Build and deploy web
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Normalize image name
run: |
echo "IMAGE_NAME_LOWER=$(echo '${{ env.IMAGE_NAME }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ env.IMAGE_NAME_LOWER }}:latest
${{ env.IMAGE_NAME_LOWER }}:${{ github.sha }}
build-args: |
VITE_API_URL=${{ secrets.VITE_API_URL }}
VITE_WS_URL=${{ secrets.VITE_WS_URL }}
VITE_APP_NAME=${{ secrets.VITE_APP_NAME }}

- name: Prepare SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.VM_SSH_KEY }}" > ~/.ssh/syncdesk_vm_key
chmod 600 ~/.ssh/syncdesk_vm_key
ssh-keyscan -H ${{ secrets.VM_HOST }} >> ~/.ssh/known_hosts

- name: Deploy on VM
run: |
ssh -i ~/.ssh/syncdesk_vm_key ${{ secrets.VM_USER }}@${{ secrets.VM_HOST }} << EOF
set -e

mkdir -p /opt/syncdesk/syncdesk-web
cd /opt/syncdesk/syncdesk-web

cat > docker-compose.prod.yml << 'COMPOSE'
services:
syncdesk-web:
image: ${IMAGE_NAME_LOWER}:latest
container_name: syncdesk_web
restart: unless-stopped
ports:
- "80:80"
environment:
TZ: America/Sao_Paulo
COMPOSE

if [ -n "${{ secrets.GHCR_TOKEN }}" ]; then
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
fi

docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker image prune -f
EOF
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM node:22-alpine AS builder

WORKDIR /app

ARG VITE_API_URL
ARG VITE_WS_URL
ARG VITE_APP_NAME

ENV VITE_API_URL=$VITE_API_URL
ENV VITE_WS_URL=$VITE_WS_URL
ENV VITE_APP_NAME=$VITE_APP_NAME

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

FROM nginx:1.27-alpine AS runner

COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
16 changes: 16 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
services:
syncdesk-web:
build:
context: .
dockerfile: Dockerfile
args:
VITE_API_URL: ${VITE_API_URL}
VITE_WS_URL: ${VITE_WS_URL}
VITE_APP_NAME: ${VITE_APP_NAME}
image: syncdesk-web:latest
container_name: syncdesk_web
restart: unless-stopped
ports:
- "80:80"
environment:
TZ: America/Sao_Paulo
25 changes: 25 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
server {
listen 80;
server_name _;

root /usr/share/nginx/html;
index index.html;

client_max_body_size 20m;

location / {
try_files $uri $uri/ /index.html;
}

location /health {
access_log off;
add_header Content-Type text/plain;
return 200 "ok";
}

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
try_files $uri =404;
}
}
8 changes: 6 additions & 2 deletions src/app/router.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import Dashboard from '@/features/dashboard/pages/Dashboard'
import Chat from '@/features/chat/pages/Chat'
import Usuarios from '@/features/users/pages/Usuarios'
import CadastrarUsuario from '@/features/users/pages/CadastrarUsuario'
import EditarUsuario from '@/features/users/pages/EditarUsuario'
import EditarCliente from '@/features/users/pages/EditarCliente'
import EditarAtendente from '@/features/users/pages/EditarAtendente'
import Chamados from '@/features/ticket/pages/Chamados'
import AberturaChamado from '@/features/ticket/pages/AberturaChamado'
import ModificarChamado from '@/features/ticket/pages/ModificarChamado'
import Configuracoes from '@/features/settings/pages/Configuracoes'

export function AppRouter() {
return (
Expand All @@ -33,11 +35,13 @@ export function AppRouter() {
<Route path="/chamados" element={<Chamados />} />
<Route path="/chamados/novo" element={<AberturaChamado />} />
<Route path="/chamados/:ticketId/editar" element={<ModificarChamado />} />
<Route path="/configuracoes" element={<Configuracoes />} />

<Route element={<AdminOnlyLayout />}>
<Route path="/usuarios" element={<Usuarios />} />
<Route path="/usuarios/novo" element={<CadastrarUsuario />} />
<Route path="/usuarios/:userId/editar" element={<EditarUsuario />} />
<Route path="/usuarios/:userId/editar-cliente" element={<EditarCliente />} />
<Route path="/usuarios/:userId/editar-atendente" element={<EditarAtendente />} />
</Route>
</Route>
</Route>
Expand Down
26 changes: 19 additions & 7 deletions src/features/auth/hooks/useLoginMutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { useMutation } from '@tanstack/react-query'
import { login } from '@/features/auth/api/auth-service'
import { useAuthStore } from '@/stores/auth-stores'
import { decodeJwtPayload } from '@/shared/utils/jwt'
import { getUserById } from '@/features/users/api/user-service'

export function useLoginMutation() {
const setSession = useAuthStore((state) => state.setSession)

return useMutation({
mutationFn: login,
onSuccess: (data) => {
onSuccess: async (data) => {
const tokenPayload = decodeJwtPayload(data?.access_token)

const derivedUser = tokenPayload?.sub
Expand All @@ -18,18 +19,29 @@ export function useLoginMutation() {
}
: null

const user = data?.user
? {
...derivedUser,
...data.user
}
const partialUser = data?.user
? { ...derivedUser, ...data.user }
: derivedUser

setSession({
user,
user: partialUser,
accessToken: data?.access_token || null,
refreshToken: data?.refresh_token || null
})

if (partialUser?.id) {
try {
const fullUser = await getUserById(partialUser.id)
if (fullUser) {
setSession({
user: { ...partialUser, ...fullUser },
accessToken: data?.access_token || null,
refreshToken: data?.refresh_token || null
})
}
} catch {
}
}
}
})
}
47 changes: 42 additions & 5 deletions src/features/chat/api/chat-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ function normalizeListResponse(data) {
return data.items
}

if (Array.isArray(data?.data?.items)) {
return data.data.items
}

if (Array.isArray(data?.data)) {
return data.data
}
Expand All @@ -20,6 +24,10 @@ function normalizeObjectResponse(data) {
return data?.data ?? data
}

function isNotFoundError(error) {
return error?.response?.status === 404
}

export async function getActiveConversations(search = '') {
const params = search ? { search } : undefined
const { data } = await http.get('/conversations/active', { params })
Expand All @@ -35,9 +43,9 @@ export async function getPaginatedMessages(ticketId, { page = 1, limit = 20 } =

return {
messages: Array.isArray(payload?.messages) ? payload.messages : [],
total: payload?.total ?? 0,
page: payload?.page ?? page,
limit: payload?.limit ?? limit,
total: Number(payload?.total ?? 0),
page: Number(payload?.page ?? page),
limit: Number(payload?.limit ?? limit),
has_next: Boolean(payload?.has_next)
}
}
Expand All @@ -53,6 +61,35 @@ export async function takeTicket(ticketId) {
}

export async function getAttendanceById(triageId) {
const { data } = await http.get(`/chatbot/${triageId}`)
return normalizeObjectResponse(data)
if (!triageId) {
return null
}

try {
const { data } = await http.get(`/chatbot/${triageId}`)
return normalizeObjectResponse(data)
} catch (error) {
if (isNotFoundError(error)) {
return null
}

throw error
}
}

export async function getChatSessions(search = '') {
return getActiveConversations(search)
}

export async function getChatMessages(ticketId) {
const result = await getPaginatedMessages(ticketId, { page: 1, limit: 100 })
return result.messages
}

export async function flagChatSession() {
throw new Error('A API atual do backend não possui endpoint REST para sinalizar sessão de chat.')
}

export async function sendChatMessage() {
throw new Error('O envio de mensagens do chat ao vivo deve ser feito via WebSocket.')
}
8 changes: 6 additions & 2 deletions src/features/chat/hooks/useAssumeChatSessionMutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ export function useAssumeChatSessionMutation() {

return useMutation({
mutationFn: assumeConversation,
onSuccess: () => {
onSuccess: (_, chatId) => {
queryClient.invalidateQueries({ queryKey: ['chat', 'active-conversations'] })
queryClient.invalidateQueries({ queryKey: ['chat', 'messages'] })
queryClient.invalidateQueries({ queryKey: ['tickets'] })

if (chatId) {
queryClient.invalidateQueries({ queryKey: ['chat', 'conversation', chatId] })
}
}
})
}
9 changes: 7 additions & 2 deletions src/features/chat/hooks/useAttendanceQuery.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { useQuery } from '@tanstack/react-query'
import { getAttendanceById } from '@/features/chat/api/chat-service'

export function useAttendanceQuery(triageId) {
export function useAttendanceQuery(triageId, options = {}) {
const { enabled: optionEnabled = true, ...queryOptions } = options

return useQuery({
queryKey: ['chatbot', 'attendance', triageId],
queryFn: () => getAttendanceById(triageId),
enabled: Boolean(triageId)
retry: false,
staleTime: 30000,
...queryOptions,
enabled: Boolean(triageId) && optionEnabled
})
}
10 changes: 8 additions & 2 deletions src/features/chat/hooks/useGetPaginatedMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { useInfiniteQuery } from '@tanstack/react-query'
import { getPaginatedMessages } from '@/features/chat/api/chat-service'

export function useGetPaginatedMessages(ticketId, limit = 20, options = {}) {
const enabled = Boolean(ticketId) && (options.enabled ?? true)
const { enabled: optionEnabled = true, ...queryOptions } = options
const enabled = Boolean(ticketId) && optionEnabled

return useInfiniteQuery({
queryKey: ['chat', 'messages', ticketId, limit],
queryFn: ({ pageParam = 1 }) => getPaginatedMessages(ticketId, { page: pageParam, limit }),
initialPageParam: 1,
getNextPageParam: (lastPage) => (lastPage?.has_next ? lastPage.page + 1 : undefined),
enabled
enabled,
retry: false,
staleTime: 30000,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
...queryOptions
})
}
Loading
Loading