From e97b2b4d8ee8737e19aadb9cc21d4340b149a326 Mon Sep 17 00:00:00 2001
From: josedario87
Date: Tue, 25 Nov 2025 00:44:50 -0600
Subject: [PATCH] =?UTF-8?q?feat:=20Sistema=20de=20gesti=C3=B3n=20de=20impr?=
=?UTF-8?q?esoras=20persistente?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
app/components/layout/MobileNavigation.vue | 5 +
app/components/printers/Card.vue | 89 +++++++++
app/components/printers/Form.vue | 114 ++++++++++++
app/components/printers/List.vue | 132 ++++++++++++++
app/components/printers/Selector.vue | 36 ++++
app/composables/usePrinters.ts | 203 +++++++++++++++++++++
app/pages/index.vue | 15 ++
server/api/print/index.post.ts | 30 ++-
server/api/printers/[id].delete.ts | 34 ++++
server/api/printers/[id].get.ts | 34 ++++
server/api/printers/[id].put.ts | 50 +++++
server/api/printers/index.get.ts | 20 ++
server/api/printers/index.post.ts | 41 +++++
server/api/printers/select.post.ts | 31 ++++
server/utils/printers.ts | 178 ++++++++++++++++++
15 files changed, 1006 insertions(+), 6 deletions(-)
create mode 100644 app/components/printers/Card.vue
create mode 100644 app/components/printers/Form.vue
create mode 100644 app/components/printers/List.vue
create mode 100644 app/components/printers/Selector.vue
create mode 100644 app/composables/usePrinters.ts
create mode 100644 server/api/printers/[id].delete.ts
create mode 100644 server/api/printers/[id].get.ts
create mode 100644 server/api/printers/[id].put.ts
create mode 100644 server/api/printers/index.get.ts
create mode 100644 server/api/printers/index.post.ts
create mode 100644 server/api/printers/select.post.ts
create mode 100644 server/utils/printers.ts
diff --git a/app/components/layout/MobileNavigation.vue b/app/components/layout/MobileNavigation.vue
index e555941..dc9a7e2 100644
--- a/app/components/layout/MobileNavigation.vue
+++ b/app/components/layout/MobileNavigation.vue
@@ -18,6 +18,11 @@ const tabs = computed(() => [
label: 'Templates',
value: 'templates',
icon: 'i-heroicons-document-duplicate'
+ },
+ {
+ label: 'Impresoras',
+ value: 'printers',
+ icon: 'i-heroicons-printer'
}
])
diff --git a/app/components/printers/Card.vue b/app/components/printers/Card.vue
new file mode 100644
index 0000000..a160b5d
--- /dev/null
+++ b/app/components/printers/Card.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+ {{ printer.name }}
+
+
+ Default
+
+
+ Seleccionada
+
+
+
+
+
+
+ {{ printer.host }}
+
+
+
+ {{ printer.deviceId }}
+
+
+
+ {{ printer.timeout }}ms timeout
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/printers/Form.vue b/app/components/printers/Form.vue
new file mode 100644
index 0000000..c03808b
--- /dev/null
+++ b/app/components/printers/Form.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
diff --git a/app/components/printers/List.vue b/app/components/printers/List.vue
new file mode 100644
index 0000000..d0bb170
--- /dev/null
+++ b/app/components/printers/List.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+ Impresoras
+
+
+ Agregar
+
+
+
+
+
+
+
+
+
+
+ No hay impresoras configuradas
+
+
+ Agrega una impresora para comenzar a imprimir
+
+
+ Agregar primera impresora
+
+
+
+
+
+
+
+
+ {{ editingPrinter ? 'Editar impresora' : 'Nueva impresora' }}
+
+
+
+
+
+
+
diff --git a/app/components/printers/Selector.vue b/app/components/printers/Selector.vue
new file mode 100644
index 0000000..07de353
--- /dev/null
+++ b/app/components/printers/Selector.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ No hay impresoras configuradas
+
+
+
diff --git a/app/composables/usePrinters.ts b/app/composables/usePrinters.ts
new file mode 100644
index 0000000..7900180
--- /dev/null
+++ b/app/composables/usePrinters.ts
@@ -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([])
+const selectedPrinterId = ref(null)
+const loading = ref(false)
+const error = ref(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
+ }
+}
diff --git a/app/pages/index.vue b/app/pages/index.vue
index acb6181..9757584 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -2,6 +2,12 @@
const activeTab = ref('constructor')
const isDesktop = useMediaQuery('(min-width: 768px)')
const queue = usePrintQueue()
+const printers = usePrinters()
+
+// Cargar impresoras al iniciar
+onMounted(() => {
+ printers.fetchPrinters()
+})
@@ -23,6 +29,11 @@ const queue = usePrintQueue()
+
+
+
@@ -50,6 +61,10 @@ const queue = usePrintQueue()
+
+
+
+
diff --git a/server/api/print/index.post.ts b/server/api/print/index.post.ts
index f4ff1dc..41ef0e0 100644
--- a/server/api/print/index.post.ts
+++ b/server/api/print/index.post.ts
@@ -1,18 +1,20 @@
// Endpoint genérico de impresión que acepta una lista de operaciones
import { buildFromOperations, type Operation } from '../../utils/eposBuilder'
import { buildSoapEnvelope, sendToPrinter, parsePrinterResponse } from '../../utils/printer'
+import { getSelectedPrinter, getPrinterById } from '../../utils/printers'
export default defineEventHandler(async (event) => {
try {
- const config = useRuntimeConfig()
const body = await readBody(event)
const {
operations = [],
- dryRun = false
+ dryRun = false,
+ printerId
} = body as {
operations?: Operation[]
dryRun?: boolean
+ printerId?: string
}
// 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
const result = await sendToPrinter(
soap,
- config.printerHost,
- config.printerDeviceId,
- parseInt(config.printerTimeoutMs)
+ printer.host,
+ printer.deviceId,
+ printer.timeout
)
// Parsear la respuesta
@@ -43,7 +57,11 @@ export default defineEventHandler(async (event) => {
ok: success,
httpStatus: result.status,
code,
- raw: result.data
+ raw: result.data,
+ printerUsed: {
+ id: printer.id,
+ name: printer.name
+ }
}
} catch (err: any) {
return {
diff --git a/server/api/printers/[id].delete.ts b/server/api/printers/[id].delete.ts
new file mode 100644
index 0000000..3696d2d
--- /dev/null
+++ b/server/api/printers/[id].delete.ts
@@ -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
+ }
+ }
+})
diff --git a/server/api/printers/[id].get.ts b/server/api/printers/[id].get.ts
new file mode 100644
index 0000000..107d9d1
--- /dev/null
+++ b/server/api/printers/[id].get.ts
@@ -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
+ }
+ }
+})
diff --git a/server/api/printers/[id].put.ts b/server/api/printers/[id].put.ts
new file mode 100644
index 0000000..862a7cf
--- /dev/null
+++ b/server/api/printers/[id].put.ts
@@ -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
+ }
+ }
+})
diff --git a/server/api/printers/index.get.ts b/server/api/printers/index.get.ts
new file mode 100644
index 0000000..4ecd95a
--- /dev/null
+++ b/server/api/printers/index.get.ts
@@ -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
+ }
+ }
+})
diff --git a/server/api/printers/index.post.ts b/server/api/printers/index.post.ts
new file mode 100644
index 0000000..a5052ba
--- /dev/null
+++ b/server/api/printers/index.post.ts
@@ -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
+ }
+ }
+})
diff --git a/server/api/printers/select.post.ts b/server/api/printers/select.post.ts
new file mode 100644
index 0000000..85dc116
--- /dev/null
+++ b/server/api/printers/select.post.ts
@@ -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
+ }
+ }
+})
diff --git a/server/utils/printers.ts b/server/utils/printers.ts
new file mode 100644
index 0000000..5b467e4
--- /dev/null
+++ b/server/utils/printers.ts
@@ -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 {
+ if (!existsSync(DATA_DIR)) {
+ await mkdir(DATA_DIR, { recursive: true })
+ }
+}
+
+// Leer store de impresoras
+export async function readPrintersStore(): Promise {
+ 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 {
+ 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 {
+ const store = await readPrintersStore()
+ return store.printers
+}
+
+export async function getPrinterById(id: string): Promise {
+ const store = await readPrintersStore()
+ return store.printers.find(p => p.id === id) || null
+}
+
+export async function getDefaultPrinter(): Promise {
+ const store = await readPrintersStore()
+ return store.printers.find(p => p.isDefault) || store.printers[0] || null
+}
+
+export async function getSelectedPrinter(): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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
+}