feat(enterprise): add multi-PSA adapter stubs for Autotask and Halo PSA
- Create AutotaskProvider stub with all abstract methods raising NotImplementedError - Create HaloPSAProvider stub with all abstract methods raising NotImplementedError - Add PSA provider grid in IntegrationsPage showing ConnectWise (active), Autotask (Coming Soon), and Halo PSA (Coming Soon) with appropriate badges Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
3
backend/app/services/psa/autotask/__init__.py
Normal file
3
backend/app/services/psa/autotask/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from app.services.psa.autotask.provider import AutotaskProvider
|
||||||
|
|
||||||
|
__all__ = ["AutotaskProvider"]
|
||||||
72
backend/app/services/psa/autotask/provider.py
Normal file
72
backend/app/services/psa/autotask/provider.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""Autotask PSA provider stub. Full implementation planned for Phase 5."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.services.psa.base import PSAProvider
|
||||||
|
from app.services.psa.types import (
|
||||||
|
ConnectionTestResult,
|
||||||
|
PSATicket,
|
||||||
|
PSANote,
|
||||||
|
PSAStatus,
|
||||||
|
PSACompany,
|
||||||
|
PSAMember,
|
||||||
|
PSAConfiguration,
|
||||||
|
PSATimeEntry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutotaskProvider(PSAProvider):
|
||||||
|
"""Stub provider for Autotask PSA. All methods raise NotImplementedError.
|
||||||
|
|
||||||
|
Full implementation is planned for Phase 5. This stub allows the provider
|
||||||
|
to be registered and discovered without causing import errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def test_connection(self) -> ConnectionTestResult:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def get_ticket(self, ticket_id: str) -> PSATicket:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def search_tickets(self, query: str, **filters) -> list[PSATicket]:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def post_note(
|
||||||
|
self,
|
||||||
|
ticket_id: str,
|
||||||
|
text: str,
|
||||||
|
note_type: str,
|
||||||
|
member_id: str | None = None,
|
||||||
|
) -> PSANote:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def update_ticket_status(
|
||||||
|
self,
|
||||||
|
ticket_id: str,
|
||||||
|
status_id: int,
|
||||||
|
) -> PSATicket:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def get_ticket_statuses(self, board_id: int) -> list[PSAStatus]:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def list_companies(self, **filters) -> list[PSACompany]:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def get_company(self, company_id: str) -> PSACompany:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def list_members(self) -> list[PSAMember]:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def get_ticket_configurations(self, ticket_id: str) -> list[PSAConfiguration]:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
|
|
||||||
|
async def create_time_entry(
|
||||||
|
self,
|
||||||
|
ticket_id: str,
|
||||||
|
member_id: str,
|
||||||
|
hours: float,
|
||||||
|
notes: str | None = None,
|
||||||
|
work_type: str | None = None,
|
||||||
|
) -> PSATimeEntry:
|
||||||
|
raise NotImplementedError("Autotask integration coming soon")
|
||||||
3
backend/app/services/psa/halopsa/__init__.py
Normal file
3
backend/app/services/psa/halopsa/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from app.services.psa.halopsa.provider import HaloPSAProvider
|
||||||
|
|
||||||
|
__all__ = ["HaloPSAProvider"]
|
||||||
72
backend/app/services/psa/halopsa/provider.py
Normal file
72
backend/app/services/psa/halopsa/provider.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""Halo PSA provider stub. Full implementation planned for Phase 5."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.services.psa.base import PSAProvider
|
||||||
|
from app.services.psa.types import (
|
||||||
|
ConnectionTestResult,
|
||||||
|
PSATicket,
|
||||||
|
PSANote,
|
||||||
|
PSAStatus,
|
||||||
|
PSACompany,
|
||||||
|
PSAMember,
|
||||||
|
PSAConfiguration,
|
||||||
|
PSATimeEntry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HaloPSAProvider(PSAProvider):
|
||||||
|
"""Stub provider for Halo PSA. All methods raise NotImplementedError.
|
||||||
|
|
||||||
|
Full implementation is planned for Phase 5. This stub allows the provider
|
||||||
|
to be registered and discovered without causing import errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def test_connection(self) -> ConnectionTestResult:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def get_ticket(self, ticket_id: str) -> PSATicket:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def search_tickets(self, query: str, **filters) -> list[PSATicket]:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def post_note(
|
||||||
|
self,
|
||||||
|
ticket_id: str,
|
||||||
|
text: str,
|
||||||
|
note_type: str,
|
||||||
|
member_id: str | None = None,
|
||||||
|
) -> PSANote:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def update_ticket_status(
|
||||||
|
self,
|
||||||
|
ticket_id: str,
|
||||||
|
status_id: int,
|
||||||
|
) -> PSATicket:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def get_ticket_statuses(self, board_id: int) -> list[PSAStatus]:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def list_companies(self, **filters) -> list[PSACompany]:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def get_company(self, company_id: str) -> PSACompany:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def list_members(self) -> list[PSAMember]:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def get_ticket_configurations(self, ticket_id: str) -> list[PSAConfiguration]:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
|
|
||||||
|
async def create_time_entry(
|
||||||
|
self,
|
||||||
|
ticket_id: str,
|
||||||
|
member_id: str,
|
||||||
|
hours: float,
|
||||||
|
notes: str | None = None,
|
||||||
|
work_type: str | None = None,
|
||||||
|
) -> PSATimeEntry:
|
||||||
|
raise NotImplementedError("Halo PSA integration coming soon")
|
||||||
@@ -259,6 +259,58 @@ export function IntegrationsPage() {
|
|||||||
{/* Connection Tab */}
|
{/* Connection Tab */}
|
||||||
{activeTab === 'connection' && (
|
{activeTab === 'connection' && (
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
|
{/* PSA Provider Grid */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-3">
|
||||||
|
Available PSA Integrations
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||||
|
{/* ConnectWise — active */}
|
||||||
|
<div className={cn(
|
||||||
|
'rounded-xl border p-4 flex flex-col gap-2',
|
||||||
|
connection
|
||||||
|
? 'border-primary/40 bg-primary/5'
|
||||||
|
: 'border-border bg-card/30'
|
||||||
|
)}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-foreground">ConnectWise PSA</span>
|
||||||
|
{connection ? (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-emerald-400/10 px-2 py-0.5 text-[0.625rem] font-label text-emerald-400">
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-[0.625rem] font-label text-primary">
|
||||||
|
Available
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Manage, ticket linking, time entries</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Autotask — coming soon */}
|
||||||
|
<div className="rounded-xl border border-border bg-card/20 p-4 flex flex-col gap-2 opacity-60 cursor-not-allowed select-none">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-foreground">Autotask PSA</span>
|
||||||
|
<span className="inline-flex items-center rounded-full bg-amber-400/10 px-2 py-0.5 text-[0.625rem] font-label text-amber-400">
|
||||||
|
Coming Soon
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Datto / Kaseya integration</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Halo PSA — coming soon */}
|
||||||
|
<div className="rounded-xl border border-border bg-card/20 p-4 flex flex-col gap-2 opacity-60 cursor-not-allowed select-none">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-foreground">Halo PSA</span>
|
||||||
|
<span className="inline-flex items-center rounded-full bg-amber-400/10 px-2 py-0.5 text-[0.625rem] font-label text-amber-400">
|
||||||
|
Coming Soon
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">HaloPSA / HaloITSM integration</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Illustrative empty state when no connection exists */}
|
{/* Illustrative empty state when no connection exists */}
|
||||||
{mode === 'setup' && (
|
{mode === 'setup' && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user