gigafibre-fsm/apps/ops/src/composables/useDetailModal.js
louispaulb 7d7b4fdb06 feat: nested tasks, project wizard, n8n webhooks, inline task editing
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>
2026-04-01 13:01:20 -04:00

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,
}
}