refactor(ops): extract DeviceStrip from ClientDetailPage
T7 découpage géants — sort le bandeau d'appareils (chips ONT/routeur + menu Ajouter) dans components/customer/DeviceStrip.vue. Consomme le composable singleton useDeviceStatus (couche données ACS/OLT partagée) ; les helpers de présentation propres aux appareils (signalColor, formatTimeAgo, doReboot, doRefreshParams) déménagent DANS le composant (utilisés nulle part ailleurs). Le parent émet open-device + add-*. ClientDetailPage -108 lignes ; imports/destructures device retirés. Vérifié en local sur hub LIVE (C-LPB4) : chip vert + point acs-online réel, tooltip ACS complet (SN TPLGC4160688, OLT oltRem03 1/3/0, En ligne il y a 4m, Fibre Up, 17 clients WiFi, FW/WAN IP/SSID), 0 warning console. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b9df6d720a
commit
dc28437d5f
161
apps/ops/src/components/customer/DeviceStrip.vue
Normal file
161
apps/ops/src/components/customer/DeviceStrip.vue
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
<template>
|
||||||
|
<div class="device-strip">
|
||||||
|
<div v-for="eq in equipment" :key="eq.name" class="device-icon-chip" :class="deviceColorClass(eq.status)"
|
||||||
|
@click="$emit('open-device', eq)">
|
||||||
|
<q-icon :name="deviceLucideIcon(eq.equipment_type)" size="20px" />
|
||||||
|
<span v-if="isOnline(eq.serial_number) === true" class="acs-dot acs-online" />
|
||||||
|
<span v-else-if="isOnline(eq.serial_number) === false" class="acs-dot acs-offline" />
|
||||||
|
<span v-else-if="eq.serial_number && !getDevice(eq.serial_number)" class="acs-dot acs-probing" />
|
||||||
|
<q-tooltip class="bg-grey-9 text-caption" :offset="[0, 6]" style="max-width:320px">
|
||||||
|
<div><strong>{{ eq.equipment_type }}{{ eq.brand ? ' — ' + eq.brand : '' }}{{ eq.model ? ' ' + eq.model : '' }}</strong></div>
|
||||||
|
<div>SN: {{ eq.serial_number }}</div>
|
||||||
|
<template v-if="eq.mac_address"><div>MAC: {{ eq.mac_address }}</div></template>
|
||||||
|
<template v-if="eq.olt_name"><div>OLT: {{ eq.olt_name }} — Slot {{ eq.olt_slot }}/Port {{ eq.olt_port }}/ONT {{ eq.olt_ontid }}</div></template>
|
||||||
|
<template v-else-if="eq.ip_address"><div>IP: {{ eq.ip_address }}</div></template>
|
||||||
|
<div>{{ eq.status }}</div>
|
||||||
|
<template v-if="getDevice(eq.serial_number) || combinedStatus(eq.serial_number).source !== 'unknown'">
|
||||||
|
<q-separator dark class="q-my-xs" />
|
||||||
|
<div :style="{ color: combinedStatus(eq.serial_number).online ? '#4ade80' : '#f87171' }">
|
||||||
|
{{ combinedStatus(eq.serial_number).online ? '● En ligne' : '● Hors ligne' }}
|
||||||
|
<template v-if="getDevice(eq.serial_number)?.lastInform"> — {{ formatTimeAgo(getDevice(eq.serial_number).lastInform) }}</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-grey-4" style="font-size:0.7rem">{{ combinedStatus(eq.serial_number).detail }}</div>
|
||||||
|
<template v-if="getDevice(eq.serial_number)?.opticalStatus">
|
||||||
|
<div>Fibre: <span :style="{ color: getDevice(eq.serial_number).opticalStatus === 'Up' ? '#4ade80' : '#f87171' }">{{ getDevice(eq.serial_number).opticalStatus }}</span></div>
|
||||||
|
</template>
|
||||||
|
<template v-if="getDevice(eq.serial_number).rxPower != null && getDevice(eq.serial_number).rxPower !== 0">
|
||||||
|
<div :style="{ color: signalColor(eq.serial_number) }">
|
||||||
|
Rx: {{ getDevice(eq.serial_number).rxPower }} dBm
|
||||||
|
<template v-if="getDevice(eq.serial_number).txPower != null && getDevice(eq.serial_number).txPower !== 0"> / Tx: {{ getDevice(eq.serial_number).txPower }} dBm</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="getDevice(eq.serial_number).wifi">
|
||||||
|
<div v-if="getDevice(eq.serial_number).wifi.totalClients != null || getDevice(eq.serial_number).wifi.radio1?.clients != null">
|
||||||
|
WiFi: {{ getDevice(eq.serial_number).wifi.totalClients ?? ((getDevice(eq.serial_number).wifi.radio1?.clients || 0) + (getDevice(eq.serial_number).wifi.radio2?.clients || 0) + (getDevice(eq.serial_number).wifi.radio3?.clients || 0)) }} clients
|
||||||
|
<span v-if="getDevice(eq.serial_number).wifi.meshClients > 0">({{ getDevice(eq.serial_number).wifi.meshClients }} via mesh)</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="getDevice(eq.serial_number).firmware"><div>FW: {{ getDevice(eq.serial_number).firmware }}</div></template>
|
||||||
|
<template v-if="getDevice(eq.serial_number).ip"><div>WAN IP: {{ getDevice(eq.serial_number).ip }}</div></template>
|
||||||
|
<template v-if="getDevice(eq.serial_number).ssid"><div>SSID: {{ getDevice(eq.serial_number).ssid }}</div></template>
|
||||||
|
</template>
|
||||||
|
</q-tooltip>
|
||||||
|
<q-menu context-menu v-if="getDevice(eq.serial_number)">
|
||||||
|
<q-list dense style="min-width:160px">
|
||||||
|
<q-item clickable v-close-popup @click="doReboot(eq)">
|
||||||
|
<q-item-section avatar><q-icon name="restart_alt" size="18px" /></q-item-section>
|
||||||
|
<q-item-section>Redémarrer</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="doRefreshParams(eq)">
|
||||||
|
<q-item-section avatar><q-icon name="sync" size="18px" /></q-item-section>
|
||||||
|
<q-item-section>Rafraîchir params</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</div>
|
||||||
|
<div class="device-icon-chip device-add-chip">
|
||||||
|
<q-icon name="add" size="20px" />
|
||||||
|
<q-tooltip>Ajouter</q-tooltip>
|
||||||
|
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
||||||
|
<q-list dense style="min-width:240px">
|
||||||
|
<q-item clickable v-close-popup @click="$emit('add-submission')">
|
||||||
|
<q-item-section avatar><q-icon name="description" size="18px" color="primary" /></q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Nouvelle soumission</q-item-label>
|
||||||
|
<q-item-label caption>Wizard avec étapes dispatchables</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator />
|
||||||
|
<q-item clickable v-close-popup @click="$emit('add-service')">
|
||||||
|
<q-item-section avatar><q-icon name="wifi" size="18px" color="info" /></q-item-section>
|
||||||
|
<q-item-section>Forfait / Service (direct)</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="$emit('add-rebate')">
|
||||||
|
<q-item-section avatar><q-icon name="sell" size="18px" color="negative" /></q-item-section>
|
||||||
|
<q-item-section>Rabais / Crédit</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator />
|
||||||
|
<q-item clickable v-close-popup @click="$emit('add-equipment')">
|
||||||
|
<q-item-section avatar><q-icon name="router" size="18px" color="positive" /></q-item-section>
|
||||||
|
<q-item-section>Équipement</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Notify } from 'quasar'
|
||||||
|
import { deviceColorClass } from 'src/composables/useStatusClasses'
|
||||||
|
import { deviceLucideIcon } from 'src/config/device-icons'
|
||||||
|
import { useDeviceStatus } from 'src/composables/useDeviceStatus'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
equipment: { type: Array, default: () => [] },
|
||||||
|
})
|
||||||
|
defineEmits(['open-device', 'add-submission', 'add-service', 'add-rebate', 'add-equipment'])
|
||||||
|
|
||||||
|
// useDeviceStatus is a singleton composable — shares the live GenieACS/OLT
|
||||||
|
// device map that ClientDetailPage populates via fetchStatus/fetchOltStatus.
|
||||||
|
const { getDevice, isOnline, combinedStatus, signalQuality, rebootDevice, refreshDeviceParams, fetchStatus } = useDeviceStatus()
|
||||||
|
|
||||||
|
// ── Device-only presentation/action helpers (used nowhere else) ──
|
||||||
|
function signalColor (serial) {
|
||||||
|
const q = signalQuality(serial)
|
||||||
|
return q === 'excellent' ? '#4ade80' : q === 'good' ? '#a3e635' : q === 'fair' ? '#fbbf24' : '#f87171'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeAgo (dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const diff = Date.now() - new Date(dateStr).getTime()
|
||||||
|
const mins = Math.floor(diff / 60000)
|
||||||
|
if (mins < 1) return 'à l\'instant'
|
||||||
|
if (mins < 60) return `il y a ${mins}m`
|
||||||
|
const hrs = Math.floor(mins / 60)
|
||||||
|
return hrs < 24 ? `il y a ${hrs}h` : `il y a ${Math.floor(hrs / 24)}j`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doReboot (eq) {
|
||||||
|
try {
|
||||||
|
await rebootDevice(eq.serial_number)
|
||||||
|
Notify.create({ type: 'positive', message: `Redémarrage envoyé: ${eq.serial_number}`, position: 'top' })
|
||||||
|
} catch (e) {
|
||||||
|
Notify.create({ type: 'negative', message: `Erreur: ${e.message}`, position: 'top' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRefreshParams (eq) {
|
||||||
|
try {
|
||||||
|
await refreshDeviceParams(eq.serial_number)
|
||||||
|
Notify.create({ type: 'info', message: `Rafraîchissement lancé: ${eq.serial_number}`, position: 'top' })
|
||||||
|
setTimeout(() => fetchStatus([eq]), 3000)
|
||||||
|
} catch (e) {
|
||||||
|
Notify.create({ type: 'negative', message: `Erreur: ${e.message}`, position: 'top' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-strip { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.device-icon-chip {
|
||||||
|
display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 8px; cursor: pointer; transition: transform 0.15s; position: relative;
|
||||||
|
&:hover { transform: scale(1.1); }
|
||||||
|
&.small { width: 28px; height: 28px; border-radius: 6px; flex-shrink: 0; }
|
||||||
|
&.dev-green { background: #dcfce7; color: #16a34a; }
|
||||||
|
&.dev-red { background: #fee2e2; color: #dc2626; }
|
||||||
|
&.dev-grey { background: #f1f5f9; color: #94a3b8; }
|
||||||
|
}
|
||||||
|
.acs-dot { position: absolute; top: 2px; right: 2px; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid #fff; }
|
||||||
|
.acs-online { background: #22c55e; box-shadow: 0 0 4px #22c55e88; }
|
||||||
|
.acs-offline { background: #ef4444; box-shadow: 0 0 4px #ef444488; }
|
||||||
|
.acs-probing { background: #f59e0b; animation: acs-pulse 1.2s ease-in-out infinite; }
|
||||||
|
@keyframes acs-pulse {
|
||||||
|
0%, 100% { opacity: 1; box-shadow: 0 0 4px #f59e0b88; }
|
||||||
|
50% { opacity: 0.3; box-shadow: 0 0 8px #f59e0bcc; }
|
||||||
|
}
|
||||||
|
.device-add-chip {
|
||||||
|
background: #f1f5f9; color: #94a3b8; border: 2px dashed #cbd5e1;
|
||||||
|
&:hover { border-color: #6366f1; color: #6366f1; background: #eef2ff; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -123,90 +123,14 @@
|
||||||
· <InlineField :value="loc.network_id" field="network_id" doctype="Service Location" :docname="loc.name"
|
· <InlineField :value="loc.network_id" field="network_id" doctype="Service Location" :docname="loc.name"
|
||||||
placeholder="Network ID" @saved="v => loc.network_id = v.value" />
|
placeholder="Network ID" @saved="v => loc.network_id = v.value" />
|
||||||
</div>
|
</div>
|
||||||
<div class="device-strip">
|
<DeviceStrip
|
||||||
<div v-for="eq in locEquip(loc.name)" :key="eq.name" class="device-icon-chip" :class="deviceColorClass(eq.status)"
|
:equipment="locEquip(loc.name)"
|
||||||
@click="openModal('Service Equipment', eq.name, eq.equipment_type + (eq.brand ? ' — ' + eq.brand : ''))">
|
@open-device="eq => openModal('Service Equipment', eq.name, eq.equipment_type + (eq.brand ? ' — ' + eq.brand : ''))"
|
||||||
<q-icon :name="deviceLucideIcon(eq.equipment_type)" size="20px" />
|
@add-submission="openWizardForAddress(loc)"
|
||||||
<span v-if="isOnline(eq.serial_number) === true" class="acs-dot acs-online" />
|
@add-service="openAddService(loc)"
|
||||||
<span v-else-if="isOnline(eq.serial_number) === false" class="acs-dot acs-offline" />
|
@add-rebate="openAddService(loc, 'rabais')"
|
||||||
<span v-else-if="eq.serial_number && !getDevice(eq.serial_number)" class="acs-dot acs-probing" />
|
@add-equipment="openAddEquipment(loc)"
|
||||||
<q-tooltip class="bg-grey-9 text-caption" :offset="[0, 6]" style="max-width:320px">
|
/>
|
||||||
<div><strong>{{ eq.equipment_type }}{{ eq.brand ? ' — ' + eq.brand : '' }}{{ eq.model ? ' ' + eq.model : '' }}</strong></div>
|
|
||||||
<div>SN: {{ eq.serial_number }}</div>
|
|
||||||
<template v-if="eq.mac_address"><div>MAC: {{ eq.mac_address }}</div></template>
|
|
||||||
<template v-if="eq.olt_name"><div>OLT: {{ eq.olt_name }} — Slot {{ eq.olt_slot }}/Port {{ eq.olt_port }}/ONT {{ eq.olt_ontid }}</div></template>
|
|
||||||
<template v-else-if="eq.ip_address"><div>IP: {{ eq.ip_address }}</div></template>
|
|
||||||
<div>{{ eq.status }}</div>
|
|
||||||
<template v-if="getDevice(eq.serial_number) || combinedStatus(eq.serial_number).source !== 'unknown'">
|
|
||||||
<q-separator dark class="q-my-xs" />
|
|
||||||
<div :style="{ color: combinedStatus(eq.serial_number).online ? '#4ade80' : '#f87171' }">
|
|
||||||
{{ combinedStatus(eq.serial_number).online ? '● En ligne' : '● Hors ligne' }}
|
|
||||||
<template v-if="getDevice(eq.serial_number)?.lastInform"> — {{ formatTimeAgo(getDevice(eq.serial_number).lastInform) }}</template>
|
|
||||||
</div>
|
|
||||||
<div class="text-grey-4" style="font-size:0.7rem">{{ combinedStatus(eq.serial_number).detail }}</div>
|
|
||||||
<template v-if="getDevice(eq.serial_number)?.opticalStatus">
|
|
||||||
<div>Fibre: <span :style="{ color: getDevice(eq.serial_number).opticalStatus === 'Up' ? '#4ade80' : '#f87171' }">{{ getDevice(eq.serial_number).opticalStatus }}</span></div>
|
|
||||||
</template>
|
|
||||||
<template v-if="getDevice(eq.serial_number).rxPower != null && getDevice(eq.serial_number).rxPower !== 0">
|
|
||||||
<div :style="{ color: signalColor(eq.serial_number) }">
|
|
||||||
Rx: {{ getDevice(eq.serial_number).rxPower }} dBm
|
|
||||||
<template v-if="getDevice(eq.serial_number).txPower != null && getDevice(eq.serial_number).txPower !== 0"> / Tx: {{ getDevice(eq.serial_number).txPower }} dBm</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-if="getDevice(eq.serial_number).wifi">
|
|
||||||
<div v-if="getDevice(eq.serial_number).wifi.totalClients != null || getDevice(eq.serial_number).wifi.radio1?.clients != null">
|
|
||||||
WiFi: {{ getDevice(eq.serial_number).wifi.totalClients ?? ((getDevice(eq.serial_number).wifi.radio1?.clients || 0) + (getDevice(eq.serial_number).wifi.radio2?.clients || 0) + (getDevice(eq.serial_number).wifi.radio3?.clients || 0)) }} clients
|
|
||||||
<span v-if="getDevice(eq.serial_number).wifi.meshClients > 0">({{ getDevice(eq.serial_number).wifi.meshClients }} via mesh)</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-if="getDevice(eq.serial_number).firmware"><div>FW: {{ getDevice(eq.serial_number).firmware }}</div></template>
|
|
||||||
<template v-if="getDevice(eq.serial_number).ip"><div>WAN IP: {{ getDevice(eq.serial_number).ip }}</div></template>
|
|
||||||
<template v-if="getDevice(eq.serial_number).ssid"><div>SSID: {{ getDevice(eq.serial_number).ssid }}</div></template>
|
|
||||||
</template>
|
|
||||||
</q-tooltip>
|
|
||||||
<q-menu context-menu v-if="getDevice(eq.serial_number)">
|
|
||||||
<q-list dense style="min-width:160px">
|
|
||||||
<q-item clickable v-close-popup @click="doReboot(eq)">
|
|
||||||
<q-item-section avatar><q-icon name="restart_alt" size="18px" /></q-item-section>
|
|
||||||
<q-item-section>Redémarrer</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-close-popup @click="doRefreshParams(eq)">
|
|
||||||
<q-item-section avatar><q-icon name="sync" size="18px" /></q-item-section>
|
|
||||||
<q-item-section>Rafraîchir params</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</div>
|
|
||||||
<div class="device-icon-chip device-add-chip">
|
|
||||||
<q-icon name="add" size="20px" />
|
|
||||||
<q-tooltip>Ajouter</q-tooltip>
|
|
||||||
<q-menu anchor="bottom right" self="top right" :offset="[0, 4]">
|
|
||||||
<q-list dense style="min-width:240px">
|
|
||||||
<q-item clickable v-close-popup @click="openWizardForAddress(loc)">
|
|
||||||
<q-item-section avatar><q-icon name="description" size="18px" color="primary" /></q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Nouvelle soumission</q-item-label>
|
|
||||||
<q-item-label caption>Wizard avec étapes dispatchables</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-separator />
|
|
||||||
<q-item clickable v-close-popup @click="openAddService(loc)">
|
|
||||||
<q-item-section avatar><q-icon name="wifi" size="18px" color="info" /></q-item-section>
|
|
||||||
<q-item-section>Forfait / Service (direct)</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable v-close-popup @click="openAddService(loc, 'rabais')">
|
|
||||||
<q-item-section avatar><q-icon name="sell" size="18px" color="negative" /></q-item-section>
|
|
||||||
<q-item-section>Rabais / Crédit</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-separator />
|
|
||||||
<q-item clickable v-close-popup @click="openAddEquipment(loc)">
|
|
||||||
<q-item-section avatar><q-icon name="router" size="18px" color="positive" /></q-item-section>
|
|
||||||
<q-item-section>Équipement</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-block subs-block">
|
<div class="info-block subs-block">
|
||||||
|
|
@ -1137,14 +1061,13 @@ import { authFetch } from 'src/api/auth'
|
||||||
import { BASE_URL, ERP_DESK_URL } from 'src/config/erpnext'
|
import { BASE_URL, ERP_DESK_URL } from 'src/config/erpnext'
|
||||||
import { HUB_URL } from 'src/config/hub'
|
import { HUB_URL } from 'src/config/hub'
|
||||||
import { formatDate, formatDateTime, formatMoney, staffColor, staffInitials, noteAuthorName } from 'src/composables/useFormatters'
|
import { formatDate, formatDateTime, formatMoney, staffColor, staffInitials, noteAuthorName } from 'src/composables/useFormatters'
|
||||||
import { locStatusClass, ticketStatusClass, invStatusClass, deviceColorClass, priorityClass } from 'src/composables/useStatusClasses'
|
import { locStatusClass, ticketStatusClass, invStatusClass, priorityClass } from 'src/composables/useStatusClasses'
|
||||||
import { useDetailModal } from 'src/composables/useDetailModal'
|
import { useDetailModal } from 'src/composables/useDetailModal'
|
||||||
import { useSubscriptionGroups, isRebate, subMainLabel, sectionTotal, annualPrice } from 'src/composables/useSubscriptionGroups'
|
import { useSubscriptionGroups, isRebate, subMainLabel, sectionTotal, annualPrice } from 'src/composables/useSubscriptionGroups'
|
||||||
import { useSubscriptionActions } from 'src/composables/useSubscriptionActions'
|
import { useSubscriptionActions } from 'src/composables/useSubscriptionActions'
|
||||||
import { useCustomerNotes } from 'src/composables/useCustomerNotes'
|
import { useCustomerNotes } from 'src/composables/useCustomerNotes'
|
||||||
import { useConversations } from 'src/composables/useConversations'
|
import { useConversations } from 'src/composables/useConversations'
|
||||||
import { invoiceCols, paymentCols, ticketCols, voipCols, paymentMethodCols, arrangementCols, quotationCols, serviceContractCols } from 'src/config/table-columns'
|
import { invoiceCols, paymentCols, ticketCols, voipCols, paymentMethodCols, arrangementCols, quotationCols, serviceContractCols } from 'src/config/table-columns'
|
||||||
import { deviceLucideIcon } from 'src/config/device-icons'
|
|
||||||
import DetailModal from 'src/components/shared/DetailModal.vue'
|
import DetailModal from 'src/components/shared/DetailModal.vue'
|
||||||
import DataTable from 'src/components/shared/DataTable.vue'
|
import DataTable from 'src/components/shared/DataTable.vue'
|
||||||
import CustomerHeader from 'src/components/customer/CustomerHeader.vue'
|
import CustomerHeader from 'src/components/customer/CustomerHeader.vue'
|
||||||
|
|
@ -1166,6 +1089,7 @@ import { locInlineFields, equipTypeOptions, equipStatusOptions, defaultSectionsO
|
||||||
import { useUserPrefs } from 'src/composables/useUserPrefs'
|
import { useUserPrefs } from 'src/composables/useUserPrefs'
|
||||||
import { erpPdfUrl } from 'src/utils/erp-pdf'
|
import { erpPdfUrl } from 'src/utils/erp-pdf'
|
||||||
import RewardDialog from 'src/components/customer/RewardDialog.vue'
|
import RewardDialog from 'src/components/customer/RewardDialog.vue'
|
||||||
|
import DeviceStrip from 'src/components/customer/DeviceStrip.vue'
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const { can } = usePermissions()
|
const { can } = usePermissions()
|
||||||
|
|
@ -1176,7 +1100,9 @@ const ticketsExpanded = ref(false)
|
||||||
const invoicesExpanded = ref(false)
|
const invoicesExpanded = ref(false)
|
||||||
const paymentsExpanded = ref(false)
|
const paymentsExpanded = ref(false)
|
||||||
|
|
||||||
const { fetchStatus, fetchOltStatus, getDevice, isOnline, combinedStatus, signalQuality, rebootDevice, refreshDeviceParams } = useDeviceStatus()
|
// Device status still needed at the page level to seed the shared map (fetchStatus/fetchOltStatus
|
||||||
|
// passed to useClientData); DeviceStrip consumes the same singleton composable for display.
|
||||||
|
const { fetchStatus, fetchOltStatus } = useDeviceStatus()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
modalOpen, modalLoading, modalDoctype, modalDocName, modalTitle,
|
modalOpen, modalLoading, modalDoctype, modalDocName, modalTitle,
|
||||||
|
|
@ -1509,40 +1435,6 @@ async function confirmDeleteSub (sub) {
|
||||||
|
|
||||||
function onDispatchCreated (job) { modalDispatchJobs.value.push(job) }
|
function onDispatchCreated (job) { modalDispatchJobs.value.push(job) }
|
||||||
|
|
||||||
function formatTimeAgo (dateStr) {
|
|
||||||
if (!dateStr) return ''
|
|
||||||
const diff = Date.now() - new Date(dateStr).getTime()
|
|
||||||
const mins = Math.floor(diff / 60000)
|
|
||||||
if (mins < 1) return 'à l\'instant'
|
|
||||||
if (mins < 60) return `il y a ${mins}m`
|
|
||||||
const hrs = Math.floor(mins / 60)
|
|
||||||
return hrs < 24 ? `il y a ${hrs}h` : `il y a ${Math.floor(hrs / 24)}j`
|
|
||||||
}
|
|
||||||
|
|
||||||
function signalColor (serial) {
|
|
||||||
const q = signalQuality(serial)
|
|
||||||
return q === 'excellent' ? '#4ade80' : q === 'good' ? '#a3e635' : q === 'fair' ? '#fbbf24' : '#f87171'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doReboot (eq) {
|
|
||||||
try {
|
|
||||||
await rebootDevice(eq.serial_number)
|
|
||||||
Notify.create({ type: 'positive', message: `Redémarrage envoyé: ${eq.serial_number}`, position: 'top' })
|
|
||||||
} catch (e) {
|
|
||||||
Notify.create({ type: 'negative', message: `Erreur: ${e.message}`, position: 'top' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doRefreshParams (eq) {
|
|
||||||
try {
|
|
||||||
await refreshDeviceParams(eq.serial_number)
|
|
||||||
Notify.create({ type: 'info', message: `Rafraîchissement lancé: ${eq.serial_number}`, position: 'top' })
|
|
||||||
setTimeout(() => fetchStatus([eq]), 3000)
|
|
||||||
} catch (e) {
|
|
||||||
Notify.create({ type: 'negative', message: `Erreur: ${e.message}`, position: 'top' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const locCollapsed = reactive({})
|
const locCollapsed = reactive({})
|
||||||
const locHasSubs = (locName) => subscriptions.value.some(s => s.service_location === locName)
|
const locHasSubs = (locName) => subscriptions.value.some(s => s.service_location === locName)
|
||||||
const toggleLocCollapse = (locName) => { locCollapsed[locName] = locCollapsed[locName] === false }
|
const toggleLocCollapse = (locName) => { locCollapsed[locName] = locCollapsed[locName] === false }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user