@@ -333,11 +58,22 @@
diff --git a/apps/ops/src/components/shared/ProjectWizard.vue b/apps/ops/src/components/shared/ProjectWizard.vue
new file mode 100644
index 0000000..f465ad9
--- /dev/null
+++ b/apps/ops/src/components/shared/ProjectWizard.vue
@@ -0,0 +1,424 @@
+
+
+
+
+
+
+
+
+ Projet — {{ stepLabels[currentStep] }}
+
+
{{ issue?.name }} · {{ issue?.subject }}
+
+
+
+
+
+
+
+
{{ i < currentStep ? '✓' : i + 1 }}
+
{{ label }}
+
+
+
+
+
+ Choisissez un modèle de projet ou créez un projet vide
+
+
+
+
{{ tpl.name }}
+
{{ tpl.description }}
+
+
+
+
+
Projet vide
+
Créer les étapes manuellement
+
+
+
+
+
+
+ Modifiez les étapes, dates et assignations
+
+
+
+
+
+
+
+
+
+
+
+
+ Après: étape {{ step.depends_on_step + 1 }}
+
+
+
+
+
+
+
+
+ Vérifiez le projet avant publication
+
+
+
+
+
+
+
+
{{ i + 1 }}
+
+
{{ step.subject }}
+
+ {{ step.job_type }}
+ {{ step.assigned_group }}
+ {{ step.scheduled_date }}
+ {{ step.duration_h }}h
+
+ n8n
+
+
+
+
{{ step.priority }}
+
+
+
+
+
+
+
+
+
Prêt à publier
+
+ {{ wizardSteps.length }} tâches seront créées et liées au ticket {{ issue?.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/ops/src/components/shared/TaskNode.vue b/apps/ops/src/components/shared/TaskNode.vue
new file mode 100644
index 0000000..d039a6e
--- /dev/null
+++ b/apps/ops/src/components/shared/TaskNode.vue
@@ -0,0 +1,443 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ job.name }}
+ {{ job.step_order }}
+ {{ job.subject }}
+
+
+ {{ job.job_type }}
+ {{ job.assigned_tech }}
+ {{ job.scheduled_date }}
+
+ après {{ job.depends_on }}
+
+
+ n8n
+
+
+ {{ children.length }}
+
+
+ {{ tag }}
+
+
+
+ {{ job.status || 'open' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tags (pour dispatch timeline)
+
+
+
+ {{ tag }}
+
+
+
+
+
+
+
+
+ Ouvrir dans le tableau dispatch
+
+
+
+ Supprimer cette tâche
+
+
+
+
+
+
+ $emit('fire-webhook', url, j)"
+ @deleted="(name) => $emit('deleted', name)"
+ @updated="(name, data) => $emit('updated', name, data)"
+ />
+
+
+
+
+
+
+
diff --git a/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue b/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue
new file mode 100644
index 0000000..8184283
--- /dev/null
+++ b/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue
@@ -0,0 +1,84 @@
+
+
+
Type
+ doc.equipment_type = v.value" />
+
+
Statut
+ doc.status = v.value" />
+
+
Marque
+ doc.brand = v.value" />
+
+
Modele
+ doc.model = v.value" />
+
+
N serie
+ doc.serial_number = v.value" />
+
+
MAC
+ doc.mac_address = v.value" />
+
+
Code-barres{{ doc.barcode }}
+
IP
+ doc.ip_address = v.value" />
+
+
Firmware
+ doc.firmware_version = v.value" />
+
+
Propriete
+ doc.ownership = v.value" />
+
+
+
+
Information OLT
+
+
OLT{{ doc.olt_name }}
+
IP OLT{{ doc.olt_ip }}
+
Slot / Port{{ doc.olt_slot }} / {{ doc.olt_port }}
+
ONT ID{{ doc.olt_ontid }}
+
+
Installe le{{ doc.installation_date }}
+
Fin garantie{{ doc.warranty_end }}
+
Adresse{{ doc.service_location }}
+
Abonnement{{ doc.subscription }}
+
+
+
Acces distant
+
Utilisateur
+ doc.login_user = v.value" />
+
+
+
+
Notes
+
doc.notes = v.value" />
+
+
+
Historique ({{ doc.move_log.length }})
+
+ {{ m.date }} --- {{ m.from_location || '?' }} → {{ m.to_location || '?' }} {{ m.reason ? '(' + m.reason + ')' : '' }}
+
+
+
+
+
diff --git a/apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue b/apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue
new file mode 100644
index 0000000..ca55ff7
--- /dev/null
+++ b/apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue
@@ -0,0 +1,62 @@
+
+
+
Date{{ doc.posting_date }}
+
Echeance{{ doc.due_date || '---' }}
+
Statut{{ doc.status }}
+
Total HT{{ formatMoney(doc.net_total) }}
+
Taxes{{ formatMoney(doc.total_taxes_and_charges) }}
+
Total TTC{{ formatMoney(doc.grand_total) }}
+
Solde du{{ formatMoney(doc.outstanding_amount) }}
+
Devise{{ doc.currency || 'CAD' }}
+
TypeNote de credit
+
+
+
+
Remarques
+
doc.remarks = v.value" />
+
+
+
Articles ({{ doc.items.length }})
+
+
+ {{ formatMoney(p.row.amount) }}
+
+
+ {{ formatMoney(p.row.rate) }}
+
+
+
+
+
Taxes
+
+ {{ t.description || t.account_head }}
+
+ {{ formatMoney(t.tax_amount) }}
+
+
+
+
Notes ({{ comments.length }})
+
+
{{ mc.comment_by || 'Systeme' }} · {{ formatDateShort(mc.creation) }}
+
{{ mc.content }}
+
+
+
+
+
diff --git a/apps/ops/src/components/shared/detail-sections/IssueDetail.vue b/apps/ops/src/components/shared/detail-sections/IssueDetail.vue
new file mode 100644
index 0000000..9a6f763
--- /dev/null
+++ b/apps/ops/src/components/shared/detail-sections/IssueDetail.vue
@@ -0,0 +1,282 @@
+
+
+
Statut
+ doc.status = v.value" />
+
+
Priorite
+ doc.priority = v.value" />
+
+
Ouvert le{{ formatDate(doc.opening_date) }}
+
Resolu le{{ doc.resolution_time }}
+
Type
+ doc.issue_type = v.value" />
+
+
Adresse{{ doc.service_location }}
+
Soumis par{{ doc.raised_by }}
+
Proprietaire{{ doc.owner }}
+
+
+
+
+
+
+
+ Tâches ({{ dispatchJobs.length }})
+
+
+
+
+ Créer un projet (modèle multi-étapes)
+
+
+ Ajouter une tâche simple
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Aucune tâche liée à ce ticket.
+
+
+
+
+
Sujet
+
doc.subject = v.value" />
+
+
+
Ticket parent
+
+ {{ doc.issue_split_from }}
+
+
+
+
+
+
Echanges ({{ comms.length }})
+
+
+
+
{{ comm.sender || comm.owner }}
+
+
+
+ {{ formatDate(comm.creation) }}
+
+
+
+
+
+
Pieces jointes ({{ files.length }})
+
+
+
+
Fil de discussion ({{ comments.length }})
+
+
+
+ Aucun contenu pour ce ticket
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/ops/src/components/shared/detail-sections/PaymentDetail.vue b/apps/ops/src/components/shared/detail-sections/PaymentDetail.vue
new file mode 100644
index 0000000..dce0582
--- /dev/null
+++ b/apps/ops/src/components/shared/detail-sections/PaymentDetail.vue
@@ -0,0 +1,29 @@
+
+
+
Date{{ doc.posting_date }}
+
Mode{{ doc.mode_of_payment || '---' }}
+
Montant paye{{ formatMoney(doc.paid_amount) }}
+
Reference{{ doc.reference_no || '---' }}
+
Date ref.{{ doc.reference_date }}
+
Compte paye de{{ doc.paid_from || '---' }}
+
Compte paye a{{ doc.paid_to || '---' }}
+
+
+
Factures liees
+
+ {{ r.reference_name }}
+
+ {{ formatMoney(r.allocated_amount) }}
+
+
+
+
+
diff --git a/apps/ops/src/components/shared/detail-sections/SubscriptionDetail.vue b/apps/ops/src/components/shared/detail-sections/SubscriptionDetail.vue
new file mode 100644
index 0000000..c846272
--- /dev/null
+++ b/apps/ops/src/components/shared/detail-sections/SubscriptionDetail.vue
@@ -0,0 +1,52 @@
+
+
+
Statut{{ doc.status }}
+
SKU{{ doc.item_code }}
+
Article{{ doc.item_name }}
+
Frequence{{ doc.billing_frequency === 'A' ? 'Annuel' : 'Mensuel' }}
+
Debut{{ doc.start_date || '---' }}
+
Fin{{ doc.end_date || '---' }}
+
Adresse{{ doc.service_location }}
+
PPPoE{{ doc.radius_user }}
+
Mot de passe{{ doc.radius_pwd }}
+
+
+
Modifier
+
+
+
+
+
+
+ Prochaine facture: {{ formatDate(doc.current_invoice_start) }} --- {{ formatDate(doc.current_invoice_end) }}
+
+
+
+
+
+
Plans
+
+ {{ p.plan || p.item }}
+
+ {{ formatMoney(p.cost) }}
+
+
+
+
+
diff --git a/apps/ops/src/composables/useContextMenus.js b/apps/ops/src/composables/useContextMenus.js
new file mode 100644
index 0000000..96cfd03
--- /dev/null
+++ b/apps/ops/src/composables/useContextMenus.js
@@ -0,0 +1,71 @@
+import { ref } from 'vue'
+import { updateJob } from 'src/api/dispatch'
+import { serializeAssistants } from 'src/composables/useHelpers'
+
+export function useContextMenus ({ store, rightPanel, invalidateRoutes, fullUnassign, openMoveModal, openEditModal }) {
+ const ctxMenu = ref(null)
+ const techCtx = ref(null)
+ const assistCtx = ref(null)
+ const assistNoteModal = ref(null)
+
+ function openCtxMenu (e, job, techId) {
+ e.preventDefault(); e.stopPropagation()
+ ctxMenu.value = { x: Math.min(e.clientX, window.innerWidth-180), y: Math.min(e.clientY, window.innerHeight-200), job, techId }
+ }
+ function closeCtxMenu () { ctxMenu.value = null }
+ function openTechCtx (e, tech) {
+ e.preventDefault(); e.stopPropagation()
+ techCtx.value = { x: Math.min(e.clientX, window.innerWidth-200), y: Math.min(e.clientY, window.innerHeight-200), tech }
+ }
+ function openAssistCtx (e, job, techId) {
+ e.preventDefault(); e.stopPropagation()
+ assistCtx.value = { x: Math.min(e.clientX, window.innerWidth-200), y: Math.min(e.clientY, window.innerHeight-150), job, techId }
+ }
+ function assistCtxTogglePin () {
+ if (!assistCtx.value) return
+ const { job, techId } = assistCtx.value
+ const assist = job.assistants.find(a => a.techId === techId)
+ if (assist) {
+ assist.pinned = !assist.pinned
+ updateJob(job.name || job.id, { assistants: serializeAssistants(job.assistants) }).catch(() => {})
+ invalidateRoutes()
+ }
+ assistCtx.value = null
+ }
+ function assistCtxRemove () {
+ if (!assistCtx.value) return
+ store.removeAssistant(assistCtx.value.job.id, assistCtx.value.techId)
+ invalidateRoutes(); assistCtx.value = null
+ }
+ function assistCtxNote () {
+ if (!assistCtx.value) return
+ const { job, techId } = assistCtx.value
+ const assist = job.assistants.find(a => a.techId === techId)
+ assistNoteModal.value = { job, techId, note: assist?.note || '' }
+ assistCtx.value = null
+ }
+ function confirmAssistNote () {
+ if (!assistNoteModal.value) return
+ const { job, techId, note } = assistNoteModal.value
+ const assist = job.assistants.find(a => a.techId === techId)
+ if (assist) {
+ assist.note = note
+ updateJob(job.name || job.id, { assistants: serializeAssistants(job.assistants) }).catch(() => {})
+ }
+ assistNoteModal.value = null
+ }
+
+ function ctxDetails () {
+ const { job, techId } = ctxMenu.value
+ rightPanel.value = { mode: 'details', data: { job, tech: store.technicians.find(t => t.id === techId) } }; closeCtxMenu()
+ }
+ function ctxMove () { const { job, techId } = ctxMenu.value; openMoveModal(job, techId); closeCtxMenu() }
+ function ctxUnschedule () { fullUnassign(ctxMenu.value.job); closeCtxMenu() }
+
+ return {
+ ctxMenu, techCtx, assistCtx, assistNoteModal,
+ openCtxMenu, closeCtxMenu, openTechCtx, openAssistCtx,
+ assistCtxTogglePin, assistCtxRemove, assistCtxNote, confirmAssistNote,
+ ctxDetails, ctxMove, ctxUnschedule,
+ }
+}
diff --git a/apps/ops/src/composables/useCustomerNotes.js b/apps/ops/src/composables/useCustomerNotes.js
new file mode 100644
index 0000000..9ff0279
--- /dev/null
+++ b/apps/ops/src/composables/useCustomerNotes.js
@@ -0,0 +1,101 @@
+/**
+ * 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
} comments - Reactive comments list
+ * @param {import('vue').Ref