Major dispatch/task system overhaul: - Project templates with 3-step wizard (choose template → edit steps → publish) - 4 built-in templates: phone service, fiber install, move, repair - Nested task tree with recursive TaskNode component (parent_job hierarchy) - n8n webhook integration (on_open_webhook, on_close_webhook per task) - Inline task editing: status, priority, type, tech assignment, tags, delete - Tech assignment + tags from ticket modal → jobs appear on dispatch timeline - ERPNext custom fields: parent_job, on_open_webhook, on_close_webhook, step_order - Refactored ClientDetailPage, ChatterPanel, DetailModal, dispatch store - CSS consolidation, dead code cleanup, composable extraction - Dashboard KPIs with dispatch integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.4 KiB
JavaScript
132 lines
4.4 KiB
JavaScript
import { ref, computed } from 'vue'
|
|
import { getDoc, listDocs } from 'src/api/erp'
|
|
import { erpLink } from 'src/composables/useFormatters'
|
|
|
|
/**
|
|
* Composable for the slide-over detail modal.
|
|
* Handles state management, data fetching, and navigation for any ERPNext doctype.
|
|
*/
|
|
export function useDetailModal () {
|
|
const modalOpen = ref(false)
|
|
const modalLoading = ref(false)
|
|
const modalDoctype = ref('')
|
|
const modalDocName = ref('')
|
|
const modalTitle = ref('')
|
|
const modalDoc = ref(null)
|
|
const modalComments = ref([])
|
|
const modalComms = ref([])
|
|
const modalFiles = ref([])
|
|
const modalDispatchJobs = ref([])
|
|
|
|
const modalErpLink = computed(() => erpLink(modalDoctype.value, modalDocName.value))
|
|
|
|
const modalDocFields = computed(() => {
|
|
if (!modalDoc.value) return {}
|
|
const skip = new Set(['name', 'owner', 'creation', 'modified', 'modified_by', 'docstatus', 'idx', 'doctype', '_user_tags', '_comments', '_assign', '_liked_by'])
|
|
const out = {}
|
|
for (const [k, v] of Object.entries(modalDoc.value)) {
|
|
if (skip.has(k) || Array.isArray(v) || v === null || v === '' || typeof v === 'object') continue
|
|
out[k] = v
|
|
}
|
|
return out
|
|
})
|
|
|
|
/**
|
|
* Open the modal for any doctype.
|
|
* Fetches the document and optional related data (comments, communications, files).
|
|
*/
|
|
async function openModal (doctype, name, title) {
|
|
modalDoctype.value = doctype
|
|
modalDocName.value = name
|
|
modalTitle.value = title || name
|
|
modalDoc.value = null
|
|
modalComments.value = []
|
|
modalComms.value = []
|
|
modalFiles.value = []
|
|
modalDispatchJobs.value = []
|
|
modalOpen.value = true
|
|
modalLoading.value = true
|
|
|
|
try {
|
|
const promises = [getDoc(doctype, name)]
|
|
|
|
// Fetch comments for invoices (legacy notes imported as Comments)
|
|
if (doctype === 'Sales Invoice') {
|
|
promises.push(listDocs('Comment', {
|
|
filters: { reference_doctype: 'Sales Invoice', reference_name: name, comment_type: 'Comment' },
|
|
fields: ['name', 'content', 'comment_by', 'creation'],
|
|
limit: 50, orderBy: 'creation desc',
|
|
}))
|
|
}
|
|
|
|
// Fetch communications, files, comments, and linked dispatch jobs for Issues
|
|
if (doctype === 'Issue') {
|
|
promises.push(
|
|
listDocs('Communication', {
|
|
filters: { reference_doctype: 'Issue', reference_name: name },
|
|
fields: ['name', 'sender', 'owner', 'content', 'creation', 'communication_type', 'subject'],
|
|
limit: 50, orderBy: 'creation asc',
|
|
}).catch(() => []),
|
|
listDocs('File', {
|
|
filters: { attached_to_doctype: 'Issue', attached_to_name: name },
|
|
fields: ['name', 'file_name', 'file_url', 'file_size'],
|
|
limit: 20,
|
|
}).catch(() => []),
|
|
listDocs('Comment', {
|
|
filters: { reference_doctype: 'Issue', reference_name: name, comment_type: 'Comment' },
|
|
fields: ['name', 'content', 'comment_by', 'creation'],
|
|
limit: 200, orderBy: 'creation asc',
|
|
}).catch(() => []),
|
|
listDocs('Dispatch Job', {
|
|
filters: { source_issue: name },
|
|
fields: ['name', 'subject', 'status', 'assigned_tech', 'scheduled_date', 'job_type', 'priority', 'depends_on', 'duration_h', 'parent_job', 'step_order', 'on_open_webhook', 'on_close_webhook'],
|
|
limit: 50, orderBy: 'step_order asc, creation asc',
|
|
}).catch(() => []),
|
|
)
|
|
}
|
|
|
|
const results = await Promise.all(promises)
|
|
modalDoc.value = results[0]
|
|
|
|
if (doctype === 'Sales Invoice') {
|
|
modalComments.value = results[1] || []
|
|
} else if (doctype === 'Issue') {
|
|
modalComms.value = results[1] || []
|
|
modalFiles.value = results[2] || []
|
|
modalComments.value = results[3] || []
|
|
modalDispatchJobs.value = results[4] || []
|
|
}
|
|
|
|
// Auto-derive title from doc if not provided
|
|
if (!title) {
|
|
const d = modalDoc.value
|
|
modalTitle.value = d.title || d.subject || d.item_name || d.customer_name || name
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load', doctype, name, e)
|
|
}
|
|
modalLoading.value = false
|
|
}
|
|
|
|
function closeModal () {
|
|
modalOpen.value = false
|
|
}
|
|
|
|
return {
|
|
modalOpen,
|
|
modalLoading,
|
|
modalDoctype,
|
|
modalDocName,
|
|
modalTitle,
|
|
modalDoc,
|
|
modalComments,
|
|
modalComms,
|
|
modalFiles,
|
|
modalErpLink,
|
|
modalDocFields,
|
|
modalDispatchJobs,
|
|
openModal,
|
|
closeModal,
|
|
}
|
|
}
|