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:
parent
e8462ef301
commit
6eee14f8af
171 changed files with 32671 additions and 621 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue