220 lines
8.3 KiB
Vue
220 lines
8.3 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Printer Drivers</h2>
|
|
<label class="show-inactive"><input type="checkbox" v-model="showInactive" /> Show inactive</label>
|
|
<button class="btn btn-primary" @click="openModal()">+ Add Driver</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<p class="hint">
|
|
Each driver is a name + a link to the driver package - an SMB path
|
|
(<code>\\server\share\driver</code>) or an HTTP URL. HTTP links open;
|
|
SMB paths are shown for copy-paste (browsers block file:// SMB).
|
|
</p>
|
|
<div v-if="loading" class="muted">Loading...</div>
|
|
<template v-else>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Printer Model</th>
|
|
<th>Location</th>
|
|
<th>Description</th>
|
|
<th>Active</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="d in visibleItems" :key="d.driverid">
|
|
<td><strong>{{ d.name }}</strong></td>
|
|
<td>{{ d.modelname || '-' }}</td>
|
|
<td>
|
|
<a v-if="isHttp(d.location)" :href="d.location" target="_blank" class="mono">{{ d.location }}</a>
|
|
<span v-else class="mono">{{ d.location }}</span>
|
|
</td>
|
|
<td class="cell-truncate" :title="d.description">{{ d.description || '-' }}</td>
|
|
<td>
|
|
<span class="badge" :class="d.isactive ? 'badge-success' : 'badge-secondary'">{{ d.isactive ? 'yes' : 'no' }}</span>
|
|
</td>
|
|
<td class="actions">
|
|
<button class="btn btn-secondary btn-sm" @click="openModal(d)">Edit</button>
|
|
<button class="btn btn-danger btn-sm" @click="deleteDriver(d)">Delete</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="visibleItems.length === 0">
|
|
<td colspan="6" style="text-align: center; color: var(--text-light);">No drivers</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="showModal" class="modal-overlay">
|
|
<div class="modal">
|
|
<div class="modal-header"><h3>{{ editing ? 'Edit' : 'Add' }} Driver</h3></div>
|
|
<form @submit.prevent="save">
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label>Name *</label>
|
|
<input v-model="form.name" type="text" class="form-control" maxlength="150" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>
|
|
INF Driver Name
|
|
<span class="hint">(the exact string the INF declares - what
|
|
Add-PrinterDriver matches on. Blank means the converger
|
|
cannot install this driver.)</span>
|
|
</label>
|
|
<input v-model="form.drivername" type="text" class="form-control"
|
|
maxlength="255"
|
|
placeholder="HP Universal Printing PCL 6" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Vendor <span class="hint">(a universal driver serves every printer of that make; a model below wins over it)</span></label>
|
|
<select v-model="form.vendorid" class="form-control">
|
|
<option :value="null">-- none --</option>
|
|
<option v-for="v in vendors" :key="v.vendorid" :value="v.vendorid">
|
|
{{ v.vendor }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Printer Model <span class="hint">(links driver to a model so it shows on matching printers)</span></label>
|
|
<select v-model="form.modelnumberid" class="form-control">
|
|
<option :value="null">-- none --</option>
|
|
<option v-for="m in models" :key="m.modelnumberid" :value="m.modelnumberid">
|
|
{{ m.modelnumber }}<template v-if="m.vendorname"> ({{ m.vendorname }})</template>
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Location * <span class="hint">(SMB path or HTTP URL)</span></label>
|
|
<input v-model="form.location" type="text" class="form-control" maxlength="500"
|
|
placeholder="\\server\share\driver or https://..." required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Description</label>
|
|
<textarea v-model="form.description" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="checkbox-label"><input type="checkbox" v-model="form.isactive" /> Active</label>
|
|
</div>
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { printersApi, vendorsApi } from '@/api'
|
|
import { useToast } from '@/composables/toast'
|
|
import { apiError } from '@/utils/apiError'
|
|
const toast = useToast()
|
|
|
|
const items = ref([])
|
|
const models = ref([])
|
|
const vendors = ref([])
|
|
const showInactive = ref(false)
|
|
const visibleItems = computed(() => showInactive.value ? items.value : items.value.filter(x => x.isactive !== false))
|
|
const loading = ref(true)
|
|
const showModal = ref(false)
|
|
const editing = ref(null)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
const form = ref({ name: '', location: '', description: '', drivername: '', vendorid: null, modelnumberid: null, isactive: true })
|
|
|
|
function isHttp(loc) {
|
|
return typeof loc === 'string' && /^https?:\/\//i.test(loc)
|
|
}
|
|
|
|
onMounted(() => { loadData(); loadModels(); loadVendors() })
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const response = await printersApi.drivers.list({ active: false })
|
|
items.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading drivers:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadModels() {
|
|
try {
|
|
const response = await printersApi.modelSupplies.listModels({ perpage: 100 })
|
|
models.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading models:', err)
|
|
}
|
|
}
|
|
|
|
async function loadVendors() {
|
|
try {
|
|
// A universal driver (HP UPD, Xerox GPD) attaches to the whole make rather
|
|
// than one model, which is what vendorid is for. listAll, because the
|
|
// picker must show every vendor - perpage is clamped at 100.
|
|
const response = await vendorsApi.list({ perpage: 100 })
|
|
vendors.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading vendors:', err)
|
|
}
|
|
}
|
|
|
|
function openModal(item = null) {
|
|
editing.value = item
|
|
form.value = item
|
|
? { name: item.name || '', location: item.location || '', description: item.description || '', drivername: item.drivername || '', vendorid: item.vendorid || null, modelnumberid: item.modelnumberid || null, isactive: item.isactive !== false }
|
|
: { name: '', location: '', description: '', drivername: '', vendorid: null, modelnumberid: null, isactive: true }
|
|
error.value = ''
|
|
showModal.value = true
|
|
}
|
|
|
|
function closeModal() { showModal.value = false; editing.value = null }
|
|
|
|
async function save() {
|
|
error.value = ''
|
|
saving.value = true
|
|
try {
|
|
if (editing.value) {
|
|
await printersApi.drivers.update(editing.value.driverid, form.value)
|
|
} else {
|
|
await printersApi.drivers.create(form.value)
|
|
}
|
|
closeModal()
|
|
loadData()
|
|
} catch (err) {
|
|
error.value = apiError(err, 'Failed to save')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function deleteDriver(d) {
|
|
if (!confirm(`Delete driver "${d.name}"?`)) return
|
|
try {
|
|
await printersApi.drivers.delete(d.driverid)
|
|
loadData()
|
|
} catch (err) {
|
|
toast.error(apiError(err, 'Failed to delete'))
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.hint { color: var(--text-light); font-size: 0.9rem; margin: 0 0 14px; }
|
|
.muted { color: var(--text-light); }
|
|
.mono { font-family: monospace; font-size: 0.85rem; word-break: break-all; }
|
|
</style>
|