Add comprehensive admin features and multi-system integration

- 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
This commit is contained in:
root 2025-11-19 14:18:16 -05:00
parent e8462ef301
commit 6eee14f8af
171 changed files with 32671 additions and 621 deletions

View file

@ -180,6 +180,136 @@ async getAllCompanies() {
}
```
#### Configuration Items
Configuration Items represent physical or virtual assets (computers, servers, network devices, etc.) associated with companies.
```javascript
// Get configuration items for a company
async getConfigurationItemsByCompany(companyId) {
const query = {
filter: [
{ op: 'eq', field: 'companyID', value: companyId },
{ op: 'eq', field: 'isActive', value: true } // Only active items
]
};
return this.queryEntity('ConfigurationItems', query);
}
// Get a single configuration item by ID
async getConfigurationItemById(id) {
return this.getEntityById('ConfigurationItems', id);
}
// Update a configuration item
async updateConfigurationItem(id, updates) {
// IMPORTANT: Must fetch current item first to get required fields
const currentItem = await this.getConfigurationItemById(id);
// Autotask requires certain fields even for updates
const updateData = {
id: id,
companyID: currentItem.companyID, // Required
productID: currentItem.productID, // Required
referenceTitle: updates.referenceTitle ?? currentItem.referenceTitle,
isActive: updates.isActive !== undefined ? updates.isActive : currentItem.isActive,
// Optional fields
serialNumber: updates.serialNumber ?? currentItem.serialNumber,
referenceNumber: updates.referenceNumber ?? currentItem.referenceNumber,
location: updates.location ?? currentItem.location,
notes: updates.notes ?? currentItem.notes
};
const url = `${this.baseUrl}/ConfigurationItems`;
const response = await this.makeApiCall(url, {
method: 'PUT',
headers: this.getAuthHeaders(),
body: JSON.stringify(updateData)
});
// IMPORTANT: Autotask returns { itemId: X } on successful update, not { item: {...} }
// You must fetch the updated item separately
if (response.itemId) {
return this.getConfigurationItemById(response.itemId);
}
throw new Error('Failed to update configuration item - no itemId in response');
}
```
**Configuration Item Key Fields:**
- `id` - Unique identifier
- `companyID` - Associated company (required)
- `productID` - Product/asset type (required, even for updates)
- `referenceTitle` - Display name
- `serialNumber` - Device serial number
- `isActive` - Active status (true/false)
- `contactID` - Primary contact for the device
- `rmmDeviceUID` - RMM system unique identifier
- `rmmDeviceAuditIPAddress` - IP address from RMM
- `dattoSerialNumber` - Datto RMM serial number
- `dattoInternalIP` / `dattoRemoteIP` - Datto IP addresses
#### Contacts
Contacts can be associated with configuration items to track device ownership/responsibility.
```javascript
// Get contacts for a company
async getContactsByCompany(companyId) {
const query = {
filter: [
{ op: 'eq', field: 'companyID', value: companyId },
{ op: 'eq', field: 'isActive', value: 1 }
]
};
return this.queryEntity('Contacts', query);
}
// Batch fetch contacts to avoid rate limits
async getContactsBatch(contactIds) {
const contacts = {};
// Fetch in parallel but respect rate limits
const promises = contactIds.map(async (id) => {
try {
const contact = await this.getEntityById('Contacts', id);
if (contact) {
contacts[id] = contact;
}
} catch (error) {
console.error(`Failed to fetch contact ${id}:`, error.message);
// Continue with other contacts
}
});
await Promise.all(promises);
return contacts;
}
```
#### Billing Items
Billing items represent products/services that can be billed to customers.
```javascript
// Get billing items (products) for a company
async getBillingItemsByCompany(companyId) {
const query = {
filter: [
{ op: 'eq', field: 'companyID', value: companyId }
]
};
return this.queryEntity('BillingItems', query);
}
// Get all products (for product catalog)
async getAllProducts() {
return this.queryEntity('Products', {});
}
```
### 4. Working with Picklists
Picklists provide dropdown values for fields like status, priority, etc.
@ -457,6 +587,113 @@ If your API password contains `$`, escape it in `.env` files (but NOT in docker-
- Always check for null before accessing nested properties
- Use optional chaining: `ticket?.companyID`
### 8. Configuration Item Updates - CRITICAL
**The update response format is different from other entities:**
```javascript
// ❌ WRONG - This will fail
const response = await updateConfigurationItem(id, data);
return response.item; // item doesn't exist!
// ✅ CORRECT - Autotask returns itemId, not item
const response = await updateConfigurationItem(id, data);
if (response.itemId) {
// Must fetch the updated item separately
return await getConfigurationItemById(response.itemId);
}
```
**Key points:**
- PUT requests to ConfigurationItems return `{ itemId: 12345 }` not `{ item: {...} }`
- You MUST fetch the updated item with a separate GET request
- Always include `productID` and `companyID` even for updates (required fields)
- Fetch the current item first to preserve required fields you're not updating
### 9. Rate Limiting with Batch Operations
When fetching multiple related entities (like contacts for devices):
```javascript
// ❌ BAD - Sequential requests, very slow
for (const contactId of contactIds) {
const contact = await getContact(contactId);
}
// ✅ BETTER - Parallel requests, but can hit rate limits
await Promise.all(contactIds.map(id => getContact(id)));
// ✅ BEST - Batch fetch on server side, cache results
// Fetch all contacts for a company once, then lookup by ID
const allContacts = await getContactsByCompany(companyId);
const contactMap = {};
allContacts.forEach(c => contactMap[c.id] = c);
```
**Strategy for avoiding rate limits:**
1. Fetch related data in bulk when possible (e.g., all contacts for a company)
2. Cache frequently accessed data (companies, products, picklists)
3. Use batch endpoints when available
4. Implement request throttling/queuing for parallel operations
### 10. Configuration Item Filtering by Status
When querying configuration items, the `isActive` field behaves differently:
```javascript
// Get only active items
const query = {
filter: [
{ op: 'eq', field: 'companyID', value: companyId },
{ op: 'eq', field: 'isActive', value: true } // Boolean true
]
};
// Get only inactive items
const query = {
filter: [
{ op: 'eq', field: 'companyID', value: companyId },
{ op: 'eq', field: 'isActive', value: false } // Boolean false
]
};
// Get all items (active and inactive)
const query = {
filter: [
{ op: 'eq', field: 'companyID', value: companyId }
// Don't filter by isActive
]
};
```
### 11. RMM Integration Fields
Configuration Items have special fields for RMM system integration:
- `rmmDeviceUID` - Unique identifier from RMM system (use for matching)
- `rmmDeviceAuditIPAddress` - IP address reported by RMM
- `rmmDeviceAuditHostname` - Hostname from RMM
- `dattoSerialNumber` - Datto-specific serial number
- `dattoInternalIP` / `dattoRemoteIP` - Datto-specific IP addresses
**Best practice for matching RMM devices to Autotask:**
1. First try matching by `rmmDeviceUID`
2. Fall back to `serialNumber` or `dattoSerialNumber`
3. Last resort: match by IP address (less reliable)
### 12. Contact Association
Contacts can be linked to configuration items via `contactID`:
```javascript
// Update configuration item with contact
await updateConfigurationItem(itemId, {
contactID: contactId // Links device to a specific contact
});
// Remove contact association
await updateConfigurationItem(itemId, {
contactID: null // Removes contact link
});
```
**Note:** Contact must belong to the same company as the configuration item.
## Testing Strategy
1. **Start with read-only operations** (GET requests)