feat: Mimecast multi-tenant held mail viewer
- migration 062: mimecast_tenants table (company_id, client_id/secret, account_code) - Seed Wulf (CUSA13A95) + Seubert (CUSA96A181) tenants - MimecastClient.getHeldMessages(): full pagination via meta.pagination.next cursor (API always returns 10/page regardless of pageSize param, totalCount in meta) - getMimecastClientForTenant() factory for per-tenant instantiation - GET /api/mimecast/held?tenantId=&recipient= — fetches all tenants in parallel, merges + sorts by date, returns per-tenant counts + combined messages[] - Held Mail tab on /admin/sync/mimecast (on-demand load, recipient filter, tenant badges, policy filter dropdown, DMARC/impersonation highlighted red)
This commit is contained in:
parent
a98c0daf15
commit
fcdec8e38b
12 changed files with 2208 additions and 27 deletions
|
|
@ -7,7 +7,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||
import {
|
||||
ArrowLeft, Activity, History, Calendar, Mail, RefreshCw, Loader2,
|
||||
CheckCircle2, XCircle, AlertTriangle, Shield, Inbox, Send,
|
||||
Clock, ChevronDown, ChevronRight,
|
||||
Clock, ChevronDown, ChevronRight, Users, Search, LockKeyhole, UnlockKeyhole,
|
||||
PauseCircle, Building2,
|
||||
} from 'lucide-react';
|
||||
import SyncScheduler from '@/components/admin/SyncScheduler';
|
||||
|
||||
|
|
@ -288,6 +289,117 @@ function ThreatsTab() {
|
|||
);
|
||||
}
|
||||
|
||||
// ── Cloud Users Tab ───────────────────────────────────────────────────────────
|
||||
function CloudUserTab() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [domain, setDomain] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
|
||||
const handleEmailChange = (v: string) => {
|
||||
setEmail(v);
|
||||
const atIdx = v.indexOf('@');
|
||||
if (atIdx >= 0) setDomain(v.slice(atIdx + 1));
|
||||
};
|
||||
|
||||
const lookup = async () => {
|
||||
if (!email || !domain) return;
|
||||
setLoading(true);
|
||||
setResult(null);
|
||||
setShowRaw(false);
|
||||
try {
|
||||
const params = new URLSearchParams({ emailAddress: email, domain });
|
||||
const res = await fetch(`/api/mimecast/cloud-user?${params}`);
|
||||
setResult(await res.json());
|
||||
} catch (err: any) {
|
||||
setResult({ error: err.message });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const user = result?.user;
|
||||
const lockedOut = user?.lockedOut ?? false;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-wrap gap-2 items-end">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-muted-foreground">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="user@domain.com"
|
||||
value={email}
|
||||
onChange={e => handleEmailChange(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && lookup()}
|
||||
className="border rounded-md px-3 py-1.5 text-sm bg-background w-72"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-muted-foreground">Domain</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="domain.com"
|
||||
value={domain}
|
||||
onChange={e => setDomain(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && lookup()}
|
||||
className="border rounded-md px-3 py-1.5 text-sm bg-background w-48"
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" onClick={lookup} disabled={loading || !email || !domain}>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-1" /> : <Search className="w-4 h-4 mr-1" />}
|
||||
Look Up
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{result?.error && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 dark:bg-red-950/20 p-3 text-sm text-red-600">
|
||||
{result.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && !result.error && !result.found && (
|
||||
<div className="rounded-lg border p-4 text-sm text-muted-foreground">
|
||||
User not found in Mimecast Cloud Gateway.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user && (
|
||||
<div className="space-y-3">
|
||||
<div className={`rounded-lg border p-4 flex items-center gap-3 ${lockedOut ? 'border-red-400 bg-red-50 dark:bg-red-950/20' : 'border-green-400 bg-green-50 dark:bg-green-950/20'}`}>
|
||||
{lockedOut
|
||||
? <LockKeyhole className="w-5 h-5 text-red-600 shrink-0" />
|
||||
: <UnlockKeyhole className="w-5 h-5 text-green-600 shrink-0" />}
|
||||
<div>
|
||||
<p className={`font-semibold text-sm ${lockedOut ? 'text-red-700' : 'text-green-700'}`}>
|
||||
{lockedOut ? 'Account Locked Out' : 'Account Active'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{user.name && <span>{user.name} · </span>}
|
||||
{user.emailAddress}
|
||||
{user.status && <span> · Status: {user.status}</span>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="text-xs text-muted-foreground underline-offset-2 hover:underline"
|
||||
onClick={() => setShowRaw(v => !v)}
|
||||
>
|
||||
{showRaw ? 'Hide' : 'Show'} raw response
|
||||
</button>
|
||||
{showRaw && (
|
||||
<pre className="rounded-lg border bg-muted/30 p-3 text-xs overflow-auto max-h-72">
|
||||
{JSON.stringify(user._raw ?? user, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── History Tab ───────────────────────────────────────────────────────────────
|
||||
function HistoryTab() {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
|
|
@ -346,6 +458,177 @@ function HistoryTab() {
|
|||
);
|
||||
}
|
||||
|
||||
// ── Held Mail Tab ─────────────────────────────────────────────────────────────
|
||||
function HeldMailTab() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [tenantFilter, setTenantFilter] = useState('');
|
||||
const [policyFilter, setPolicyFilter] = useState('');
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const load = (recipientVal?: string) => {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams();
|
||||
const r = recipientVal ?? recipient;
|
||||
if (r) params.set('recipient', r);
|
||||
if (tenantFilter) params.set('tenantId', tenantFilter);
|
||||
fetch(`/api/mimecast/held?${params}`)
|
||||
.then(res => res.json())
|
||||
.then(d => { setData(d); setLoaded(true); })
|
||||
.catch(() => setData(null))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const messages: any[] = data?.messages ?? [];
|
||||
const filtered = policyFilter
|
||||
? messages.filter(m => m.policyInfo?.toLowerCase().includes(policyFilter.toLowerCase()))
|
||||
: messages;
|
||||
|
||||
const policies = [...new Set(messages.map((m: any) => m.policyInfo).filter(Boolean))].sort();
|
||||
const tenants: any[] = data?.tenants ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search bar */}
|
||||
<div className="flex flex-wrap gap-2 items-end">
|
||||
<div className="flex-1 min-w-56">
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Recipient email</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="filter by recipient address…"
|
||||
value={recipient}
|
||||
onChange={e => setRecipient(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && load()}
|
||||
className="w-full border rounded-md px-3 py-1.5 text-sm bg-background"
|
||||
/>
|
||||
</div>
|
||||
{tenants.length > 1 && (
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Tenant</label>
|
||||
<select value={tenantFilter} onChange={e => setTenantFilter(e.target.value)}
|
||||
className="border rounded-md px-3 py-1.5 text-sm bg-background">
|
||||
<option value="">All tenants</option>
|
||||
{tenants.map(t => (
|
||||
<option key={t.tenantId} value={String(t.tenantId)}>{t.accountName}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{loaded && policies.length > 0 && (
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">Policy</label>
|
||||
<select value={policyFilter} onChange={e => setPolicyFilter(e.target.value)}
|
||||
className="border rounded-md px-3 py-1.5 text-sm bg-background min-w-48">
|
||||
<option value="">All policies</option>
|
||||
{policies.map(p => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={() => load()} disabled={loading} className="gap-2">
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
|
||||
{loaded ? 'Refresh' : 'Load Held Mail'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tenant summary badges */}
|
||||
{loaded && tenants.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tenants.map(t => (
|
||||
<div key={t.tenantId} className={`flex items-center gap-1.5 rounded-full px-3 py-1 text-xs border ${
|
||||
t.error ? 'border-red-300 bg-red-50 dark:bg-red-950/20 text-red-600' :
|
||||
t.count > 0 ? 'border-yellow-400/40 bg-yellow-500/5 text-yellow-700' :
|
||||
'border-border bg-muted/30 text-muted-foreground'
|
||||
}`}>
|
||||
<Building2 className="w-3 h-3" />
|
||||
<span className="font-medium">{t.accountName}</span>
|
||||
{t.error
|
||||
? <span>error</span>
|
||||
: <span>— {t.count.toLocaleString()}{t.totalCount > t.count ? ` of ${t.totalCount.toLocaleString()}` : ''} held</span>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loaded && !loading && (
|
||||
<div className="rounded-lg border border-dashed p-12 text-center text-muted-foreground text-sm">
|
||||
Click “Load Held Mail” to fetch held messages across all configured Mimecast tenants.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">Fetching held messages from all tenants…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loaded && !loading && filtered.length === 0 && (
|
||||
<div className="rounded-lg border p-12 text-center text-muted-foreground text-sm">
|
||||
No held messages found.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loaded && !loading && filtered.length > 0 && (
|
||||
<div className="rounded-lg border overflow-hidden">
|
||||
<div className="px-4 py-2 bg-muted/40 border-b flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
{filtered.length.toLocaleString()} held message{filtered.length !== 1 ? 's' : ''}
|
||||
{data?.totalCount > filtered.length ? ` (showing ${filtered.length} of ${data.totalCount.toLocaleString()} total)` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/30">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2 font-medium">Date</th>
|
||||
<th className="text-left px-4 py-2 font-medium">To</th>
|
||||
<th className="text-left px-4 py-2 font-medium">From</th>
|
||||
<th className="text-left px-4 py-2 font-medium">Subject</th>
|
||||
<th className="text-left px-4 py-2 font-medium">Policy</th>
|
||||
{tenants.length > 1 && <th className="text-left px-4 py-2 font-medium">Tenant</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{filtered.map((m: any) => (
|
||||
<tr key={m.id} className="hover:bg-muted/20">
|
||||
<td className="px-4 py-2 text-muted-foreground whitespace-nowrap text-xs">
|
||||
{new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="text-xs">{m.to}</div>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="font-medium text-xs">{m.fromDisplay || m.from}</div>
|
||||
{m.fromDisplay && <div className="text-xs text-muted-foreground">{m.from}</div>}
|
||||
</td>
|
||||
<td className="px-4 py-2 max-w-xs">
|
||||
<div className="truncate">{m.subject || '(no subject)'}</div>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
m.policyInfo?.includes('DMARC') || m.policyInfo?.includes('Impersonation')
|
||||
? 'bg-red-500/10 text-red-600'
|
||||
: 'bg-yellow-500/10 text-yellow-700'
|
||||
}`}>
|
||||
{m.policyInfo || m.reason || '—'}
|
||||
</span>
|
||||
</td>
|
||||
{tenants.length > 1 && (
|
||||
<td className="px-4 py-2 text-xs text-muted-foreground">{m.accountName}</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||||
export default function MimecastSyncPage() {
|
||||
const [statusData, setStatusData] = useState<any>(null);
|
||||
|
|
@ -414,21 +697,25 @@ export default function MimecastSyncPage() {
|
|||
)}
|
||||
|
||||
<Tabs defaultValue="status" className="w-full">
|
||||
<TabsList className="grid w-full max-w-2xl grid-cols-5">
|
||||
<TabsTrigger value="status" className="gap-1.5"><Activity className="h-4 w-4" />Status</TabsTrigger>
|
||||
<TabsTrigger value="messages" className="gap-1.5"><Mail className="h-4 w-4" />Messages</TabsTrigger>
|
||||
<TabsTrigger value="threats" className="gap-1.5"><Shield className="h-4 w-4" />Threats</TabsTrigger>
|
||||
<TabsTrigger value="history" className="gap-1.5"><Clock className="h-4 w-4" />History</TabsTrigger>
|
||||
<TabsTrigger value="schedules" className="gap-1.5"><Calendar className="h-4 w-4" />Schedules</TabsTrigger>
|
||||
<TabsList className="grid w-full max-w-4xl grid-cols-7">
|
||||
<TabsTrigger value="status" className="gap-1.5"><Activity className="h-4 w-4" />Status</TabsTrigger>
|
||||
<TabsTrigger value="held" className="gap-1.5"><PauseCircle className="h-4 w-4" />Held Mail</TabsTrigger>
|
||||
<TabsTrigger value="messages" className="gap-1.5"><Mail className="h-4 w-4" />Messages</TabsTrigger>
|
||||
<TabsTrigger value="threats" className="gap-1.5"><Shield className="h-4 w-4" />Threats</TabsTrigger>
|
||||
<TabsTrigger value="cloudusers" className="gap-1.5"><Users className="h-4 w-4" />Cloud Users</TabsTrigger>
|
||||
<TabsTrigger value="history" className="gap-1.5"><Clock className="h-4 w-4" />History</TabsTrigger>
|
||||
<TabsTrigger value="schedules" className="gap-1.5"><Calendar className="h-4 w-4" />Schedules</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="status" className="mt-6">
|
||||
<TabsContent value="status" className="mt-6">
|
||||
<StatusTab data={statusData} onSync={handleSync} syncing={syncing} />
|
||||
</TabsContent>
|
||||
<TabsContent value="messages" className="mt-6"><MessagesTab /></TabsContent>
|
||||
<TabsContent value="threats" className="mt-6"><ThreatsTab /></TabsContent>
|
||||
<TabsContent value="history" className="mt-6"><HistoryTab /></TabsContent>
|
||||
<TabsContent value="schedules" className="mt-6"><SyncScheduler /></TabsContent>
|
||||
<TabsContent value="held" className="mt-6"><HeldMailTab /></TabsContent>
|
||||
<TabsContent value="messages" className="mt-6"><MessagesTab /></TabsContent>
|
||||
<TabsContent value="threats" className="mt-6"><ThreatsTab /></TabsContent>
|
||||
<TabsContent value="cloudusers" className="mt-6"><CloudUserTab /></TabsContent>
|
||||
<TabsContent value="history" className="mt-6"><HistoryTab /></TabsContent>
|
||||
<TabsContent value="schedules" className="mt-6"><SyncScheduler /></TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
22
app/api/mimecast/cloud-user/route.ts
Normal file
22
app/api/mimecast/cloud-user/route.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getMimecastClient } from '@/lib/services/mimecast-client';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const emailAddress = req.nextUrl.searchParams.get('emailAddress');
|
||||
const domain = req.nextUrl.searchParams.get('domain');
|
||||
|
||||
if (!emailAddress || !domain) {
|
||||
return NextResponse.json({ error: 'emailAddress and domain are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const client = getMimecastClient();
|
||||
const user = await client.getCloudUser(emailAddress, domain);
|
||||
if (!user) {
|
||||
return NextResponse.json({ found: false });
|
||||
}
|
||||
return NextResponse.json({ found: true, user });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
91
app/api/mimecast/held/route.ts
Normal file
91
app/api/mimecast/held/route.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { postgresClient } from '@/lib/services/postgres-client';
|
||||
import { getMimecastClientForTenant } from '@/lib/services/mimecast-client';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const tenantId = searchParams.get('tenantId');
|
||||
const recipient = searchParams.get('recipient') ?? undefined;
|
||||
|
||||
// Build query — filter by tenant if provided, else all enabled tenants
|
||||
let tenants: any[];
|
||||
if (tenantId) {
|
||||
const r = await postgresClient.query(
|
||||
`SELECT mt.*, c.company_name FROM mimecast_tenants mt
|
||||
LEFT JOIN companies c ON c.id = mt.company_id
|
||||
WHERE mt.id = $1 AND mt.enabled = true`,
|
||||
[tenantId]
|
||||
);
|
||||
tenants = r.rows;
|
||||
} else {
|
||||
const r = await postgresClient.query(
|
||||
`SELECT mt.*, c.company_name FROM mimecast_tenants mt
|
||||
LEFT JOIN companies c ON c.id = mt.company_id
|
||||
WHERE mt.enabled = true
|
||||
ORDER BY mt.account_name`
|
||||
);
|
||||
tenants = r.rows;
|
||||
}
|
||||
|
||||
if (!tenants.length) {
|
||||
return NextResponse.json({ error: 'No configured Mimecast tenants found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Fetch held messages for each tenant in parallel
|
||||
const results = await Promise.allSettled(
|
||||
tenants.map(async (tenant) => {
|
||||
const client = getMimecastClientForTenant(tenant);
|
||||
const { messages, totalCount } = await client.getHeldMessages({ recipient, maxMessages: 500 });
|
||||
return {
|
||||
tenantId: tenant.id,
|
||||
accountCode: tenant.account_code,
|
||||
accountName: tenant.account_name,
|
||||
companyName: tenant.company_name ?? tenant.account_name,
|
||||
companyId: tenant.company_id,
|
||||
messages,
|
||||
totalCount,
|
||||
error: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const tenantResults = results.map((r, i) => {
|
||||
if (r.status === 'fulfilled') return r.value;
|
||||
return {
|
||||
tenantId: tenants[i].id,
|
||||
accountCode: tenants[i].account_code,
|
||||
accountName: tenants[i].account_name,
|
||||
companyName: tenants[i].company_name ?? tenants[i].account_name,
|
||||
companyId: tenants[i].company_id,
|
||||
messages: [],
|
||||
totalCount: 0,
|
||||
error: r.reason?.message ?? 'Failed to fetch',
|
||||
};
|
||||
});
|
||||
|
||||
const allMessages = tenantResults.flatMap(t =>
|
||||
t.messages.map((m: any) => ({ ...m, tenantId: t.tenantId, accountName: t.accountName, companyName: t.companyName }))
|
||||
);
|
||||
|
||||
// Sort all messages newest first
|
||||
allMessages.sort((a, b) => new Date(b.dateReceived).getTime() - new Date(a.dateReceived).getTime());
|
||||
|
||||
return NextResponse.json({
|
||||
tenants: tenantResults.map(t => ({
|
||||
tenantId: t.tenantId,
|
||||
accountName: t.accountName,
|
||||
companyName: t.companyName,
|
||||
companyId: t.companyId,
|
||||
count: t.messages.length,
|
||||
totalCount: t.totalCount,
|
||||
error: t.error,
|
||||
})),
|
||||
messages: allMessages,
|
||||
totalCount: allMessages.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[Mimecast] held messages error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -6,18 +6,20 @@ export async function POST(request: NextRequest) {
|
|||
try {
|
||||
console.log('Starting classification icons sync...');
|
||||
|
||||
// Fetch classification icons from Autotask API
|
||||
// Fetch classification picklist from Companies field info
|
||||
const autotaskClient = getAutotaskClient();
|
||||
const classifications = await autotaskClient.getClassificationIcons();
|
||||
|
||||
if (!classifications || classifications.length === 0) {
|
||||
const fields = await autotaskClient.getFieldInfo('Companies');
|
||||
const classificationField = fields.find((f: any) => f.name === 'classification');
|
||||
|
||||
if (!classificationField || !classificationField.picklistValues?.length) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No classifications found in Autotask' },
|
||||
{ error: 'No classification picklist found in Companies field info' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Fetched ${classifications.length} classification icons from Autotask`);
|
||||
const classifications = classificationField.picklistValues;
|
||||
console.log(`Fetched ${classifications.length} classification values from Autotask`);
|
||||
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
|
|
@ -26,16 +28,16 @@ export async function POST(request: NextRequest) {
|
|||
for (const classification of classifications) {
|
||||
const result = await postgresClient.query(
|
||||
`INSERT INTO company_classifications (
|
||||
classification_id,
|
||||
name,
|
||||
description,
|
||||
classification_id,
|
||||
name,
|
||||
description,
|
||||
is_active,
|
||||
is_system,
|
||||
updated_at,
|
||||
synced_at
|
||||
) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (classification_id)
|
||||
DO UPDATE SET
|
||||
ON CONFLICT (classification_id)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
is_active = EXCLUDED.is_active,
|
||||
|
|
@ -44,9 +46,9 @@ export async function POST(request: NextRequest) {
|
|||
synced_at = CURRENT_TIMESTAMP
|
||||
RETURNING (xmax = 0) AS inserted`,
|
||||
[
|
||||
classification.id,
|
||||
classification.name,
|
||||
classification.description || null,
|
||||
parseInt(String(classification.value)),
|
||||
classification.label,
|
||||
null,
|
||||
classification.isActive !== false,
|
||||
classification.isSystem || false
|
||||
]
|
||||
|
|
@ -66,7 +68,7 @@ export async function POST(request: NextRequest) {
|
|||
total: classifications.length,
|
||||
inserted,
|
||||
updated,
|
||||
message: `Synced ${classifications.length} classification icons`
|
||||
message: `Synced ${classifications.length} classifications from Companies field info`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue