gigafibre-fsm/apps/ops/src/composables/useCustomerNotes.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

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