feat: Sistema de gestión de impresoras persistente
- Crear modelo Printer con campos: id, name, host, deviceId, timeout, isDefault - Almacenamiento persistente en data/printers.json - APIs CRUD: GET/POST /api/printers, GET/PUT/DELETE /api/printers/:id - API para seleccionar impresora activa: POST /api/printers/select - Endpoint de impresión ahora usa la impresora seleccionada o la especificada por printerId - Composable usePrinters() para el cliente - UI: Selector de impresora en sidebar, tab Impresoras en mobile - Componentes: PrintersList, PrintersCard, PrintersForm, PrintersSelector
This commit is contained in:
@@ -18,6 +18,11 @@ const tabs = computed(() => [
|
|||||||
label: 'Templates',
|
label: 'Templates',
|
||||||
value: 'templates',
|
value: 'templates',
|
||||||
icon: 'i-heroicons-document-duplicate'
|
icon: 'i-heroicons-document-duplicate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Impresoras',
|
||||||
|
value: 'printers',
|
||||||
|
icon: 'i-heroicons-printer'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
89
app/components/printers/Card.vue
Normal file
89
app/components/printers/Card.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Printer } from '~/composables/usePrinters'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
printer: Printer
|
||||||
|
isSelected: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: []
|
||||||
|
edit: []
|
||||||
|
delete: []
|
||||||
|
setDefault: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const menuItems = computed(() => [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Editar',
|
||||||
|
icon: 'i-heroicons-pencil-square',
|
||||||
|
click: () => emit('edit')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: props.printer.isDefault ? 'Es predeterminada' : 'Hacer predeterminada',
|
||||||
|
icon: 'i-heroicons-star',
|
||||||
|
disabled: props.printer.isDefault,
|
||||||
|
click: () => emit('setDefault')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Eliminar',
|
||||||
|
icon: 'i-heroicons-trash',
|
||||||
|
color: 'error' as const,
|
||||||
|
click: () => emit('delete')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCard
|
||||||
|
:class="[
|
||||||
|
'cursor-pointer transition-all',
|
||||||
|
isSelected ? 'ring-2 ring-primary-500' : 'hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||||
|
]"
|
||||||
|
@click="emit('select')"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ printer.name }}
|
||||||
|
</h3>
|
||||||
|
<UBadge v-if="printer.isDefault" color="primary" variant="subtle" size="xs">
|
||||||
|
Default
|
||||||
|
</UBadge>
|
||||||
|
<UBadge v-if="isSelected" color="success" variant="subtle" size="xs">
|
||||||
|
Seleccionada
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 space-y-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-globe-alt" class="w-4 h-4" />
|
||||||
|
<span>{{ printer.host }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-identification" class="w-4 h-4" />
|
||||||
|
<span>{{ printer.deviceId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-clock" class="w-4 h-4" />
|
||||||
|
<span>{{ printer.timeout }}ms timeout</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UDropdownMenu :items="menuItems" @click.stop>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-ellipsis-vertical"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</UDropdownMenu>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
114
app/components/printers/Form.vue
Normal file
114
app/components/printers/Form.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Printer } from '~/composables/usePrinters'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
printer?: Printer | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [data: {
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout: number
|
||||||
|
isDefault: boolean
|
||||||
|
}]
|
||||||
|
cancel: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
name: props.printer?.name || '',
|
||||||
|
host: props.printer?.host || '',
|
||||||
|
deviceId: props.printer?.deviceId || '',
|
||||||
|
timeout: props.printer?.timeout || 60000,
|
||||||
|
isDefault: props.printer?.isDefault || false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Actualizar form cuando cambia el printer
|
||||||
|
watch(() => props.printer, (newPrinter) => {
|
||||||
|
if (newPrinter) {
|
||||||
|
form.name = newPrinter.name
|
||||||
|
form.host = newPrinter.host
|
||||||
|
form.deviceId = newPrinter.deviceId
|
||||||
|
form.timeout = newPrinter.timeout
|
||||||
|
form.isDefault = newPrinter.isDefault
|
||||||
|
} else {
|
||||||
|
form.name = ''
|
||||||
|
form.host = ''
|
||||||
|
form.deviceId = ''
|
||||||
|
form.timeout = 60000
|
||||||
|
form.isDefault = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (!form.name || !form.host || !form.deviceId) return
|
||||||
|
|
||||||
|
emit('submit', {
|
||||||
|
name: form.name,
|
||||||
|
host: form.host,
|
||||||
|
deviceId: form.deviceId,
|
||||||
|
timeout: form.timeout,
|
||||||
|
isDefault: form.isDefault
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
|
<UFormField label="Nombre" required>
|
||||||
|
<UInput
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="Ej: Impresora Cocina"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Host / IP" required>
|
||||||
|
<UInput
|
||||||
|
v-model="form.host"
|
||||||
|
placeholder="Ej: 192.168.1.100"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Device ID" required>
|
||||||
|
<UInput
|
||||||
|
v-model="form.deviceId"
|
||||||
|
placeholder="Ej: local_printer"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Timeout (ms)">
|
||||||
|
<UInput
|
||||||
|
v-model.number="form.timeout"
|
||||||
|
type="number"
|
||||||
|
placeholder="60000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UCheckbox
|
||||||
|
v-model="form.isDefault"
|
||||||
|
label="Establecer como impresora predeterminada"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-4">
|
||||||
|
<UButton
|
||||||
|
type="button"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
@click="emit('cancel')"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
:disabled="!form.name || !form.host || !form.deviceId"
|
||||||
|
>
|
||||||
|
{{ printer ? 'Guardar cambios' : 'Agregar impresora' }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
132
app/components/printers/List.vue
Normal file
132
app/components/printers/List.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const printers = usePrinters()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const showDrawer = ref(false)
|
||||||
|
const editingPrinter = ref<typeof printers.printers.value[0] | null>(null)
|
||||||
|
|
||||||
|
// Cargar impresoras al montar
|
||||||
|
onMounted(() => {
|
||||||
|
printers.fetchPrinters()
|
||||||
|
})
|
||||||
|
|
||||||
|
function openNewPrinter() {
|
||||||
|
editingPrinter.value = null
|
||||||
|
showDrawer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditPrinter(printer: typeof printers.printers.value[0]) {
|
||||||
|
editingPrinter.value = printer
|
||||||
|
showDrawer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(data: {
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout: number
|
||||||
|
isDefault: boolean
|
||||||
|
}) {
|
||||||
|
if (editingPrinter.value) {
|
||||||
|
const result = await printers.updatePrinter(editingPrinter.value.id, data)
|
||||||
|
if (result) {
|
||||||
|
toast.add({ title: 'Impresora actualizada', color: 'success' })
|
||||||
|
showDrawer.value = false
|
||||||
|
} else {
|
||||||
|
toast.add({ title: printers.error.value || 'Error', color: 'error' })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await printers.createPrinter(data)
|
||||||
|
if (result) {
|
||||||
|
toast.add({ title: 'Impresora agregada', color: 'success' })
|
||||||
|
showDrawer.value = false
|
||||||
|
} else {
|
||||||
|
toast.add({ title: printers.error.value || 'Error', color: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSelect(printer: typeof printers.printers.value[0]) {
|
||||||
|
await printers.selectPrinter(printer.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetDefault(printer: typeof printers.printers.value[0]) {
|
||||||
|
const result = await printers.updatePrinter(printer.id, { isDefault: true })
|
||||||
|
if (result) {
|
||||||
|
toast.add({ title: 'Impresora predeterminada actualizada', color: 'success' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(printer: typeof printers.printers.value[0]) {
|
||||||
|
if (!confirm(`¿Eliminar la impresora "${printer.name}"?`)) return
|
||||||
|
|
||||||
|
const result = await printers.deletePrinter(printer.id)
|
||||||
|
if (result) {
|
||||||
|
toast.add({ title: 'Impresora eliminada', color: 'success' })
|
||||||
|
} else {
|
||||||
|
toast.add({ title: printers.error.value || 'Error', color: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Impresoras
|
||||||
|
</h2>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
@click="openNewPrinter"
|
||||||
|
>
|
||||||
|
Agregar
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="printers.loading.value" class="flex justify-center py-8">
|
||||||
|
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="printers.printers.value.length === 0" class="text-center py-8">
|
||||||
|
<UIcon name="i-heroicons-printer" class="w-12 h-12 text-gray-400 mx-auto mb-2" />
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
No hay impresoras configuradas
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-400 dark:text-gray-500 mb-4">
|
||||||
|
Agrega una impresora para comenzar a imprimir
|
||||||
|
</p>
|
||||||
|
<UButton @click="openNewPrinter">
|
||||||
|
Agregar primera impresora
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<PrintersCard
|
||||||
|
v-for="printer in printers.printers.value"
|
||||||
|
:key="printer.id"
|
||||||
|
:printer="printer"
|
||||||
|
:is-selected="printer.id === printers.selectedPrinterId.value"
|
||||||
|
@select="handleSelect(printer)"
|
||||||
|
@edit="openEditPrinter(printer)"
|
||||||
|
@delete="handleDelete(printer)"
|
||||||
|
@set-default="handleSetDefault(printer)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UDrawer v-model:open="showDrawer">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">
|
||||||
|
{{ editingPrinter ? 'Editar impresora' : 'Nueva impresora' }}
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<PrintersForm
|
||||||
|
:printer="editingPrinter"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="showDrawer = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UDrawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
36
app/components/printers/Selector.vue
Normal file
36
app/components/printers/Selector.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const printers = usePrinters()
|
||||||
|
|
||||||
|
// Cargar impresoras al montar
|
||||||
|
onMounted(() => {
|
||||||
|
printers.fetchPrinters()
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = computed(() =>
|
||||||
|
printers.printers.value.map(p => ({
|
||||||
|
label: p.name + (p.isDefault ? ' (default)' : ''),
|
||||||
|
value: p.id
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
async function handleChange(value: string) {
|
||||||
|
await printers.selectPrinter(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-printer" class="w-5 h-5 text-gray-500" />
|
||||||
|
<USelect
|
||||||
|
v-if="printers.printers.value.length > 0"
|
||||||
|
:model-value="printers.selectedPrinterId.value"
|
||||||
|
:items="options"
|
||||||
|
placeholder="Seleccionar impresora"
|
||||||
|
class="min-w-[200px]"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-sm text-gray-500">
|
||||||
|
No hay impresoras configuradas
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
203
app/composables/usePrinters.ts
Normal file
203
app/composables/usePrinters.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// Composable para gestión de impresoras
|
||||||
|
export interface Printer {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout: number
|
||||||
|
isDefault: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const printers = ref<Printer[]>([])
|
||||||
|
const selectedPrinterId = ref<string | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
export function usePrinters() {
|
||||||
|
const selectedPrinter = computed(() =>
|
||||||
|
printers.value.find(p => p.id === selectedPrinterId.value) || null
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPrinter = computed(() =>
|
||||||
|
printers.value.find(p => p.isDefault) || printers.value[0] || null
|
||||||
|
)
|
||||||
|
|
||||||
|
async function fetchPrinters() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch<{
|
||||||
|
ok: boolean
|
||||||
|
printers: Printer[]
|
||||||
|
selectedPrinterId: string | null
|
||||||
|
error?: string
|
||||||
|
}>('/api/printers')
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
printers.value = response.printers
|
||||||
|
selectedPrinterId.value = response.selectedPrinterId
|
||||||
|
} else {
|
||||||
|
error.value = response.error || 'Error al cargar impresoras'
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPrinter(data: {
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout?: number
|
||||||
|
isDefault?: boolean
|
||||||
|
}) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch<{
|
||||||
|
ok: boolean
|
||||||
|
printer?: Printer
|
||||||
|
error?: string
|
||||||
|
}>('/api/printers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok && response.printer) {
|
||||||
|
printers.value.push(response.printer)
|
||||||
|
// Si es la primera impresora, seleccionarla
|
||||||
|
if (printers.value.length === 1) {
|
||||||
|
selectedPrinterId.value = response.printer.id
|
||||||
|
}
|
||||||
|
return response.printer
|
||||||
|
} else {
|
||||||
|
error.value = response.error || 'Error al crear impresora'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePrinter(id: string, data: Partial<{
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout: number
|
||||||
|
isDefault: boolean
|
||||||
|
}>) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch<{
|
||||||
|
ok: boolean
|
||||||
|
printer?: Printer
|
||||||
|
error?: string
|
||||||
|
}>(`/api/printers/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok && response.printer) {
|
||||||
|
const index = printers.value.findIndex(p => p.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
// Si se estableció como default, quitar default de las demás
|
||||||
|
if (data.isDefault) {
|
||||||
|
printers.value.forEach(p => p.isDefault = false)
|
||||||
|
}
|
||||||
|
printers.value[index] = response.printer
|
||||||
|
}
|
||||||
|
return response.printer
|
||||||
|
} else {
|
||||||
|
error.value = response.error || 'Error al actualizar impresora'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePrinter(id: string) {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch<{
|
||||||
|
ok: boolean
|
||||||
|
error?: string
|
||||||
|
}>(`/api/printers/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const index = printers.value.findIndex(p => p.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
printers.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
// Si era la seleccionada, seleccionar otra
|
||||||
|
if (selectedPrinterId.value === id) {
|
||||||
|
selectedPrinterId.value = printers.value[0]?.id || null
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
error.value = response.error || 'Error al eliminar impresora'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectPrinter(printerId: string | null) {
|
||||||
|
try {
|
||||||
|
const response = await $fetch<{
|
||||||
|
ok: boolean
|
||||||
|
error?: string
|
||||||
|
}>('/api/printers/select', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { printerId }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
selectedPrinterId.value = printerId
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
error.value = response.error || 'Error al seleccionar impresora'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
printers,
|
||||||
|
selectedPrinterId,
|
||||||
|
selectedPrinter,
|
||||||
|
defaultPrinter,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchPrinters,
|
||||||
|
createPrinter,
|
||||||
|
updatePrinter,
|
||||||
|
deletePrinter,
|
||||||
|
selectPrinter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
const activeTab = ref('constructor')
|
const activeTab = ref('constructor')
|
||||||
const isDesktop = useMediaQuery('(min-width: 768px)')
|
const isDesktop = useMediaQuery('(min-width: 768px)')
|
||||||
const queue = usePrintQueue()
|
const queue = usePrintQueue()
|
||||||
|
const printers = usePrinters()
|
||||||
|
|
||||||
|
// Cargar impresoras al iniciar
|
||||||
|
onMounted(() => {
|
||||||
|
printers.fetchPrinters()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -23,6 +29,11 @@ const queue = usePrintQueue()
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Selector de impresora -->
|
||||||
|
<div class="p-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<PrintersSelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4">
|
<div class="flex-1 overflow-y-auto p-4">
|
||||||
<QueuePrintQueue />
|
<QueuePrintQueue />
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +61,10 @@ const queue = usePrintQueue()
|
|||||||
<template v-else-if="activeTab === 'templates'">
|
<template v-else-if="activeTab === 'templates'">
|
||||||
<TemplatesTemplateList />
|
<TemplatesTemplateList />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="activeTab === 'printers'">
|
||||||
|
<PrintersList />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
// Endpoint genérico de impresión que acepta una lista de operaciones
|
// Endpoint genérico de impresión que acepta una lista de operaciones
|
||||||
import { buildFromOperations, type Operation } from '../../utils/eposBuilder'
|
import { buildFromOperations, type Operation } from '../../utils/eposBuilder'
|
||||||
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '../../utils/printer'
|
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '../../utils/printer'
|
||||||
|
import { getSelectedPrinter, getPrinterById } from '../../utils/printers'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
operations = [],
|
operations = [],
|
||||||
dryRun = false
|
dryRun = false,
|
||||||
|
printerId
|
||||||
} = body as {
|
} = body as {
|
||||||
operations?: Operation[]
|
operations?: Operation[]
|
||||||
dryRun?: boolean
|
dryRun?: boolean
|
||||||
|
printerId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construir el XML interior con las operaciones
|
// Construir el XML interior con las operaciones
|
||||||
@@ -28,12 +30,24 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obtener la impresora a usar
|
||||||
|
let printer = printerId
|
||||||
|
? await getPrinterById(printerId)
|
||||||
|
: await getSelectedPrinter()
|
||||||
|
|
||||||
|
if (!printer) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'No hay impresora configurada. Por favor, agrega una impresora primero.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Enviar a la impresora
|
// Enviar a la impresora
|
||||||
const result = await sendToPrinter(
|
const result = await sendToPrinter(
|
||||||
soap,
|
soap,
|
||||||
config.printerHost,
|
printer.host,
|
||||||
config.printerDeviceId,
|
printer.deviceId,
|
||||||
parseInt(config.printerTimeoutMs)
|
printer.timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parsear la respuesta
|
// Parsear la respuesta
|
||||||
@@ -43,7 +57,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
ok: success,
|
ok: success,
|
||||||
httpStatus: result.status,
|
httpStatus: result.status,
|
||||||
code,
|
code,
|
||||||
raw: result.data
|
raw: result.data,
|
||||||
|
printerUsed: {
|
||||||
|
id: printer.id,
|
||||||
|
name: printer.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
34
server/api/printers/[id].delete.ts
Normal file
34
server/api/printers/[id].delete.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// DELETE /api/printers/:id - Eliminar una impresora
|
||||||
|
import { deletePrinter } from '../../utils/printers'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const id = getRouterParam(event, 'id')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'ID requerido'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await deletePrinter(id)
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'Impresora no encontrada'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
message: 'Impresora eliminada'
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
34
server/api/printers/[id].get.ts
Normal file
34
server/api/printers/[id].get.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// GET /api/printers/:id - Obtener una impresora por ID
|
||||||
|
import { getPrinterById } from '../../utils/printers'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const id = getRouterParam(event, 'id')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'ID requerido'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const printer = await getPrinterById(id)
|
||||||
|
|
||||||
|
if (!printer) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'Impresora no encontrada'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
printer
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
50
server/api/printers/[id].put.ts
Normal file
50
server/api/printers/[id].put.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// PUT /api/printers/:id - Actualizar una impresora
|
||||||
|
import { updatePrinter } from '../../utils/printers'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const id = getRouterParam(event, 'id')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'ID requerido'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
const { name, host, deviceId, timeout, isDefault } = body as {
|
||||||
|
name?: string
|
||||||
|
host?: string
|
||||||
|
deviceId?: string
|
||||||
|
timeout?: number
|
||||||
|
isDefault?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const printer = await updatePrinter(id, {
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
deviceId,
|
||||||
|
timeout,
|
||||||
|
isDefault
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!printer) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'Impresora no encontrada'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
printer
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
20
server/api/printers/index.get.ts
Normal file
20
server/api/printers/index.get.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// GET /api/printers - Listar todas las impresoras
|
||||||
|
import { getAllPrinters, getSelectedPrinter } from '../../utils/printers'
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
try {
|
||||||
|
const printers = await getAllPrinters()
|
||||||
|
const selected = await getSelectedPrinter()
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
printers,
|
||||||
|
selectedPrinterId: selected?.id || null
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
41
server/api/printers/index.post.ts
Normal file
41
server/api/printers/index.post.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// POST /api/printers - Crear nueva impresora
|
||||||
|
import { createPrinter } from '../../utils/printers'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
const { name, host, deviceId, timeout, isDefault } = body as {
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout?: number
|
||||||
|
isDefault?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || !host || !deviceId) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'name, host y deviceId son requeridos'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const printer = await createPrinter({
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
deviceId,
|
||||||
|
timeout,
|
||||||
|
isDefault
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
printer
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
31
server/api/printers/select.post.ts
Normal file
31
server/api/printers/select.post.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// POST /api/printers/select - Seleccionar la impresora activa
|
||||||
|
import { setSelectedPrinter, getPrinterById } from '../../utils/printers'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event)
|
||||||
|
const { printerId } = body as { printerId: string | null }
|
||||||
|
|
||||||
|
if (printerId) {
|
||||||
|
const printer = await getPrinterById(printerId)
|
||||||
|
if (!printer) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'Impresora no encontrada'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await setSelectedPrinter(printerId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
selectedPrinterId: printerId
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
178
server/utils/printers.ts
Normal file
178
server/utils/printers.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// Gestión de impresoras persistentes
|
||||||
|
import { readFile, writeFile, mkdir } from 'fs/promises'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
export interface Printer {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout: number
|
||||||
|
isDefault: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrintersStore {
|
||||||
|
printers: Printer[]
|
||||||
|
selectedPrinterId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directorio de datos persistentes
|
||||||
|
const DATA_DIR = join(process.cwd(), 'data')
|
||||||
|
const PRINTERS_FILE = join(DATA_DIR, 'printers.json')
|
||||||
|
|
||||||
|
// Asegurar que el directorio existe
|
||||||
|
async function ensureDataDir(): Promise<void> {
|
||||||
|
if (!existsSync(DATA_DIR)) {
|
||||||
|
await mkdir(DATA_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leer store de impresoras
|
||||||
|
export async function readPrintersStore(): Promise<PrintersStore> {
|
||||||
|
await ensureDataDir()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await readFile(PRINTERS_FILE, 'utf-8')
|
||||||
|
return JSON.parse(data)
|
||||||
|
} catch {
|
||||||
|
// Si no existe el archivo, retornar store vacío
|
||||||
|
return {
|
||||||
|
printers: [],
|
||||||
|
selectedPrinterId: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar store de impresoras
|
||||||
|
export async function writePrintersStore(store: PrintersStore): Promise<void> {
|
||||||
|
await ensureDataDir()
|
||||||
|
await writeFile(PRINTERS_FILE, JSON.stringify(store, null, 2), 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generar ID único
|
||||||
|
function generateId(): string {
|
||||||
|
return `printer_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRUD Operations
|
||||||
|
|
||||||
|
export async function getAllPrinters(): Promise<Printer[]> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
return store.printers
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPrinterById(id: string): Promise<Printer | null> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
return store.printers.find(p => p.id === id) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDefaultPrinter(): Promise<Printer | null> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
return store.printers.find(p => p.isDefault) || store.printers[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSelectedPrinter(): Promise<Printer | null> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
if (store.selectedPrinterId) {
|
||||||
|
const printer = store.printers.find(p => p.id === store.selectedPrinterId)
|
||||||
|
if (printer) return printer
|
||||||
|
}
|
||||||
|
// Fallback a la impresora por defecto
|
||||||
|
return getDefaultPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setSelectedPrinter(printerId: string | null): Promise<void> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
store.selectedPrinterId = printerId
|
||||||
|
await writePrintersStore(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPrinter(data: {
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout?: number
|
||||||
|
isDefault?: boolean
|
||||||
|
}): Promise<Printer> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const printer: Printer = {
|
||||||
|
id: generateId(),
|
||||||
|
name: data.name,
|
||||||
|
host: data.host,
|
||||||
|
deviceId: data.deviceId,
|
||||||
|
timeout: data.timeout || 60000,
|
||||||
|
isDefault: data.isDefault || store.printers.length === 0, // Primera impresora es default
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si esta es default, quitar default de las demás
|
||||||
|
if (printer.isDefault) {
|
||||||
|
store.printers.forEach(p => p.isDefault = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.printers.push(printer)
|
||||||
|
|
||||||
|
// Si es la primera impresora, seleccionarla
|
||||||
|
if (store.printers.length === 1) {
|
||||||
|
store.selectedPrinterId = printer.id
|
||||||
|
}
|
||||||
|
|
||||||
|
await writePrintersStore(store)
|
||||||
|
return printer
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePrinter(id: string, data: Partial<{
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
deviceId: string
|
||||||
|
timeout: number
|
||||||
|
isDefault: boolean
|
||||||
|
}>): Promise<Printer | null> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
const index = store.printers.findIndex(p => p.id === id)
|
||||||
|
|
||||||
|
if (index === -1) return null
|
||||||
|
|
||||||
|
// Si se está estableciendo como default, quitar default de las demás
|
||||||
|
if (data.isDefault) {
|
||||||
|
store.printers.forEach(p => p.isDefault = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.printers[index] = {
|
||||||
|
...store.printers[index],
|
||||||
|
...data,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
await writePrintersStore(store)
|
||||||
|
return store.printers[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePrinter(id: string): Promise<boolean> {
|
||||||
|
const store = await readPrintersStore()
|
||||||
|
const index = store.printers.findIndex(p => p.id === id)
|
||||||
|
|
||||||
|
if (index === -1) return false
|
||||||
|
|
||||||
|
const wasDefault = store.printers[index].isDefault
|
||||||
|
store.printers.splice(index, 1)
|
||||||
|
|
||||||
|
// Si era la impresora seleccionada, limpiar selección
|
||||||
|
if (store.selectedPrinterId === id) {
|
||||||
|
store.selectedPrinterId = store.printers[0]?.id || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si era default y hay otras impresoras, hacer la primera default
|
||||||
|
if (wasDefault && store.printers.length > 0) {
|
||||||
|
store.printers[0].isDefault = true
|
||||||
|
}
|
||||||
|
|
||||||
|
await writePrintersStore(store)
|
||||||
|
return true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user