- Add admin dashboard with sync controls and data browser - Implement RMM, Auvik, and Addigy organization mappings - Add chunked ticket sync with progress tracking - Implement entity sync service with rate limiting - Add analytics engine and performance optimizer - Create data browser for all PSA entities - Add navigation components and UI improvements - Implement background processing and sync services - Add comprehensive documentation and migration scripts - Update configuration items with multi-system support - Enhance contact management and purchase history - Add issue type assignment and LLM analyzer - Improve error handling and logging utilities
323 lines
8.4 KiB
TypeScript
323 lines
8.4 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { Pool } from 'pg';
|
|
import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
|
|
import { DattoRMMSite } from '@/lib/types/datto-rmm';
|
|
|
|
const pool = new Pool({
|
|
host: process.env.POSTGRES_HOST,
|
|
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
|
database: process.env.POSTGRES_DB,
|
|
user: process.env.POSTGRES_USER,
|
|
password: process.env.POSTGRES_PASSWORD,
|
|
});
|
|
|
|
interface RMMSiteMapping {
|
|
id: number;
|
|
company_id: number;
|
|
company_name?: string;
|
|
rmm_site_uid: string;
|
|
rmm_site_name: string;
|
|
is_primary: boolean;
|
|
device_count: number;
|
|
notes: string | null;
|
|
last_sync_at: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
created_by: string | null;
|
|
}
|
|
|
|
// GET /api/rmm/site-mappings
|
|
// Get all RMM site mappings, optionally including unmapped sites
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const searchParams = request.nextUrl.searchParams;
|
|
const includeUnmapped = searchParams.get('includeUnmapped') === 'true';
|
|
const companyId = searchParams.get('companyId');
|
|
|
|
// Get all existing mappings
|
|
let query = `
|
|
SELECT
|
|
rsm.id,
|
|
rsm.company_id,
|
|
rsm.rmm_site_uid,
|
|
rsm.rmm_site_name,
|
|
rsm.is_primary,
|
|
rsm.device_count,
|
|
rsm.notes,
|
|
rsm.last_sync_at,
|
|
rsm.created_at,
|
|
rsm.updated_at,
|
|
rsm.created_by,
|
|
c.company_name
|
|
FROM rmm_site_mappings rsm
|
|
JOIN companies c ON c.id = rsm.company_id
|
|
`;
|
|
|
|
const queryParams: any[] = [];
|
|
|
|
if (companyId) {
|
|
query += ' WHERE rsm.company_id = $1';
|
|
queryParams.push(companyId);
|
|
}
|
|
|
|
query += ' ORDER BY c.company_name, rsm.rmm_site_name';
|
|
|
|
const mappingsResult = await pool.query(query, queryParams);
|
|
const mappings = mappingsResult.rows;
|
|
|
|
if (!includeUnmapped) {
|
|
return NextResponse.json({ mappings });
|
|
}
|
|
|
|
// Get all RMM sites from the RMM API
|
|
const rmmClient = getDattoRMMClient();
|
|
const allSites = await rmmClient.getSites();
|
|
|
|
// Get list of already mapped site UIDs
|
|
const mappedSiteUids = new Set(mappings.map((m: any) => m.rmm_site_uid));
|
|
|
|
// Create mapping entries for unmapped sites
|
|
const unmappedSites = allSites
|
|
.filter((site: DattoRMMSite) => !mappedSiteUids.has(site.uid))
|
|
.map((site: DattoRMMSite) => ({
|
|
id: null,
|
|
company_id: null,
|
|
company_name: null,
|
|
rmm_site_uid: site.uid,
|
|
rmm_site_name: site.name,
|
|
is_primary: false,
|
|
device_count: 0,
|
|
notes: null,
|
|
last_sync_at: null,
|
|
created_at: null,
|
|
updated_at: null,
|
|
created_by: null,
|
|
}));
|
|
|
|
// Combine mapped and unmapped sites
|
|
const allMappings = [...mappings, ...unmappedSites];
|
|
|
|
// Sort by mapping status (mapped first), then by site name
|
|
allMappings.sort((a, b) => {
|
|
if (a.company_id && !b.company_id) return -1;
|
|
if (!a.company_id && b.company_id) return 1;
|
|
return (a.rmm_site_name || '').localeCompare(b.rmm_site_name || '');
|
|
});
|
|
|
|
return NextResponse.json({
|
|
mappings: allMappings,
|
|
stats: {
|
|
total: allMappings.length,
|
|
mapped: mappings.length,
|
|
unmapped: unmappedSites.length
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching RMM site mappings:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to fetch RMM site mappings' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// POST /api/rmm/site-mappings
|
|
// Create or update an RMM site mapping
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
const {
|
|
rmmSiteUid,
|
|
rmmSiteName,
|
|
companyId,
|
|
companyName,
|
|
isPrimary = false,
|
|
notes = null,
|
|
createdBy = 'system'
|
|
} = body;
|
|
|
|
if (!rmmSiteUid || !rmmSiteName || !companyId) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing required fields: rmmSiteUid, rmmSiteName, and companyId are required' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// If setting as primary, unset other primary sites for this company
|
|
if (isPrimary) {
|
|
await pool.query(
|
|
'UPDATE rmm_site_mappings SET is_primary = false WHERE company_id = $1',
|
|
[companyId]
|
|
);
|
|
}
|
|
|
|
// Insert or update the mapping
|
|
const query = `
|
|
INSERT INTO rmm_site_mappings (
|
|
company_id,
|
|
rmm_site_uid,
|
|
rmm_site_name,
|
|
is_primary,
|
|
notes,
|
|
created_by
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (company_id, rmm_site_uid)
|
|
DO UPDATE SET
|
|
rmm_site_name = EXCLUDED.rmm_site_name,
|
|
is_primary = EXCLUDED.is_primary,
|
|
notes = EXCLUDED.notes,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await pool.query(query, [
|
|
companyId,
|
|
rmmSiteUid,
|
|
rmmSiteName,
|
|
isPrimary,
|
|
notes,
|
|
createdBy
|
|
]);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
mapping: result.rows[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error saving RMM site mapping:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to save RMM site mapping' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// DELETE /api/rmm/site-mappings
|
|
// Delete an RMM site mapping
|
|
export async function DELETE(request: NextRequest) {
|
|
try {
|
|
const searchParams = request.nextUrl.searchParams;
|
|
const id = searchParams.get('id');
|
|
|
|
if (!id) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing required parameter: id' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const result = await pool.query(
|
|
'DELETE FROM rmm_site_mappings WHERE id = $1 RETURNING *',
|
|
[id]
|
|
);
|
|
|
|
if (result.rowCount === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'Mapping not found' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
deleted: result.rows[0]
|
|
});
|
|
} catch (error) {
|
|
console.error('Error deleting RMM site mapping:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to delete RMM site mapping' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// PUT /api/rmm/site-mappings/bulk
|
|
// Create multiple mappings at once
|
|
export async function PUT(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
const { mappings, createdBy = 'system' } = body;
|
|
|
|
if (!mappings || !Array.isArray(mappings)) {
|
|
return NextResponse.json(
|
|
{ error: 'Missing required field: mappings (array)' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const results = [];
|
|
for (const mapping of mappings) {
|
|
const {
|
|
rmmSiteUid,
|
|
rmmSiteName,
|
|
companyId,
|
|
isPrimary = false,
|
|
notes = null
|
|
} = mapping;
|
|
|
|
if (!rmmSiteUid || !rmmSiteName || !companyId) {
|
|
await client.query('ROLLBACK');
|
|
return NextResponse.json(
|
|
{ error: 'Each mapping must have rmmSiteUid, rmmSiteName, and companyId' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// If setting as primary, unset other primary sites for this company
|
|
if (isPrimary) {
|
|
await client.query(
|
|
'UPDATE rmm_site_mappings SET is_primary = false WHERE company_id = $1',
|
|
[companyId]
|
|
);
|
|
}
|
|
|
|
const result = await client.query(
|
|
`
|
|
INSERT INTO rmm_site_mappings (
|
|
company_id,
|
|
rmm_site_uid,
|
|
rmm_site_name,
|
|
is_primary,
|
|
notes,
|
|
created_by
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (company_id, rmm_site_uid)
|
|
DO UPDATE SET
|
|
rmm_site_name = EXCLUDED.rmm_site_name,
|
|
is_primary = EXCLUDED.is_primary,
|
|
notes = EXCLUDED.notes,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
RETURNING *
|
|
`,
|
|
[companyId, rmmSiteUid, rmmSiteName, isPrimary, notes, createdBy]
|
|
);
|
|
|
|
results.push(result.rows[0]);
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
mappings: results
|
|
});
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving bulk RMM site mappings:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to save bulk RMM site mappings' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|