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>
102 lines
2.8 KiB
JavaScript
102 lines
2.8 KiB
JavaScript
/**
|
|
* Composable for customer notes/comments management.
|
|
*/
|
|
import { ref, computed } from 'vue'
|
|
import { listDocs, updateDoc } from 'src/api/erp'
|
|
import { authFetch } from 'src/api/auth'
|
|
import { BASE_URL } from 'src/config/erpnext'
|
|
|
|
/**
|
|
* @param {import('vue').Ref<Array>} comments - Reactive comments list
|
|
* @param {import('vue').Ref<Object>} customer - Reactive customer object
|
|
*/
|
|
export function useCustomerNotes (comments, customer) {
|
|
const newNote = ref('')
|
|
const addingNote = ref(false)
|
|
const noteInputFocused = ref(false)
|
|
const editingNote = ref(null)
|
|
|
|
const sortedComments = computed(() => {
|
|
const pinned = comments.value.filter(c => c._sticky)
|
|
const rest = comments.value.filter(c => !c._sticky)
|
|
return [...pinned, ...rest]
|
|
})
|
|
|
|
async function addNote () {
|
|
if (!newNote.value?.trim() || addingNote.value) return
|
|
addingNote.value = true
|
|
try {
|
|
const body = {
|
|
doctype: 'Comment', comment_type: 'Comment',
|
|
reference_doctype: 'Customer', reference_name: customer.value.name,
|
|
content: newNote.value.trim(),
|
|
}
|
|
const res = await authFetch(BASE_URL + '/api/resource/Comment', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
comments.value.unshift(data.data)
|
|
newNote.value = ''
|
|
noteInputFocused.value = false
|
|
}
|
|
} catch {} finally {
|
|
addingNote.value = false
|
|
}
|
|
}
|
|
|
|
async function deleteNote (note) {
|
|
try {
|
|
const res = await authFetch(BASE_URL + '/api/resource/Comment/' + encodeURIComponent(note.name), { method: 'DELETE' })
|
|
if (res.ok) {
|
|
comments.value = comments.value.filter(c => c.name !== note.name)
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
function toggleStickyNote (note) {
|
|
note._sticky = !note._sticky
|
|
}
|
|
|
|
async function saveEditNote (note, newContent) {
|
|
if (!newContent?.trim()) return
|
|
try {
|
|
await updateDoc('Comment', note.name, { content: newContent.trim() })
|
|
note.content = newContent.trim()
|
|
editingNote.value = null
|
|
} catch {}
|
|
}
|
|
|
|
function startEditNote (note) {
|
|
editingNote.value = note.name
|
|
note._editContent = note.content?.replace(/<[^>]*>/g, '') || ''
|
|
}
|
|
|
|
async function onNoteAdded () {
|
|
try {
|
|
comments.value = await listDocs('Comment', {
|
|
filters: { reference_doctype: 'Customer', reference_name: customer.value?.name, comment_type: 'Comment' },
|
|
fields: ['name', 'content', 'comment_by', 'creation'],
|
|
limit: 50,
|
|
orderBy: 'creation desc',
|
|
})
|
|
} catch {}
|
|
}
|
|
|
|
return {
|
|
newNote,
|
|
addingNote,
|
|
noteInputFocused,
|
|
editingNote,
|
|
sortedComments,
|
|
addNote,
|
|
deleteNote,
|
|
toggleStickyNote,
|
|
saveEditNote,
|
|
startEditNote,
|
|
onNoteAdded,
|
|
}
|
|
}
|