From 155995c7730a785fc85c9ac7d2ed1f689a118c8d Mon Sep 17 00:00:00 2001 From: josedario87 Date: Tue, 25 Nov 2025 01:18:36 -0600 Subject: [PATCH] feat: Templates persistentes en servidor + Constructor con tabs por tipo de comando MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Templates ahora se guardan en servidor (data/templates.json) disponibles para todos - API CRUD para templates: GET/POST /api/templates, GET/PUT/DELETE /api/templates/[id] - Constructor de comandos rediseñado con tabs: Texto, Feed, Cortar, Pulse, QR, Barcode - Cada tipo de comando tiene su formulario específico con campos relevantes - Eliminado QuickActions (integrado en tabs del constructor) - Mejorada UI de lista de impresoras con renderizado condicional - Agregado data/ a .gitignore (datos de runtime) --- .gitignore | 3 + app/components/constructor/CommandBuilder.vue | 447 ++++++++++++++---- app/components/constructor/QuickActions.vue | 54 --- app/components/printers/List.vue | 112 +++-- app/components/queue/Actions.vue | 34 +- app/components/templates/TemplateList.vue | 33 +- app/composables/useTemplates.ts | 158 ++++--- app/pages/index.vue | 11 +- server/api/templates/[id].delete.ts | 23 + server/api/templates/[id].get.ts | 23 + server/api/templates/[id].put.ts | 28 ++ server/api/templates/[id]/duplicate.post.ts | 23 + server/api/templates/index.get.ts | 6 + server/api/templates/index.post.ts | 27 ++ server/utils/templates.ts | 136 ++++++ 15 files changed, 825 insertions(+), 293 deletions(-) delete mode 100644 app/components/constructor/QuickActions.vue create mode 100644 server/api/templates/[id].delete.ts create mode 100644 server/api/templates/[id].get.ts create mode 100644 server/api/templates/[id].put.ts create mode 100644 server/api/templates/[id]/duplicate.post.ts create mode 100644 server/api/templates/index.get.ts create mode 100644 server/api/templates/index.post.ts create mode 100644 server/utils/templates.ts diff --git a/.gitignore b/.gitignore index 4a7f73a..5ce9865 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ logs .env .env.* !.env.example + +# Data files (runtime) +data/ diff --git a/app/components/constructor/CommandBuilder.vue b/app/components/constructor/CommandBuilder.vue index 38bb9f0..08df1fb 100644 --- a/app/components/constructor/CommandBuilder.vue +++ b/app/components/constructor/CommandBuilder.vue @@ -1,30 +1,64 @@ - - - - + +
+ + {{ cmd.label }} + +
- - - + +
+ + + - + + - + - - + + +
+ + +
+ + + +

+ Avanza el papel la cantidad de líneas especificadas. +

+
+ + +
+ + + +

+ Corta el papel según el tipo seleccionado. +

+
+ + +
+
+ + + + + + +
+

+ Envía un pulso al cajón de dinero para abrirlo. +

+
+ + +
+ + + +
+ + + + + + + + + +
+
+ + +
+ + + +
+ + + + + + +
+
+ + + + + + +
+
diff --git a/app/components/constructor/QuickActions.vue b/app/components/constructor/QuickActions.vue deleted file mode 100644 index f2006a2..0000000 --- a/app/components/constructor/QuickActions.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/app/components/printers/List.vue b/app/components/printers/List.vue index d0bb170..a850db5 100644 --- a/app/components/printers/List.vue +++ b/app/components/printers/List.vue @@ -71,62 +71,70 @@ async function handleDelete(printer: typeof printers.printers.value[0]) { diff --git a/app/components/queue/Actions.vue b/app/components/queue/Actions.vue index 0fdf9e5..f04b556 100644 --- a/app/components/queue/Actions.vue +++ b/app/components/queue/Actions.vue @@ -1,21 +1,35 @@ @@ -101,7 +115,7 @@ function saveAsTemplate() { Cancelar - + Guardar diff --git a/app/components/templates/TemplateList.vue b/app/components/templates/TemplateList.vue index 9432464..837da88 100644 --- a/app/components/templates/TemplateList.vue +++ b/app/components/templates/TemplateList.vue @@ -1,20 +1,39 @@ @@ -29,7 +48,11 @@ function deleteTemplate(id: string) { -
+
+ +
+ +

No hay templates guardados diff --git a/app/composables/useTemplates.ts b/app/composables/useTemplates.ts index c1866bf..70c9f7c 100644 --- a/app/composables/useTemplates.ts +++ b/app/composables/useTemplates.ts @@ -5,92 +5,116 @@ export interface PrintTemplate { name: string description?: string operations: Operation[] - createdAt: number - updatedAt: number -} - -const STORAGE_KEY = 'printercentral-templates' - -// Función para generar UUID compatible con todos los navegadores -function generateId(): string { - if (typeof crypto !== 'undefined' && crypto.randomUUID) { - return crypto.randomUUID() - } - // Fallback para navegadores sin crypto.randomUUID - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = Math.random() * 16 | 0 - const v = c === 'x' ? r : (r & 0x3 | 0x8) - return v.toString(16) - }) + createdAt: string + updatedAt: string } export function useTemplates() { const templates = useState('templates', () => []) - const initialized = useState('templatesInitialized', () => false) + const loading = useState('templatesLoading', () => false) + const error = useState('templatesError', () => null) - // Cargar de localStorage al iniciar (solo cliente) - if (import.meta.client && !initialized.value) { - const stored = localStorage.getItem(STORAGE_KEY) - if (stored) { - try { - templates.value = JSON.parse(stored) - } catch (e) { - console.error('Error parsing templates:', e) - } - } - initialized.value = true - } - - // Guardar en localStorage cuando cambie - watch(templates, (val) => { - if (import.meta.client) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(val)) - } - }, { deep: true }) - - function saveTemplate(name: string, description: string, operations: Operation[]): PrintTemplate { - const template: PrintTemplate = { - id: generateId(), - name, - description, - operations: JSON.parse(JSON.stringify(operations)), - createdAt: Date.now(), - updatedAt: Date.now() - } - templates.value = [...templates.value, template] - return template - } - - function updateTemplate(id: string, updates: Partial>) { - const idx = templates.value.findIndex(t => t.id === id) - if (idx !== -1) { - templates.value = templates.value.map((t, i) => - i === idx ? { ...t, ...updates, updatedAt: Date.now() } : t - ) + // Cargar templates del servidor + async function fetchTemplates(): Promise { + loading.value = true + error.value = null + try { + const data = await $fetch('/api/templates') + templates.value = data + } catch (e) { + console.error('Error fetching templates:', e) + error.value = 'Error al cargar templates' + } finally { + loading.value = false } } - function deleteTemplate(id: string) { - templates.value = templates.value.filter(t => t.id !== id) + // Guardar nuevo template + async function saveTemplate( + name: string, + description: string, + operations: Operation[] + ): Promise { + error.value = null + try { + const template = await $fetch('/api/templates', { + method: 'POST', + body: { + name, + description, + operations + } + }) + templates.value = [...templates.value, template] + return template + } catch (e) { + console.error('Error saving template:', e) + error.value = 'Error al guardar template' + return null + } } + // Actualizar template existente + async function updateTemplate( + id: string, + updates: Partial<{ name: string; description: string; operations: Operation[] }> + ): Promise { + error.value = null + try { + const template = await $fetch(`/api/templates/${id}`, { + method: 'PUT', + body: updates + }) + templates.value = templates.value.map(t => t.id === id ? template : t) + return template + } catch (e) { + console.error('Error updating template:', e) + error.value = 'Error al actualizar template' + return null + } + } + + // Eliminar template + async function deleteTemplate(id: string): Promise { + error.value = null + try { + await $fetch(`/api/templates/${id}`, { method: 'DELETE' }) + templates.value = templates.value.filter(t => t.id !== id) + return true + } catch (e) { + console.error('Error deleting template:', e) + error.value = 'Error al eliminar template' + return false + } + } + + // Cargar operaciones de un template function loadTemplate(id: string): Operation[] | null { const template = templates.value.find(t => t.id === id) return template ? JSON.parse(JSON.stringify(template.operations)) : null } - function duplicateTemplate(id: string): PrintTemplate | null { - const template = templates.value.find(t => t.id === id) - if (!template) return null - return saveTemplate( - `${template.name} (copia)`, - template.description || '', - template.operations - ) + // Duplicar template + async function duplicateTemplate(id: string): Promise { + error.value = null + try { + const template = await $fetch(`/api/templates/${id}/duplicate`, { + method: 'POST' + }) + templates.value = [...templates.value, template] + return template + } catch (e) { + console.error('Error duplicating template:', e) + error.value = 'Error al duplicar template' + return null + } } return { templates: readonly(templates), + loading: readonly(loading), + error: readonly(error), + fetchTemplates, saveTemplate, updateTemplate, deleteTemplate, diff --git a/app/pages/index.vue b/app/pages/index.vue index ac48086..f3b6235 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -63,7 +63,6 @@ onMounted(() => { diff --git a/server/api/templates/[id].delete.ts b/server/api/templates/[id].delete.ts new file mode 100644 index 0000000..4eff177 --- /dev/null +++ b/server/api/templates/[id].delete.ts @@ -0,0 +1,23 @@ +import { deleteTemplate } from '../../utils/templates' + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, 'id') + + if (!id) { + throw createError({ + statusCode: 400, + message: 'ID requerido' + }) + } + + const success = await deleteTemplate(id) + + if (!success) { + throw createError({ + statusCode: 404, + message: 'Template no encontrado' + }) + } + + return { success: true } +}) diff --git a/server/api/templates/[id].get.ts b/server/api/templates/[id].get.ts new file mode 100644 index 0000000..06f5e05 --- /dev/null +++ b/server/api/templates/[id].get.ts @@ -0,0 +1,23 @@ +import { getTemplateById } from '../../utils/templates' + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, 'id') + + if (!id) { + throw createError({ + statusCode: 400, + message: 'ID requerido' + }) + } + + const template = await getTemplateById(id) + + if (!template) { + throw createError({ + statusCode: 404, + message: 'Template no encontrado' + }) + } + + return template +}) diff --git a/server/api/templates/[id].put.ts b/server/api/templates/[id].put.ts new file mode 100644 index 0000000..4883d2e --- /dev/null +++ b/server/api/templates/[id].put.ts @@ -0,0 +1,28 @@ +import { updateTemplate } from '../../utils/templates' + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, 'id') + const body = await readBody(event) + + if (!id) { + throw createError({ + statusCode: 400, + message: 'ID requerido' + }) + } + + const template = await updateTemplate(id, { + name: body.name, + description: body.description, + operations: body.operations + }) + + if (!template) { + throw createError({ + statusCode: 404, + message: 'Template no encontrado' + }) + } + + return template +}) diff --git a/server/api/templates/[id]/duplicate.post.ts b/server/api/templates/[id]/duplicate.post.ts new file mode 100644 index 0000000..4c3bd97 --- /dev/null +++ b/server/api/templates/[id]/duplicate.post.ts @@ -0,0 +1,23 @@ +import { duplicateTemplate } from '../../../utils/templates' + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, 'id') + + if (!id) { + throw createError({ + statusCode: 400, + message: 'ID requerido' + }) + } + + const template = await duplicateTemplate(id) + + if (!template) { + throw createError({ + statusCode: 404, + message: 'Template no encontrado' + }) + } + + return template +}) diff --git a/server/api/templates/index.get.ts b/server/api/templates/index.get.ts new file mode 100644 index 0000000..d4f36d9 --- /dev/null +++ b/server/api/templates/index.get.ts @@ -0,0 +1,6 @@ +import { getAllTemplates } from '../../utils/templates' + +export default defineEventHandler(async () => { + const templates = await getAllTemplates() + return templates +}) diff --git a/server/api/templates/index.post.ts b/server/api/templates/index.post.ts new file mode 100644 index 0000000..3ae9a03 --- /dev/null +++ b/server/api/templates/index.post.ts @@ -0,0 +1,27 @@ +import { createTemplate } from '../../utils/templates' + +export default defineEventHandler(async (event) => { + const body = await readBody(event) + + if (!body.name) { + throw createError({ + statusCode: 400, + message: 'El nombre es requerido' + }) + } + + if (!body.operations || !Array.isArray(body.operations)) { + throw createError({ + statusCode: 400, + message: 'Las operaciones son requeridas' + }) + } + + const template = await createTemplate({ + name: body.name, + description: body.description, + operations: body.operations + }) + + return template +}) diff --git a/server/utils/templates.ts b/server/utils/templates.ts new file mode 100644 index 0000000..bb4f700 --- /dev/null +++ b/server/utils/templates.ts @@ -0,0 +1,136 @@ +// Gestión de templates persistentes +import { readFile, writeFile, mkdir } from 'fs/promises' +import { existsSync } from 'fs' +import { join } from 'path' + +export interface Operation { + id: string + type: 'text' | 'feed' | 'cut' | 'pulse' | 'image' | 'barcode' | 'qrcode' + params: Record +} + +export interface PrintTemplate { + id: string + name: string + description?: string + operations: Operation[] + createdAt: string + updatedAt: string +} + +export interface TemplatesStore { + templates: PrintTemplate[] +} + +// Directorio de datos persistentes +const DATA_DIR = join(process.cwd(), 'data') +const TEMPLATES_FILE = join(DATA_DIR, 'templates.json') + +// Asegurar que el directorio existe +async function ensureDataDir(): Promise { + if (!existsSync(DATA_DIR)) { + await mkdir(DATA_DIR, { recursive: true }) + } +} + +// Leer store de templates +export async function readTemplatesStore(): Promise { + await ensureDataDir() + + try { + const data = await readFile(TEMPLATES_FILE, 'utf-8') + return JSON.parse(data) + } catch { + // Si no existe el archivo, retornar store vacío + return { + templates: [] + } + } +} + +// Guardar store de templates +export async function writeTemplatesStore(store: TemplatesStore): Promise { + await ensureDataDir() + await writeFile(TEMPLATES_FILE, JSON.stringify(store, null, 2), 'utf-8') +} + +// Generar ID único +function generateId(): string { + return `template_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` +} + +// CRUD Operations + +export async function getAllTemplates(): Promise { + const store = await readTemplatesStore() + return store.templates +} + +export async function getTemplateById(id: string): Promise { + const store = await readTemplatesStore() + return store.templates.find(t => t.id === id) || null +} + +export async function createTemplate(data: { + name: string + description?: string + operations: Operation[] +}): Promise { + const store = await readTemplatesStore() + + const now = new Date().toISOString() + const template: PrintTemplate = { + id: generateId(), + name: data.name, + description: data.description || '', + operations: data.operations, + createdAt: now, + updatedAt: now + } + + store.templates.push(template) + await writeTemplatesStore(store) + return template +} + +export async function updateTemplate(id: string, data: Partial<{ + name: string + description: string + operations: Operation[] +}>): Promise { + const store = await readTemplatesStore() + const index = store.templates.findIndex(t => t.id === id) + + if (index === -1) return null + + store.templates[index] = { + ...store.templates[index], + ...data, + updatedAt: new Date().toISOString() + } + + await writeTemplatesStore(store) + return store.templates[index] +} + +export async function deleteTemplate(id: string): Promise { + const store = await readTemplatesStore() + const index = store.templates.findIndex(t => t.id === id) + + if (index === -1) return false + + store.templates.splice(index, 1) + await writeTemplatesStore(store) + return true +} + +export async function duplicateTemplate(id: string): Promise { + const template = await getTemplateById(id) + if (!template) return null + + return createTemplate({ + name: `${template.name} (copia)`, + description: template.description, + operations: JSON.parse(JSON.stringify(template.operations)) + }) +}