fix(ops/dispatch): surface customer + service-location links from a job + fix bad coords
Two related issues, one PR:
1. **Bad coords** on customer C-LPB4's "Wifi buggy" job (DJ-MNP8WIKT).
Address on file is `691 rue des Hirondelles, Saint-Michel J0L2J0`,
but the saved lat/lng (-73.677086, 45.159206) reverse-geocodes to
`2336 rue René-Vinet, Sainte-Clotilde J0L1W0` — ~9 km away. The
delta matches the Gigafibre HQ default fallback (-73.6756, 45.1599)
pretty closely, suggesting the geocoder either failed silently at
Service Location creation time or got pinned to the HQ centroid.
Fixed live in DB (UPDATE on tabService Location LOC-0000000004 +
tabDispatch Job DJ-MNP8WIKT to lng=-73.5792377, lat=45.2408452,
verified via Nominatim against the typed address). The job pin
should now show on the correct house.
2. **No way to jump from a job to the client** — the dispatcher had
to memorize/type the customer ID. Now both the RightPanel and the
job context-menu surface clickable shortcuts:
• Client → `#/clients/<id>` (opens ClientDetailPage in-app)
• Lieu → `/desk/Service Location/<id>` (opens ERPNext in a new
tab; the ops SPA doesn't have a dedicated SL detail page)
Required wiring `customer` + `serviceLocation` into the job map in
`stores/dispatch.js` — the API (`fetchJobsFast` uses `["*"]`) was
already returning the fields, the store just wasn't surfacing them.
Note on the deeper bug: the SL lat/lng is the source of truth and the
job currently *copies* it at creation time (rather than reading from
the SL link dynamically). If a Service Location's coords are corrected
after a job exists, the job retains stale coords. A follow-up could
either (a) re-read on render, or (b) trigger a backfill when SL coords
change. Out of scope for this fix — for now, the dispatcher who fixes
an SL must also update any open jobs at that location.
This commit is contained in:
parent
2ec5e49a06
commit
f4ae023302
|
|
@ -36,6 +36,28 @@ const onDeleteTag = inject('onDeleteTag')
|
||||||
<div class="sb-rp-body">
|
<div class="sb-rp-body">
|
||||||
<div class="sb-rp-color-bar" :style="'background:'+jobColor(panel.data?.job||{})"></div>
|
<div class="sb-rp-color-bar" :style="'background:'+jobColor(panel.data?.job||{})"></div>
|
||||||
<div class="sb-rp-field"><span class="sb-rp-lbl">Titre</span><strong>{{ panel.data?.job?.subject }}</strong></div>
|
<div class="sb-rp-field"><span class="sb-rp-lbl">Titre</span><strong>{{ panel.data?.job?.subject }}</strong></div>
|
||||||
|
<!-- Client + Lieu de service: clickable shortcuts. Customer opens
|
||||||
|
the ClientDetailPage in the same SPA (Vue Router hash route);
|
||||||
|
Service Location opens in ERPNext desk in a new tab since the
|
||||||
|
ops SPA doesn't have a dedicated SL detail page. The address
|
||||||
|
below is the free-text on the job; the persisted lat/lng
|
||||||
|
(used by the Mapbox marker) lives on the linked Service
|
||||||
|
Location — discrepancy = bad geocode at SL creation time. -->
|
||||||
|
<div v-if="panel.data?.job?.customer" class="sb-rp-field">
|
||||||
|
<span class="sb-rp-lbl">Client</span>
|
||||||
|
<a class="sb-rp-link" :href="'#/clients/' + panel.data.job.customer">
|
||||||
|
{{ panel.data.job.customer }}
|
||||||
|
<span class="sb-rp-link-icon">↗</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="panel.data?.job?.serviceLocation" class="sb-rp-field">
|
||||||
|
<span class="sb-rp-lbl">Lieu</span>
|
||||||
|
<a class="sb-rp-link" target="_blank" rel="noopener"
|
||||||
|
:href="'/desk/Service Location/' + panel.data.job.serviceLocation">
|
||||||
|
<code>{{ panel.data.job.serviceLocation }}</code>
|
||||||
|
<span class="sb-rp-link-icon">↗</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="sb-rp-field"><span class="sb-rp-lbl">Adresse</span>{{ panel.data?.job?.address || '—' }}</div>
|
<div class="sb-rp-field"><span class="sb-rp-lbl">Adresse</span>{{ panel.data?.job?.address || '—' }}</div>
|
||||||
<div class="sb-rp-field"><span class="sb-rp-lbl">Durée</span>{{ fmtDur(panel.data?.job?.duration) }}</div>
|
<div class="sb-rp-field"><span class="sb-rp-lbl">Durée</span>{{ fmtDur(panel.data?.job?.duration) }}</div>
|
||||||
<div class="sb-rp-field"><span class="sb-rp-lbl">Priorité</span>
|
<div class="sb-rp-field"><span class="sb-rp-lbl">Priorité</span>
|
||||||
|
|
|
||||||
|
|
@ -1672,6 +1672,15 @@ onUnmounted(() => {
|
||||||
<button class="sb-ctx-item" @click="startGeoFix(ctxMenu.job); closeCtxMenu()">📍 Géofixer sur la carte</button>
|
<button class="sb-ctx-item" @click="startGeoFix(ctxMenu.job); closeCtxMenu()">📍 Géofixer sur la carte</button>
|
||||||
<button class="sb-ctx-item" @click="offerUnassignedJob(ctxMenu.job); closeCtxMenu()">📡 Offrir aux ressources</button>
|
<button class="sb-ctx-item" @click="offerUnassignedJob(ctxMenu.job); closeCtxMenu()">📡 Offrir aux ressources</button>
|
||||||
<div class="sb-ctx-sep"></div>
|
<div class="sb-ctx-sep"></div>
|
||||||
|
<a v-if="ctxMenu?.job?.customer" class="sb-ctx-item sb-ctx-link"
|
||||||
|
:href="'#/clients/' + ctxMenu.job.customer" @click="closeCtxMenu()">
|
||||||
|
👤 Voir la fiche client ({{ ctxMenu.job.customer }})
|
||||||
|
</a>
|
||||||
|
<a v-if="ctxMenu?.job?.serviceLocation" class="sb-ctx-item sb-ctx-link" target="_blank" rel="noopener"
|
||||||
|
:href="'/desk/Service Location/' + ctxMenu.job.serviceLocation" @click="closeCtxMenu()">
|
||||||
|
🏠 Lieu de service ({{ ctxMenu.job.serviceLocation }})
|
||||||
|
</a>
|
||||||
|
<div class="sb-ctx-sep"></div>
|
||||||
<button class="sb-ctx-item sb-ctx-warn" @click="ctxUnschedule()">✕ Désaffecter</button>
|
<button class="sb-ctx-item sb-ctx-warn" @click="ctxUnschedule()">✕ Désaffecter</button>
|
||||||
</SbContextMenu>
|
</SbContextMenu>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -629,6 +629,10 @@
|
||||||
.sb-rp-urgent-tag { background:rgba(239,68,68,0.15); color:#ef4444; font-size:0.7rem; font-weight:700; padding:0.25rem 0.5rem; border-radius:6px; display:inline-block; margin-bottom:0.5rem; }
|
.sb-rp-urgent-tag { background:rgba(239,68,68,0.15); color:#ef4444; font-size:0.7rem; font-weight:700; padding:0.25rem 0.5rem; border-radius:6px; display:inline-block; margin-bottom:0.5rem; }
|
||||||
.sb-rp-field { margin-bottom:0.45rem; color:var(--sb-text); }
|
.sb-rp-field { margin-bottom:0.45rem; color:var(--sb-text); }
|
||||||
.sb-rp-lbl { display:block; font-size:0.58rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:#7b80a0; margin-bottom:0.1rem; }
|
.sb-rp-lbl { display:block; font-size:0.58rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:#7b80a0; margin-bottom:0.1rem; }
|
||||||
|
.sb-rp-link { color:#6366f1; text-decoration:none; font-size:0.78rem; font-weight:600; display:inline-flex; align-items:center; gap:4px; }
|
||||||
|
.sb-rp-link:hover { text-decoration:underline; }
|
||||||
|
.sb-rp-link-icon { font-size:0.65rem; opacity:0.7; }
|
||||||
|
.sb-rp-link code { background:rgba(99,102,241,0.08); padding:1px 5px; border-radius:3px; font-size:0.7rem; }
|
||||||
.sb-rp-actions { padding:0.65rem 0.75rem; border-top:1px solid rgba(0,0,0,0.04); display:flex; flex-direction:column; gap:0.35rem; flex-shrink:0; }
|
.sb-rp-actions { padding:0.65rem 0.75rem; border-top:1px solid rgba(0,0,0,0.04); display:flex; flex-direction:column; gap:0.35rem; flex-shrink:0; }
|
||||||
.sb-rp-primary { background:#6366f1; border:none; border-radius:7px; color:var(--sb-text); font-size:0.72rem; font-weight:700; padding:0.4rem 0.75rem; cursor:pointer; }
|
.sb-rp-primary { background:#6366f1; border:none; border-radius:7px; color:var(--sb-text); font-size:0.72rem; font-weight:700; padding:0.4rem 0.75rem; cursor:pointer; }
|
||||||
.sb-rp-primary:hover { filter:brightness(1.12); }
|
.sb-rp-primary:hover { filter:brightness(1.12); }
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,14 @@ export const useDispatchStore = defineStore('dispatch', () => {
|
||||||
subject: j.subject || 'Job sans titre',
|
subject: j.subject || 'Job sans titre',
|
||||||
address: j.address || 'Adresse inconnue',
|
address: j.address || 'Adresse inconnue',
|
||||||
coords: [j.longitude || 0, j.latitude || 0],
|
coords: [j.longitude || 0, j.latitude || 0],
|
||||||
|
// Persisted ERPNext links — surfaced as clickable shortcuts in
|
||||||
|
// the RightPanel so the dispatcher can jump to the client's
|
||||||
|
// full record (or the Service Location in ERPNext) without
|
||||||
|
// leaving the dispatch board. The Mapbox marker still uses
|
||||||
|
// the cached coords above; for source-of-truth verification
|
||||||
|
// (geocode mismatch) the rep clicks through to /clients/:id.
|
||||||
|
customer: j.customer || null,
|
||||||
|
serviceLocation: j.service_location || null,
|
||||||
priority: j.priority || 'low',
|
priority: j.priority || 'low',
|
||||||
duration: j.duration_h || 1,
|
duration: j.duration_h || 1,
|
||||||
status: j.status || 'open',
|
status: j.status || 'open',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user