Password Policy/App rename/PM Scheduling Improvements
This commit is contained in:
@@ -424,12 +424,22 @@
|
||||
<v-list-item-title>{{ pm.plan_name || 'Maintenance' }} · every {{ pm.frequency_value }} {{ unitLabel(pm.frequency_unit) }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<template v-if="pm.active">
|
||||
Next due {{ pm.next_due_date || '—' }}<template v-if="pm.last_completed_date"> · last done {{ pm.last_completed_date }}</template>
|
||||
Next due {{ pm.next_due_date || '—' }}
|
||||
<v-chip v-if="dueDriver(pm)" size="x-small" variant="tonal" :color="dueDriver(pm).color" class="ml-1">
|
||||
<v-icon :icon="dueDriver(pm).icon" size="12" start /> {{ dueDriver(pm).label }}
|
||||
</v-chip>
|
||||
<template v-if="pm.last_completed_date"> · last done {{ pm.last_completed_date }}</template>
|
||||
</template>
|
||||
<template v-else>Schedule complete (past {{ pm.end_date }})</template>
|
||||
<template v-if="pm.total_cost"> · spent {{ currency(pm.total_cost) }}</template>
|
||||
<div v-for="m in pm.meters" :key="m.id" class="text-caption">
|
||||
{{ m.label }}: {{ m.last_reading != null ? Math.round(m.last_reading) : '—' }}<template v-if="m.due_at_reading"> / {{ Math.round(m.due_at_reading) }}</template>
|
||||
<span v-if="m.unit"> {{ m.unit }}</span>
|
||||
<span v-if="m.projected_usage_due_date" class="quiet"> · projected {{ m.projected_usage_due_date }}</span>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn v-if="pm.active && pm.meters && pm.meters.length" size="small" variant="text" @click="openLogReading(pm)">Log reading</v-btn>
|
||||
<v-btn v-if="pm.active" size="small" variant="text" color="primary" @click="openComplete(pm)">Complete</v-btn>
|
||||
<v-btn icon="mdi-delete-outline" size="small" variant="text" color="error" @click="$emit('detach-pm', pm.id)" />
|
||||
</template>
|
||||
@@ -522,6 +532,19 @@
|
||||
|
||||
<v-text-field v-model.number="completeCost" type="number" prefix="$" label="Service cost" density="compact" hide-details class="mt-3" />
|
||||
|
||||
<template v-if="completeMeters.length">
|
||||
<v-divider class="my-3" />
|
||||
<h3 class="section-title mb-1">Usage meter readings</h3>
|
||||
<p class="quiet text-caption mb-2">Record the current reading; the next service threshold resets from here.</p>
|
||||
<div v-for="(m, i) in completeMeters" :key="`m${i}`" class="d-flex align-center mb-2" style="gap:10px">
|
||||
<div class="text-body-2" style="min-width:180px">
|
||||
{{ m.label }}<span v-if="m.unit" class="quiet"> ({{ m.unit }})</span>
|
||||
<div v-if="m.last_reading != null" class="quiet text-caption">last {{ Math.round(m.last_reading) }}<template v-if="m.due_at_reading"> · due at {{ Math.round(m.due_at_reading) }}</template></div>
|
||||
</div>
|
||||
<v-text-field v-model.number="m.reading" type="number" density="compact" hide-details placeholder="Current reading" :suffix="m.unit || ''" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-divider class="my-3" />
|
||||
<div class="toolbar-row mb-1">
|
||||
<h3 class="section-title">Completion notes</h3>
|
||||
@@ -559,6 +582,32 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Quick usage-meter reading (recomputes the schedule without completing a service) -->
|
||||
<v-dialog v-model="logDialog" max-width="420">
|
||||
<v-card v-if="logTarget">
|
||||
<v-card-title>Log usage reading</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="quiet text-body-2 mb-3">{{ logTarget.plan_name || 'Maintenance' }}</div>
|
||||
<v-select
|
||||
v-if="(logTarget.meters || []).length > 1"
|
||||
v-model="logForm.asset_pm_meter_id"
|
||||
:items="(logTarget.meters || []).map((m) => ({ title: m.label, value: m.id }))"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="Meter"
|
||||
density="compact"
|
||||
/>
|
||||
<v-text-field v-model.number="logForm.reading" type="number" label="Current reading" density="compact" />
|
||||
<v-text-field v-model="logForm.reading_date" type="date" label="Reading date" density="compact" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="logDialog = false">Cancel</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-check" :disabled="logForm.reading === '' || logForm.reading == null || !logForm.asset_pm_meter_id" @click="submitLogReading">Save reading</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Prompt shown when depreciation-critical fields change on an already-depreciated asset -->
|
||||
<v-dialog v-model="applyDialog" max-width="560">
|
||||
<v-card>
|
||||
@@ -691,7 +740,7 @@ export default {
|
||||
'add-adjustment', 'delete-adjustment',
|
||||
'save-lease', 'delete-lease', 'load-lease-schedule',
|
||||
'print-label',
|
||||
'attach-pm', 'detach-pm', 'complete-pm', 'view-completion'
|
||||
'attach-pm', 'detach-pm', 'complete-pm', 'log-reading', 'view-completion'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
@@ -722,6 +771,10 @@ export default {
|
||||
completeRatings: { safety: 0, physical: 0, operating: 0 },
|
||||
completeSignature: '',
|
||||
completePhotos: [],
|
||||
completeMeters: [],
|
||||
logDialog: false,
|
||||
logTarget: null,
|
||||
logForm: { asset_pm_meter_id: null, reading: '', reading_date: new Date().toISOString().slice(0, 10) },
|
||||
ratingQuestions: [
|
||||
{ key: 'safety', label: 'Safety condition / Is it safe?' },
|
||||
{ key: 'physical', label: 'Physical condition / appearance' },
|
||||
@@ -926,6 +979,19 @@ export default {
|
||||
unitLabel(unit) {
|
||||
return (pmFrequencyUnits.find((u) => u.value === unit) || {}).title || unit;
|
||||
},
|
||||
// Which signal drove the next-due date (only meaningful once usage/lifespan are in play).
|
||||
dueDriver(pm) {
|
||||
const map = {
|
||||
usage: { label: 'usage', icon: 'mdi-gauge', color: 'info' },
|
||||
lifespan: { label: 'age', icon: 'mdi-chart-bell-curve-cumulative', color: 'warning' },
|
||||
calendar: { label: 'date', icon: 'mdi-calendar', color: undefined }
|
||||
};
|
||||
// Only badge when a dynamic signal is active (meters present or lifespan compression applied).
|
||||
const hasMeters = pm.meters && pm.meters.length;
|
||||
const compressed = pm.signals && pm.signals.factor != null && pm.signals.factor < 1;
|
||||
if (!hasMeters && !compressed) return null;
|
||||
return map[pm.due_driver] || null;
|
||||
},
|
||||
resetPmForm() {
|
||||
this.pmForm = {
|
||||
plan_id: null,
|
||||
@@ -956,8 +1022,31 @@ export default {
|
||||
this.completeRatings = { safety: 0, physical: 0, operating: 0 };
|
||||
this.completeSignature = '';
|
||||
this.completePhotos = [];
|
||||
this.completeMeters = (pm.meters || []).map((m) => ({
|
||||
asset_pm_meter_id: m.id, label: m.label, unit: m.unit,
|
||||
last_reading: m.last_reading, due_at_reading: m.due_at_reading, reading: ''
|
||||
}));
|
||||
this.completeDialog = true;
|
||||
},
|
||||
openLogReading(pm) {
|
||||
this.logTarget = pm;
|
||||
this.logForm = {
|
||||
asset_pm_meter_id: (pm.meters && pm.meters[0]) ? pm.meters[0].id : null,
|
||||
reading: '',
|
||||
reading_date: new Date().toISOString().slice(0, 10)
|
||||
};
|
||||
this.logDialog = true;
|
||||
},
|
||||
submitLogReading() {
|
||||
this.$emit('log-reading', {
|
||||
apId: this.logTarget.id,
|
||||
asset_pm_meter_id: this.logForm.asset_pm_meter_id,
|
||||
reading: Number(this.logForm.reading),
|
||||
reading_date: this.logForm.reading_date,
|
||||
source: 'manual'
|
||||
});
|
||||
this.logDialog = false;
|
||||
},
|
||||
onCompletePhotos(event) {
|
||||
for (const file of Array.from(event.target.files || [])) {
|
||||
const reader = new FileReader();
|
||||
@@ -975,7 +1064,10 @@ export default {
|
||||
notes_list: this.completeNotesList.map((n) => String(n || '').trim()).filter(Boolean),
|
||||
ratings: { ...this.completeRatings },
|
||||
signature: this.completeSignature,
|
||||
photos: this.completePhotos
|
||||
photos: this.completePhotos,
|
||||
meter_readings: this.completeMeters
|
||||
.filter((m) => m.reading !== '' && m.reading != null)
|
||||
.map((m) => ({ asset_pm_meter_id: m.asset_pm_meter_id, reading: Number(m.reading) }))
|
||||
});
|
||||
this.completeDialog = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user