/* * Field Service Dispatch - customers, assets, work orders, tasks, and parts. * * fs_customer_site__c <- customer location and service tier * fs_asset__c <- equipment installed at a customer site * fs_work_order__c <- dispatch job for a site and optional asset * fs_task__c <- checklist/labor line on a work order * fs_part_usage__c <- parts consumed by a task * * Run: * yarn tsn examples/manifest/field-service-dispatch/field-service-dispatch.ts \ * > examples/manifest/field-service-dispatch/manifest.json */ import { Category, CustomObject, CustomObjectFieldSection, CustomObjectPageLayout, DateField, DatetimeField, FormulaField, ListViewDef, LongTextField, LookupField, ManifestBuilder, NumberField, ParentChildField, Rule, SelectField, SummaryField, TextField, Validation, } from '@rippling/rippling-sdk/lib/manifest'; const manifest = new ManifestBuilder({ key: 'field_service_dispatch', name: 'Field Service Dispatch', description: 'Dispatch work orders across customer sites, installed assets, tasks, and consumed parts', }); const category = new Category(manifest, { apiName: 'field_service__c', name: 'Field service', description: 'Service operations and dispatch records', }); const siteObj = new CustomObject(manifest, { apiName: 'fs_customer_site__c', name: 'Customer site', pluralLabel: 'Customer sites', category, description: 'Customer location with service tier and access notes', }); const siteSection = new CustomObjectFieldSection(siteObj, { name: 'Site', sectionId: 'sec_fs_site', }); const siteAccountField = new TextField(siteObj, { apiName: 'account_name__c', displayName: 'Account name', description: 'Customer account or property group', required: true, maxLength: 160, section: siteSection, }); const siteAddressField = new LongTextField(siteObj, { apiName: 'service_address__c', displayName: 'Service address', description: 'Address, gate code, and arrival instructions', maxLength: 3000, section: siteSection, }); const siteTierField = new SelectField(siteObj, { apiName: 'service_tier__c', displayName: 'Service tier', description: 'SLA tier for dispatch prioritization', options: ['Standard', 'Premium', 'Critical 24x7'], required: true, section: siteSection, }); const assetObj = new CustomObject(manifest, { apiName: 'fs_asset__c', name: 'Asset', pluralLabel: 'Assets', category, description: 'Installed equipment maintained at a customer site', }); const assetSection = new CustomObjectFieldSection(assetObj, { name: 'Asset', sectionId: 'sec_fs_asset', }); const assetSiteRef = new ParentChildField(assetObj, { apiName: 'customer_site_ref__c', displayName: 'Customer site', target: siteObj, relatedDataLabel: 'Assets', parentChildType: 'PRIMARY', enableParentOwnerInheritance: false, description: 'Site where this asset is installed', section: assetSection, }); const assetSerialField = new TextField(assetObj, { apiName: 'serial_number__c', displayName: 'Serial number', description: 'Manufacturer serial number', maxLength: 80, section: assetSection, }); const assetTypeField = new SelectField(assetObj, { apiName: 'asset_type__c', displayName: 'Asset type', description: 'Equipment category', options: ['HVAC', 'Generator', 'Security', 'Network', 'Kitchen equipment', 'Other'], required: true, section: assetSection, }); const assetWarrantyEndField = new DateField(assetObj, { apiName: 'warranty_end__c', displayName: 'Warranty end', description: 'Warranty expiration date', section: assetSection, }); const workOrderObj = new CustomObject(manifest, { apiName: 'fs_work_order__c', name: 'Work order', pluralLabel: 'Work orders', category, description: 'Dispatch job with schedule, priority, and status', }); const workOrderSection = new CustomObjectFieldSection(workOrderObj, { name: 'Work order', sectionId: 'sec_fs_work_order', }); const workOrderSiteRef = new ParentChildField(workOrderObj, { apiName: 'customer_site_ref__c', displayName: 'Customer site', target: siteObj, relatedDataLabel: 'Work orders', parentChildType: 'PRIMARY', enableParentOwnerInheritance: false, description: 'Site receiving service', section: workOrderSection, }); const workOrderAssetRef = new LookupField(workOrderObj, { apiName: 'asset_ref__c', displayName: 'Asset', target: assetObj, relatedDataLabel: 'Work orders', description: 'Optional installed asset being serviced', section: workOrderSection, }); const workOrderPriorityField = new SelectField(workOrderObj, { apiName: 'priority__c', displayName: 'Priority', description: 'Dispatch priority', options: ['Low', 'Normal', 'High', 'Emergency'], required: true, section: workOrderSection, }); const workOrderStatusField = new SelectField(workOrderObj, { apiName: 'status__c', displayName: 'Status', description: 'Current dispatch status', options: ['New', 'Scheduled', 'In progress', 'Blocked', 'Complete', 'Cancelled'], required: true, section: workOrderSection, }); const workOrderScheduledStartField = new DatetimeField(workOrderObj, { apiName: 'scheduled_start__c', displayName: 'Scheduled start', description: 'Planned arrival window start', section: workOrderSection, }); const workOrderScheduledEndField = new DatetimeField(workOrderObj, { apiName: 'scheduled_end__c', displayName: 'Scheduled end', description: 'Planned arrival window end', section: workOrderSection, }); const workOrderSummaryField = new LongTextField(workOrderObj, { apiName: 'problem_summary__c', displayName: 'Problem summary', description: 'Customer-reported issue and triage notes', maxLength: 6000, section: workOrderSection, }); const taskObj = new CustomObject(manifest, { apiName: 'fs_task__c', name: 'Task', pluralLabel: 'Tasks', category, description: 'Work order task with labor estimate and completion state', }); const taskSection = new CustomObjectFieldSection(taskObj, { name: 'Task', sectionId: 'sec_fs_task', }); const taskWorkOrderRef = new ParentChildField(taskObj, { apiName: 'work_order_ref__c', displayName: 'Work order', target: workOrderObj, relatedDataLabel: 'Tasks', parentChildType: 'PRIMARY', enableParentOwnerInheritance: true, description: 'Work order this task belongs to', section: taskSection, }); const taskTypeField = new SelectField(taskObj, { apiName: 'task_type__c', displayName: 'Task type', description: 'Type of work required', options: ['Inspect', 'Repair', 'Replace', 'Calibrate', 'Clean', 'Customer walkthrough'], required: true, section: taskSection, }); const taskStatusField = new SelectField(taskObj, { apiName: 'status__c', displayName: 'Status', description: 'Task status', options: ['Open', 'In progress', 'Blocked', 'Done'], required: true, section: taskSection, }); const taskEstimatedHoursField = new NumberField(taskObj, { apiName: 'estimated_hours__c', displayName: 'Estimated hours', description: 'Labor estimate for this task', decimalPlaces: 2, section: taskSection, }); const taskCompletedAtField = new DatetimeField(taskObj, { apiName: 'completed_at__c', displayName: 'Completed at', description: 'Completion timestamp', section: taskSection, }); const partUsageObj = new CustomObject(manifest, { apiName: 'fs_part_usage__c', name: 'Part usage', pluralLabel: 'Part usage', category, description: 'Part consumed while completing a task', }); const partUsageSection = new CustomObjectFieldSection(partUsageObj, { name: 'Part usage', sectionId: 'sec_fs_part_usage', }); const partTaskRef = new ParentChildField(partUsageObj, { apiName: 'task_ref__c', displayName: 'Task', target: taskObj, relatedDataLabel: 'Parts used', parentChildType: 'PRIMARY', enableParentOwnerInheritance: true, description: 'Task that consumed this part', section: partUsageSection, }); const partSkuField = new TextField(partUsageObj, { apiName: 'sku__c', displayName: 'SKU', description: 'Inventory SKU or part number', maxLength: 80, required: true, section: partUsageSection, }); const partQuantityField = new NumberField(partUsageObj, { apiName: 'quantity__c', displayName: 'Quantity', description: 'Quantity consumed', decimalPlaces: 2, required: true, section: partUsageSection, }); const partCostCentsField = new NumberField(partUsageObj, { apiName: 'unit_cost_cents__c', displayName: 'Unit cost (cents)', description: 'Unit material cost in cents', decimalPlaces: 0, section: partUsageSection, }); const assetCountOnSite = new SummaryField(siteObj, { apiName: 'asset_count__c', displayName: 'Assets', aggregates: assetSerialField, using: 'COUNT', lookup: assetSiteRef, description: 'Installed asset count', section: siteSection, }); const workOrderCountOnSite = new SummaryField(siteObj, { apiName: 'work_order_count__c', displayName: 'Work orders', aggregates: workOrderStatusField, using: 'COUNT', lookup: workOrderSiteRef, description: 'Work orders associated with this site', section: siteSection, }); const taskCountOnWorkOrder = new SummaryField(workOrderObj, { apiName: 'task_count__c', displayName: 'Tasks', aggregates: taskTypeField, using: 'COUNT', lookup: taskWorkOrderRef, description: 'Task count on this work order', section: workOrderSection, }); const estimatedHoursOnWorkOrder = new SummaryField(workOrderObj, { apiName: 'estimated_hours_total__c', displayName: 'Estimated hours total', aggregates: taskEstimatedHoursField, using: 'SUM', lookup: taskWorkOrderRef, description: 'Total estimated labor hours', section: workOrderSection, }); const partsCountOnTask = new SummaryField(taskObj, { apiName: 'parts_count__c', displayName: 'Parts lines', aggregates: partSkuField, using: 'COUNT', lookup: partTaskRef, description: 'Number of part usage lines', section: taskSection, }); const workOrderLabelField = new FormulaField(workOrderObj, { apiName: 'dispatch_label__c', displayName: 'Dispatch label', description: 'Compact board label', fieldType: 'TEXT', maxLength: 240, formula: "CONCATENATE(priority__c, ' - ', status__c)", section: workOrderSection, }); new Validation(workOrderObj, { ruleId: 'fs_work_order_window_valid', displayName: 'Scheduled end must be after start', rqlFormula: '(scheduled_end__c <= scheduled_start__c)', errorMessage: 'Scheduled end must be after scheduled start.', ruleDescription: 'Prevents inverted dispatch windows', state: 'ACTIVE', level: 'FIELD', fieldRef: workOrderScheduledEndField, }); new Validation(partUsageObj, { ruleId: 'fs_part_quantity_positive', displayName: 'Quantity must be positive', rqlFormula: '(quantity__c <= 0)', errorMessage: 'Quantity must be greater than zero.', ruleDescription: 'Blocks zero or negative parts usage', state: 'ACTIVE', level: 'FIELD', fieldRef: partQuantityField, }); new Rule(workOrderObj, { flowId: 'fs_default_work_order_status', flowName: 'Default work order status', triggerType: 'CREATE', description: 'New work orders start in New status', actions: [{ targetField: workOrderStatusField, fixedValue: ['New'] }], }); new Rule(taskObj, { flowId: 'fs_default_task_status', flowName: 'Default task status', triggerType: 'CREATE', description: 'New tasks start Open', actions: [{ targetField: taskStatusField, fixedValue: ['Open'] }], }); new ListViewDef(siteObj, { viewId: 'view_fs_customer_sites', name: 'Customer sites', description: 'Sites with service tier, assets, and work order volume', fields: ['name', siteAccountField, siteTierField, assetCountOnSite, workOrderCountOnSite], orderBy: 'name', sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(assetObj, { viewId: 'view_fs_assets', name: 'Assets', description: 'Installed equipment by site', fields: ['name', assetSiteRef, assetTypeField, assetSerialField, assetWarrantyEndField], orderBy: 'name', sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(workOrderObj, { viewId: 'view_fs_work_orders', name: 'Work orders', description: 'Dispatch board with schedule and rollups', fields: [ 'name', workOrderSiteRef, workOrderAssetRef, workOrderPriorityField, workOrderStatusField, workOrderScheduledStartField, taskCountOnWorkOrder, estimatedHoursOnWorkOrder, workOrderLabelField, ], orderBy: workOrderScheduledStartField, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(taskObj, { viewId: 'view_fs_tasks', name: 'Tasks', description: 'Task checklist with estimates and parts lines', fields: [ 'name', taskWorkOrderRef, taskTypeField, taskStatusField, taskEstimatedHoursField, partsCountOnTask, ], orderBy: taskWorkOrderRef, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); new ListViewDef(partUsageObj, { viewId: 'view_fs_part_usage', name: 'Part usage', description: 'Parts consumed by service tasks', fields: ['name', partTaskRef, partSkuField, partQuantityField, partCostCentsField], orderBy: partTaskRef, sortOrder: 'ASC', isPublic: true, objectScope: 'all', }); CustomObjectPageLayout.basic(siteObj, { section: siteSection, fields: ['name', siteAccountField, siteAddressField, siteTierField, assetCountOnSite, workOrderCountOnSite], }); CustomObjectPageLayout.basic(assetObj, { section: assetSection, fields: ['name', assetSiteRef, assetSerialField, assetTypeField, assetWarrantyEndField], }); CustomObjectPageLayout.basic(workOrderObj, { section: workOrderSection, fields: [ 'name', workOrderSiteRef, workOrderAssetRef, workOrderPriorityField, workOrderStatusField, workOrderScheduledStartField, workOrderScheduledEndField, workOrderSummaryField, taskCountOnWorkOrder, estimatedHoursOnWorkOrder, workOrderLabelField, ], }); CustomObjectPageLayout.basic(taskObj, { section: taskSection, fields: [ 'name', taskWorkOrderRef, taskTypeField, taskStatusField, taskEstimatedHoursField, taskCompletedAtField, partsCountOnTask, ], }); CustomObjectPageLayout.basic(partUsageObj, { section: partUsageSection, fields: ['name', partTaskRef, partSkuField, partQuantityField, partCostCentsField], }); console.log(manifest.preview());