diff --git a/backend/app/services/psa/connectwise/provider.py b/backend/app/services/psa/connectwise/provider.py index 465f66ab..74f338c8 100644 --- a/backend/app/services/psa/connectwise/provider.py +++ b/backend/app/services/psa/connectwise/provider.py @@ -74,7 +74,9 @@ class ConnectWiseProvider(PSAProvider): conditions: list[str] = [] if query: - conditions.append(f"summary contains '{query}'") + # Sanitize: strip single quotes to prevent CW condition injection + safe_query = query.replace("'", "") + conditions.append(f"summary contains '{safe_query}'") if filters.get("board_id"): conditions.append(f"board/id = {filters['board_id']}") if filters.get("status_id"): @@ -89,6 +91,8 @@ class ConnectWiseProvider(PSAProvider): if board_ids: board_list = ", ".join(str(bid) for bid in board_ids) conditions.append(f"board/id in ({board_list})") + if filters.get("company_id"): + conditions.append(f"company/id = {int(filters['company_id'])}") condition_str = " and ".join(conditions) if conditions else "" if condition_str: diff --git a/frontend/src/pages/TicketsPage.tsx b/frontend/src/pages/TicketsPage.tsx index bfb28f52..64f11cb7 100644 --- a/frontend/src/pages/TicketsPage.tsx +++ b/frontend/src/pages/TicketsPage.tsx @@ -1,6 +1,7 @@ import { useEffect, useState, useCallback } from 'react' import { useSearchParams } from 'react-router-dom' -import { Plus, Ticket } from 'lucide-react' +import { Plus, Ticket, AlertTriangle } from 'lucide-react' +import axios from 'axios' import { TicketFilterBar } from '@/components/tickets/TicketFilterBar' import { TicketListRow } from '@/components/tickets/TicketListRow' import { TicketDetailPanel } from '@/components/tickets/TicketDetailPanel' @@ -37,6 +38,7 @@ export default function TicketsPage() { const [tickets, setTickets] = useState([]) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(false) + const [psaError, setPsaError] = useState(null) const [boards, setBoards] = useState([]) const [statuses, setStatuses] = useState([]) const [priorities, setPriorities] = useState([]) @@ -66,6 +68,7 @@ export default function TicketsPage() { // Fetch tickets on filter/page change const fetchTickets = useCallback(async () => { setLoading(true) + setPsaError(null) try { const result = await ticketsApi.searchTickets({ query: filters.search || undefined, @@ -90,9 +93,20 @@ export default function TicketsPage() { }) return seen.size > 0 ? Array.from(seen, ([id, name]) => ({ id, name })) : prev }) - } catch { + } catch (err: unknown) { setTickets([]) setTotal(0) + if (axios.isAxiosError(err)) { + const status = err.response?.status + const detail = (err.response?.data as { detail?: string })?.detail ?? '' + if (status === 502 && detail.toLowerCase().includes('permission')) { + setPsaError('ConnectWise returned a permissions error. Check that the API member\'s security role has Service Tickets → Inquire → ALL and System → Table Setup → Inquire → ALL.') + } else if (status === 502) { + setPsaError('ConnectWise is unavailable or returned an error. Check your integration settings.') + } else { + setPsaError('Failed to load tickets.') + } + } } finally { setLoading(false) } @@ -165,7 +179,13 @@ export default function TicketsPage() { Loading tickets… )} - {!loading && tickets.length === 0 && ( + {!loading && psaError && ( +
+ + {psaError} +
+ )} + {!loading && !psaError && tickets.length === 0 && (
No tickets match your filters