feat: Templates persistentes en servidor + Constructor con tabs por tipo de comando
- 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)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,3 +22,6 @@ logs
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# Data files (runtime)
|
||||||
|
data/
|
||||||
|
|||||||
@@ -1,30 +1,64 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const queue = usePrintQueue()
|
const queue = usePrintQueue()
|
||||||
|
|
||||||
// Estado del formulario
|
// Tipo de comando activo
|
||||||
|
const activeCommand = ref('text')
|
||||||
|
|
||||||
|
// Tipos de comandos disponibles
|
||||||
|
const commandTypes = [
|
||||||
|
{ label: 'Texto', value: 'text', icon: 'i-heroicons-document-text' },
|
||||||
|
{ label: 'Feed', value: 'feed', icon: 'i-heroicons-arrow-down' },
|
||||||
|
{ label: 'Cortar', value: 'cut', icon: 'i-heroicons-scissors' },
|
||||||
|
{ label: 'Pulse', value: 'pulse', icon: 'i-heroicons-bolt' },
|
||||||
|
{ label: 'QR', value: 'qr', icon: 'i-heroicons-qr-code' },
|
||||||
|
{ label: 'Barcode', value: 'barcode', icon: 'i-heroicons-bars-3' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// ===== Estado para TEXTO =====
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const align = ref('')
|
const align = ref('none')
|
||||||
const font = ref('')
|
const font = ref('none')
|
||||||
const width = ref<number | undefined>()
|
const textWidth = ref<number | undefined>()
|
||||||
const height = ref<number | undefined>()
|
const textHeight = ref<number | undefined>()
|
||||||
const bold = ref(false)
|
const bold = ref(false)
|
||||||
const underline = ref(false)
|
const underline = ref(false)
|
||||||
const reverse = ref(false)
|
const reverse = ref(false)
|
||||||
const smooth = ref(false)
|
const smooth = ref(false)
|
||||||
const color = ref('')
|
const color = ref('none')
|
||||||
const feedLines = ref<number | undefined>()
|
|
||||||
const cut = ref('')
|
|
||||||
|
|
||||||
// Opciones para selects
|
// ===== Estado para FEED =====
|
||||||
|
const feedLines = ref(2)
|
||||||
|
|
||||||
|
// ===== Estado para CORTAR =====
|
||||||
|
const cutType = ref('feed')
|
||||||
|
|
||||||
|
// ===== Estado para PULSE =====
|
||||||
|
const pulseDrawer = ref('drawer_1')
|
||||||
|
const pulseTime = ref('pulse_200')
|
||||||
|
|
||||||
|
// ===== Estado para QR =====
|
||||||
|
const qrData = ref('')
|
||||||
|
const qrModel = ref('qrcode_model_2')
|
||||||
|
const qrLevel = ref('level_m')
|
||||||
|
const qrSize = ref(6)
|
||||||
|
|
||||||
|
// ===== Estado para BARCODE =====
|
||||||
|
const barcodeData = ref('')
|
||||||
|
const barcodeType = ref('ean13')
|
||||||
|
const barcodeHri = ref('below')
|
||||||
|
const barcodeWidth = ref(3)
|
||||||
|
const barcodeHeight = ref(80)
|
||||||
|
|
||||||
|
// ===== Opciones para selects =====
|
||||||
const alignOptions = [
|
const alignOptions = [
|
||||||
{ label: '(sin cambio)', value: '' },
|
{ label: '(sin cambio)', value: 'none' },
|
||||||
{ label: 'Izquierda', value: 'left' },
|
{ label: 'Izquierda', value: 'left' },
|
||||||
{ label: 'Centro', value: 'center' },
|
{ label: 'Centro', value: 'center' },
|
||||||
{ label: 'Derecha', value: 'right' }
|
{ label: 'Derecha', value: 'right' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const fontOptions = [
|
const fontOptions = [
|
||||||
{ label: '(sin cambio)', value: '' },
|
{ label: '(sin cambio)', value: 'none' },
|
||||||
{ label: 'Font A', value: 'font_a' },
|
{ label: 'Font A', value: 'font_a' },
|
||||||
{ label: 'Font B', value: 'font_b' },
|
{ label: 'Font B', value: 'font_b' },
|
||||||
{ label: 'Font C', value: 'font_c' },
|
{ label: 'Font C', value: 'font_c' },
|
||||||
@@ -35,7 +69,7 @@ const fontOptions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const colorOptions = [
|
const colorOptions = [
|
||||||
{ label: '(default)', value: '' },
|
{ label: '(default)', value: 'none' },
|
||||||
{ label: 'Color 1', value: 'color_1' },
|
{ label: 'Color 1', value: 'color_1' },
|
||||||
{ label: 'Color 2', value: 'color_2' },
|
{ label: 'Color 2', value: 'color_2' },
|
||||||
{ label: 'Color 3', value: 'color_3' },
|
{ label: 'Color 3', value: 'color_3' },
|
||||||
@@ -43,7 +77,6 @@ const colorOptions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const cutOptions = [
|
const cutOptions = [
|
||||||
{ label: '(no cortar)', value: '' },
|
|
||||||
{ label: 'Sin feed', value: 'no_feed' },
|
{ label: 'Sin feed', value: 'no_feed' },
|
||||||
{ label: 'Con feed', value: 'feed' },
|
{ label: 'Con feed', value: 'feed' },
|
||||||
{ label: 'Reservar', value: 'reserve' },
|
{ label: 'Reservar', value: 'reserve' },
|
||||||
@@ -52,51 +85,173 @@ const cutOptions = [
|
|||||||
{ label: 'Full reservar', value: 'reserve_fullcut' }
|
{ label: 'Full reservar', value: 'reserve_fullcut' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const formatSections = [
|
const drawerOptions = [
|
||||||
{ label: 'Alineación y Fuente', value: 'alignment', icon: 'i-heroicons-bars-3-bottom-left' },
|
{ label: 'Drawer 1', value: 'drawer_1' },
|
||||||
{ label: 'Tamaño', value: 'size', icon: 'i-heroicons-arrows-pointing-out' },
|
{ label: 'Drawer 2', value: 'drawer_2' }
|
||||||
{ label: 'Estilo', value: 'style', icon: 'i-heroicons-paint-brush' },
|
|
||||||
{ label: 'Acciones finales', value: 'actions', icon: 'i-heroicons-scissors' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const pulseTimeOptions = [
|
||||||
|
{ label: '100ms', value: 'pulse_100' },
|
||||||
|
{ label: '200ms', value: 'pulse_200' },
|
||||||
|
{ label: '300ms', value: 'pulse_300' },
|
||||||
|
{ label: '400ms', value: 'pulse_400' },
|
||||||
|
{ label: '500ms', value: 'pulse_500' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const qrModelOptions = [
|
||||||
|
{ label: 'Model 1', value: 'qrcode_model_1' },
|
||||||
|
{ label: 'Model 2', value: 'qrcode_model_2' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const qrLevelOptions = [
|
||||||
|
{ label: 'Level L (7%)', value: 'level_l' },
|
||||||
|
{ label: 'Level M (15%)', value: 'level_m' },
|
||||||
|
{ label: 'Level Q (25%)', value: 'level_q' },
|
||||||
|
{ label: 'Level H (30%)', value: 'level_h' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const barcodeTypeOptions = [
|
||||||
|
{ label: 'UPC-A', value: 'upc_a' },
|
||||||
|
{ label: 'UPC-E', value: 'upc_e' },
|
||||||
|
{ label: 'EAN-13', value: 'ean13' },
|
||||||
|
{ label: 'EAN-8', value: 'ean8' },
|
||||||
|
{ label: 'Code 39', value: 'code39' },
|
||||||
|
{ label: 'ITF', value: 'itf' },
|
||||||
|
{ label: 'Codabar', value: 'codabar' },
|
||||||
|
{ label: 'Code 93', value: 'code93' },
|
||||||
|
{ label: 'Code 128', value: 'code128' },
|
||||||
|
{ label: 'GS1-128', value: 'gs1_128' },
|
||||||
|
{ label: 'GS1 DataBar', value: 'gs1_databar_omnidirectional' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const barcodeHriOptions = [
|
||||||
|
{ label: 'Sin HRI', value: 'none' },
|
||||||
|
{ label: 'Arriba', value: 'above' },
|
||||||
|
{ label: 'Abajo', value: 'below' },
|
||||||
|
{ label: 'Ambos', value: 'both' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// ===== Secciones de formato para texto =====
|
||||||
|
const formatSections = [
|
||||||
|
{ label: 'Alineación y Fuente', slot: 'alignment', icon: 'i-heroicons-bars-3-bottom-left' },
|
||||||
|
{ label: 'Tamaño', slot: 'size', icon: 'i-heroicons-arrows-pointing-out' },
|
||||||
|
{ label: 'Estilo', slot: 'style', icon: 'i-heroicons-paint-brush' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// ===== Funciones =====
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
|
// Reset texto
|
||||||
text.value = ''
|
text.value = ''
|
||||||
align.value = ''
|
align.value = 'none'
|
||||||
font.value = ''
|
font.value = 'none'
|
||||||
width.value = undefined
|
textWidth.value = undefined
|
||||||
height.value = undefined
|
textHeight.value = undefined
|
||||||
bold.value = false
|
bold.value = false
|
||||||
underline.value = false
|
underline.value = false
|
||||||
reverse.value = false
|
reverse.value = false
|
||||||
smooth.value = false
|
smooth.value = false
|
||||||
color.value = ''
|
color.value = 'none'
|
||||||
feedLines.value = undefined
|
// Reset feed
|
||||||
cut.value = ''
|
feedLines.value = 2
|
||||||
|
// Reset cut
|
||||||
|
cutType.value = 'feed'
|
||||||
|
// Reset pulse
|
||||||
|
pulseDrawer.value = 'drawer_1'
|
||||||
|
pulseTime.value = 'pulse_200'
|
||||||
|
// Reset QR
|
||||||
|
qrData.value = ''
|
||||||
|
qrModel.value = 'qrcode_model_2'
|
||||||
|
qrLevel.value = 'level_m'
|
||||||
|
qrSize.value = 6
|
||||||
|
// Reset barcode
|
||||||
|
barcodeData.value = ''
|
||||||
|
barcodeType.value = 'ean13'
|
||||||
|
barcodeHri.value = 'below'
|
||||||
|
barcodeWidth.value = 3
|
||||||
|
barcodeHeight.value = 80
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueText() {
|
function addToQueue() {
|
||||||
const ops: any[] = []
|
const ops: any[] = []
|
||||||
|
|
||||||
if (align.value) ops.push({ op: 'textAlign', align: align.value })
|
switch (activeCommand.value) {
|
||||||
if (font.value) ops.push({ op: 'textFont', font: font.value })
|
case 'text':
|
||||||
if (width.value || height.value) {
|
if (align.value && align.value !== 'none') ops.push({ op: 'textAlign', align: align.value })
|
||||||
ops.push({ op: 'textSize', width: width.value, height: height.value })
|
if (font.value && font.value !== 'none') ops.push({ op: 'textFont', font: font.value })
|
||||||
|
if (textWidth.value || textHeight.value) {
|
||||||
|
ops.push({ op: 'textSize', width: textWidth.value, height: textHeight.value })
|
||||||
}
|
}
|
||||||
ops.push({
|
ops.push({
|
||||||
op: 'textStyle',
|
op: 'textStyle',
|
||||||
em: bold.value,
|
em: bold.value,
|
||||||
ul: underline.value,
|
ul: underline.value,
|
||||||
reverse: reverse.value,
|
reverse: reverse.value,
|
||||||
...(color.value ? { color: color.value } : {})
|
...(color.value && color.value !== 'none' ? { color: color.value } : {})
|
||||||
})
|
})
|
||||||
if (text.value) ops.push({ op: 'text', value: text.value })
|
if (text.value) ops.push({ op: 'text', value: text.value })
|
||||||
if (feedLines.value != null && feedLines.value !== 0) {
|
break
|
||||||
ops.push({ op: 'feedLine', line: Number(feedLines.value) })
|
|
||||||
}
|
|
||||||
if (cut.value) ops.push({ op: 'cut', type: cut.value })
|
|
||||||
|
|
||||||
|
case 'feed':
|
||||||
|
ops.push({ op: 'feedLine', line: Number(feedLines.value) || 1 })
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'cut':
|
||||||
|
ops.push({ op: 'cut', type: cutType.value })
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pulse':
|
||||||
|
ops.push({ op: 'pulse', drawer: pulseDrawer.value, time: pulseTime.value })
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'qr':
|
||||||
|
if (qrData.value) {
|
||||||
|
ops.push({
|
||||||
|
op: 'qrcode',
|
||||||
|
data: qrData.value,
|
||||||
|
model: qrModel.value,
|
||||||
|
level: qrLevel.value,
|
||||||
|
size: qrSize.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'barcode':
|
||||||
|
if (barcodeData.value) {
|
||||||
|
ops.push({
|
||||||
|
op: 'barcode',
|
||||||
|
data: barcodeData.value,
|
||||||
|
type: barcodeType.value,
|
||||||
|
hri: barcodeHri.value,
|
||||||
|
width: barcodeWidth.value,
|
||||||
|
height: barcodeHeight.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ops.length > 0) {
|
||||||
queue.addOperations(ops)
|
queue.addOperations(ops)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validación para habilitar/deshabilitar botón
|
||||||
|
const canAdd = computed(() => {
|
||||||
|
switch (activeCommand.value) {
|
||||||
|
case 'text':
|
||||||
|
return text.value.trim().length > 0
|
||||||
|
case 'feed':
|
||||||
|
return feedLines.value != null && feedLines.value > 0
|
||||||
|
case 'cut':
|
||||||
|
case 'pulse':
|
||||||
|
return true
|
||||||
|
case 'qr':
|
||||||
|
return qrData.value.trim().length > 0
|
||||||
|
case 'barcode':
|
||||||
|
return barcodeData.value.trim().length > 0
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -115,8 +270,24 @@ function queueText() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Textarea principal -->
|
<!-- Selector de tipo de comando -->
|
||||||
<UFormField label="Texto a imprimir" class="mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
<UButton
|
||||||
|
v-for="cmd in commandTypes"
|
||||||
|
:key="cmd.value"
|
||||||
|
:variant="activeCommand === cmd.value ? 'solid' : 'outline'"
|
||||||
|
:color="activeCommand === cmd.value ? 'primary' : 'neutral'"
|
||||||
|
size="sm"
|
||||||
|
:icon="cmd.icon"
|
||||||
|
@click="activeCommand = cmd.value"
|
||||||
|
>
|
||||||
|
{{ cmd.label }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario de TEXTO -->
|
||||||
|
<div v-if="activeCommand === 'text'" class="space-y-4">
|
||||||
|
<UFormField label="Texto a imprimir">
|
||||||
<UTextarea
|
<UTextarea
|
||||||
v-model="text"
|
v-model="text"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
@@ -125,18 +296,17 @@ function queueText() {
|
|||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<!-- Controles de formato con Accordion -->
|
|
||||||
<UAccordion :items="formatSections" type="multiple">
|
<UAccordion :items="formatSections" type="multiple">
|
||||||
<template #alignment>
|
<template #alignment>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 p-3">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 p-3">
|
||||||
<UFormField label="Alineación">
|
<UFormField label="Alineación">
|
||||||
<USelect v-model="align" :items="alignOptions" placeholder="Seleccionar" />
|
<USelect v-model="align" :items="alignOptions" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="Fuente">
|
<UFormField label="Fuente">
|
||||||
<USelect v-model="font" :items="fontOptions" placeholder="Seleccionar" />
|
<USelect v-model="font" :items="fontOptions" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="Color">
|
<UFormField label="Color">
|
||||||
<USelect v-model="color" :items="colorOptions" placeholder="Seleccionar" />
|
<USelect v-model="color" :items="colorOptions" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -144,10 +314,10 @@ function queueText() {
|
|||||||
<template #size>
|
<template #size>
|
||||||
<div class="grid grid-cols-2 gap-3 p-3">
|
<div class="grid grid-cols-2 gap-3 p-3">
|
||||||
<UFormField label="Ancho (1-8)">
|
<UFormField label="Ancho (1-8)">
|
||||||
<UInput v-model.number="width" type="number" :min="1" :max="8" placeholder="1" />
|
<UInput v-model.number="textWidth" type="number" :min="1" :max="8" placeholder="1" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="Alto (1-8)">
|
<UFormField label="Alto (1-8)">
|
||||||
<UInput v-model.number="height" type="number" :min="1" :max="8" placeholder="1" />
|
<UInput v-model.number="textHeight" type="number" :min="1" :max="8" placeholder="1" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -160,26 +330,105 @@ function queueText() {
|
|||||||
<UCheckbox v-model="smooth" label="Suavizado" />
|
<UCheckbox v-model="smooth" label="Suavizado" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</UAccordion>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #actions>
|
<!-- Formulario de FEED -->
|
||||||
<div class="grid grid-cols-2 gap-3 p-3">
|
<div v-else-if="activeCommand === 'feed'" class="space-y-4">
|
||||||
<UFormField label="Líneas de feed">
|
<UFormField label="Líneas de avance">
|
||||||
<UInput v-model.number="feedLines" type="number" :min="0" :max="255" placeholder="0" />
|
<UInput
|
||||||
|
v-model.number="feedLines"
|
||||||
|
type="number"
|
||||||
|
:min="1"
|
||||||
|
:max="255"
|
||||||
|
placeholder="2"
|
||||||
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="Corte de papel">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
<USelect v-model="cut" :items="cutOptions" placeholder="Seleccionar" />
|
Avanza el papel la cantidad de líneas especificadas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario de CORTAR -->
|
||||||
|
<div v-else-if="activeCommand === 'cut'" class="space-y-4">
|
||||||
|
<UFormField label="Tipo de corte">
|
||||||
|
<USelect v-model="cutType" :items="cutOptions" />
|
||||||
|
</UFormField>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Corta el papel según el tipo seleccionado.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario de PULSE -->
|
||||||
|
<div v-else-if="activeCommand === 'pulse'" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<UFormField label="Drawer">
|
||||||
|
<USelect v-model="pulseDrawer" :items="drawerOptions" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="Duración">
|
||||||
|
<USelect v-model="pulseTime" :items="pulseTimeOptions" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
</UAccordion>
|
Envía un pulso al cajón de dinero para abrirlo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario de QR -->
|
||||||
|
<div v-else-if="activeCommand === 'qr'" class="space-y-4">
|
||||||
|
<UFormField label="Datos del QR">
|
||||||
|
<UTextarea
|
||||||
|
v-model="qrData"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="https://ejemplo.com o texto"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<UFormField label="Modelo">
|
||||||
|
<USelect v-model="qrModel" :items="qrModelOptions" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="Corrección">
|
||||||
|
<USelect v-model="qrLevel" :items="qrLevelOptions" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="Tamaño">
|
||||||
|
<UInput v-model.number="qrSize" type="number" :min="1" :max="16" />
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario de BARCODE -->
|
||||||
|
<div v-else-if="activeCommand === 'barcode'" class="space-y-4">
|
||||||
|
<UFormField label="Datos del código">
|
||||||
|
<UInput
|
||||||
|
v-model="barcodeData"
|
||||||
|
placeholder="490123456789"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<UFormField label="Tipo">
|
||||||
|
<USelect v-model="barcodeType" :items="barcodeTypeOptions" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="HRI (texto)">
|
||||||
|
<USelect v-model="barcodeHri" :items="barcodeHriOptions" />
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<UFormField label="Ancho (2-6)">
|
||||||
|
<UInput v-model.number="barcodeWidth" type="number" :min="2" :max="6" />
|
||||||
|
</UFormField>
|
||||||
|
<UFormField label="Alto (1-255)">
|
||||||
|
<UInput v-model.number="barcodeHeight" type="number" :min="1" :max="255" />
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<UButton color="primary" @click="queueText">
|
<UButton color="primary" :disabled="!canAdd" @click="addToQueue">
|
||||||
Agregar a cola
|
Agregar a cola
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton variant="ghost" @click="resetForm">
|
<UButton variant="ghost" @click="resetForm">
|
||||||
Limpiar formulario
|
Limpiar
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const queue = usePrintQueue()
|
|
||||||
|
|
||||||
const quickActions = [
|
|
||||||
{
|
|
||||||
label: 'Feed 2',
|
|
||||||
icon: 'i-heroicons-arrow-down',
|
|
||||||
ops: [{ op: 'feedLine', line: 2 }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Cortar',
|
|
||||||
icon: 'i-heroicons-scissors',
|
|
||||||
ops: [{ op: 'cut', type: 'feed' }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Pulse',
|
|
||||||
icon: 'i-heroicons-bolt',
|
|
||||||
ops: [{ op: 'pulse', drawer: 'drawer_1', time: 'pulse_200' }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'QR',
|
|
||||||
icon: 'i-heroicons-qr-code',
|
|
||||||
ops: [{ op: 'qrcode', data: 'https://example.com', model: 'qrcode_model_2', level: 'level_m', size: 6 }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Barcode',
|
|
||||||
icon: 'i-heroicons-bars-3',
|
|
||||||
ops: [{ op: 'barcode', data: '490123456789', type: 'ean13', hri: 'below', width: 3, height: 80 }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<UCard variant="soft">
|
|
||||||
<template #header>
|
|
||||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
||||||
Atajos rápidos
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<UButton
|
|
||||||
v-for="action in quickActions"
|
|
||||||
:key="action.label"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
:icon="action.icon"
|
|
||||||
@click="queue.addOperations(action.ops)"
|
|
||||||
>
|
|
||||||
{{ action.label }}
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</template>
|
|
||||||
@@ -71,6 +71,29 @@ async function handleDelete(printer: typeof printers.printers.value[0]) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
<!-- Vista del formulario -->
|
||||||
|
<template v-if="showDrawer">
|
||||||
|
<div class="flex items-center gap-2 mb-4">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-left"
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
size="sm"
|
||||||
|
@click="showDrawer = false"
|
||||||
|
/>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ editingPrinter ? 'Editar impresora' : 'Nueva impresora' }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<PrintersForm
|
||||||
|
:printer="editingPrinter"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="showDrawer = false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Vista de la lista -->
|
||||||
|
<template v-else>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
Impresoras
|
Impresoras
|
||||||
@@ -112,21 +135,6 @@ async function handleDelete(printer: typeof printers.printers.value[0]) {
|
|||||||
@set-default="handleSetDefault(printer)"
|
@set-default="handleSetDefault(printer)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UDrawer v-model:open="showDrawer">
|
|
||||||
<template #header>
|
|
||||||
<h3 class="text-lg font-semibold">
|
|
||||||
{{ editingPrinter ? 'Editar impresora' : 'Nueva impresora' }}
|
|
||||||
</h3>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="p-4">
|
|
||||||
<PrintersForm
|
|
||||||
:printer="editingPrinter"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@cancel="showDrawer = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UDrawer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const queue = usePrintQueue()
|
const queue = usePrintQueue()
|
||||||
const templates = useTemplates()
|
const templates = useTemplates()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const saveDrawerOpen = ref(false)
|
const saveDrawerOpen = ref(false)
|
||||||
const templateName = ref('')
|
const templateName = ref('')
|
||||||
const templateDescription = ref('')
|
const templateDescription = ref('')
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
function saveAsTemplate() {
|
async function saveAsTemplate() {
|
||||||
if (!templateName.value.trim()) return
|
if (!templateName.value.trim()) return
|
||||||
templates.saveTemplate(
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const result = await templates.saveTemplate(
|
||||||
templateName.value.trim(),
|
templateName.value.trim(),
|
||||||
templateDescription.value.trim(),
|
templateDescription.value.trim(),
|
||||||
queue.operations.value as any
|
queue.operations.value as any
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
toast.add({ title: 'Template guardado', color: 'success' })
|
||||||
templateName.value = ''
|
templateName.value = ''
|
||||||
templateDescription.value = ''
|
templateDescription.value = ''
|
||||||
saveDrawerOpen.value = false
|
saveDrawerOpen.value = false
|
||||||
|
} else {
|
||||||
|
toast.add({ title: 'Error al guardar template', color: 'error' })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -101,7 +115,7 @@ function saveAsTemplate() {
|
|||||||
<UButton variant="ghost" @click="saveDrawerOpen = false">
|
<UButton variant="ghost" @click="saveDrawerOpen = false">
|
||||||
Cancelar
|
Cancelar
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton color="primary" :disabled="!templateName.trim()" @click="saveAsTemplate">
|
<UButton color="primary" :disabled="!templateName.trim()" :loading="saving" @click="saveAsTemplate">
|
||||||
Guardar
|
Guardar
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const templates = useTemplates()
|
const templates = useTemplates()
|
||||||
const queue = usePrintQueue()
|
const queue = usePrintQueue()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
// Cargar templates al montar
|
||||||
|
onMounted(() => {
|
||||||
|
templates.fetchTemplates()
|
||||||
|
})
|
||||||
|
|
||||||
function loadTemplate(id: string) {
|
function loadTemplate(id: string) {
|
||||||
const ops = templates.loadTemplate(id)
|
const ops = templates.loadTemplate(id)
|
||||||
if (ops) {
|
if (ops) {
|
||||||
queue.loadFromTemplate(ops)
|
queue.loadFromTemplate(ops)
|
||||||
|
toast.add({ title: 'Template cargado en la cola', color: 'success' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateTemplate(id: string) {
|
async function duplicateTemplate(id: string) {
|
||||||
templates.duplicateTemplate(id)
|
const result = await templates.duplicateTemplate(id)
|
||||||
|
if (result) {
|
||||||
|
toast.add({ title: 'Template duplicado', color: 'success' })
|
||||||
|
} else {
|
||||||
|
toast.add({ title: 'Error al duplicar template', color: 'error' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteTemplate(id: string) {
|
async function deleteTemplate(id: string) {
|
||||||
templates.deleteTemplate(id)
|
if (!confirm('¿Eliminar este template?')) return
|
||||||
|
|
||||||
|
const result = await templates.deleteTemplate(id)
|
||||||
|
if (result) {
|
||||||
|
toast.add({ title: 'Template eliminado', color: 'success' })
|
||||||
|
} else {
|
||||||
|
toast.add({ title: 'Error al eliminar template', color: 'error' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -29,7 +48,11 @@ function deleteTemplate(id: string) {
|
|||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="templates.templates.value.length === 0" class="text-center py-12">
|
<div v-if="templates.loading.value" class="flex justify-center py-12">
|
||||||
|
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="templates.templates.value.length === 0" class="text-center py-12">
|
||||||
<UIcon name="i-heroicons-document-duplicate" class="w-12 h-12 text-gray-400 dark:text-gray-600 mx-auto mb-3" />
|
<UIcon name="i-heroicons-document-duplicate" class="w-12 h-12 text-gray-400 dark:text-gray-600 mx-auto mb-3" />
|
||||||
<p class="text-gray-500 dark:text-gray-400 mb-2">
|
<p class="text-gray-500 dark:text-gray-400 mb-2">
|
||||||
No hay templates guardados
|
No hay templates guardados
|
||||||
|
|||||||
@@ -5,92 +5,116 @@ export interface PrintTemplate {
|
|||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
operations: Operation[]
|
operations: Operation[]
|
||||||
createdAt: number
|
createdAt: string
|
||||||
updatedAt: number
|
updatedAt: string
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTemplates() {
|
export function useTemplates() {
|
||||||
const templates = useState<PrintTemplate[]>('templates', () => [])
|
const templates = useState<PrintTemplate[]>('templates', () => [])
|
||||||
const initialized = useState('templatesInitialized', () => false)
|
const loading = useState('templatesLoading', () => false)
|
||||||
|
const error = useState<string | null>('templatesError', () => null)
|
||||||
|
|
||||||
// Cargar de localStorage al iniciar (solo cliente)
|
// Cargar templates del servidor
|
||||||
if (import.meta.client && !initialized.value) {
|
async function fetchTemplates(): Promise<void> {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
loading.value = true
|
||||||
if (stored) {
|
error.value = null
|
||||||
try {
|
try {
|
||||||
templates.value = JSON.parse(stored)
|
const data = await $fetch<PrintTemplate[]>('/api/templates')
|
||||||
|
templates.value = data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing templates:', e)
|
console.error('Error fetching templates:', e)
|
||||||
|
error.value = 'Error al cargar templates'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initialized.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guardar en localStorage cuando cambie
|
// Guardar nuevo template
|
||||||
watch(templates, (val) => {
|
async function saveTemplate(
|
||||||
if (import.meta.client) {
|
name: string,
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
|
description: string,
|
||||||
}
|
operations: Operation[]
|
||||||
}, { deep: true })
|
): Promise<PrintTemplate | null> {
|
||||||
|
error.value = null
|
||||||
function saveTemplate(name: string, description: string, operations: Operation[]): PrintTemplate {
|
try {
|
||||||
const template: PrintTemplate = {
|
const template = await $fetch<PrintTemplate>('/api/templates', {
|
||||||
id: generateId(),
|
method: 'POST',
|
||||||
|
body: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
operations: JSON.parse(JSON.stringify(operations)),
|
operations
|
||||||
createdAt: Date.now(),
|
|
||||||
updatedAt: Date.now()
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
templates.value = [...templates.value, template]
|
templates.value = [...templates.value, template]
|
||||||
return template
|
return template
|
||||||
}
|
} catch (e) {
|
||||||
|
console.error('Error saving template:', e)
|
||||||
function updateTemplate(id: string, updates: Partial<Omit<PrintTemplate, 'id' | 'createdAt'>>) {
|
error.value = 'Error al guardar template'
|
||||||
const idx = templates.value.findIndex(t => t.id === id)
|
return null
|
||||||
if (idx !== -1) {
|
|
||||||
templates.value = templates.value.map((t, i) =>
|
|
||||||
i === idx ? { ...t, ...updates, updatedAt: Date.now() } : t
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteTemplate(id: string) {
|
// Actualizar template existente
|
||||||
|
async function updateTemplate(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<{ name: string; description: string; operations: Operation[] }>
|
||||||
|
): Promise<PrintTemplate | null> {
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const template = await $fetch<PrintTemplate>(`/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<boolean> {
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
await $fetch(`/api/templates/${id}`, { method: 'DELETE' })
|
||||||
templates.value = templates.value.filter(t => t.id !== id)
|
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 {
|
function loadTemplate(id: string): Operation[] | null {
|
||||||
const template = templates.value.find(t => t.id === id)
|
const template = templates.value.find(t => t.id === id)
|
||||||
return template ? JSON.parse(JSON.stringify(template.operations)) : null
|
return template ? JSON.parse(JSON.stringify(template.operations)) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateTemplate(id: string): PrintTemplate | null {
|
// Duplicar template
|
||||||
const template = templates.value.find(t => t.id === id)
|
async function duplicateTemplate(id: string): Promise<PrintTemplate | null> {
|
||||||
if (!template) return null
|
error.value = null
|
||||||
return saveTemplate(
|
try {
|
||||||
`${template.name} (copia)`,
|
const template = await $fetch<PrintTemplate>(`/api/templates/${id}/duplicate`, {
|
||||||
template.description || '',
|
method: 'POST'
|
||||||
template.operations
|
})
|
||||||
)
|
templates.value = [...templates.value, template]
|
||||||
|
return template
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error duplicating template:', e)
|
||||||
|
error.value = 'Error al duplicar template'
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
templates: readonly(templates),
|
templates: readonly(templates),
|
||||||
|
loading: readonly(loading),
|
||||||
|
error: readonly(error),
|
||||||
|
fetchTemplates,
|
||||||
saveTemplate,
|
saveTemplate,
|
||||||
updateTemplate,
|
updateTemplate,
|
||||||
deleteTemplate,
|
deleteTemplate,
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ onMounted(() => {
|
|||||||
<!-- En desktop: siempre mostrar constructor -->
|
<!-- En desktop: siempre mostrar constructor -->
|
||||||
<template v-if="isDesktop || activeTab === 'constructor'">
|
<template v-if="isDesktop || activeTab === 'constructor'">
|
||||||
<ConstructorCommandBuilder />
|
<ConstructorCommandBuilder />
|
||||||
<ConstructorQuickActions class="mt-4" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeTab === 'queue'">
|
<template v-else-if="activeTab === 'queue'">
|
||||||
@@ -85,14 +84,14 @@ onMounted(() => {
|
|||||||
<!-- Navegación mobile -->
|
<!-- Navegación mobile -->
|
||||||
<LayoutMobileNavigation v-model="activeTab" />
|
<LayoutMobileNavigation v-model="activeTab" />
|
||||||
|
|
||||||
<!-- Drawer para gestionar impresoras (desktop) -->
|
<!-- Modal para gestionar impresoras (desktop) -->
|
||||||
<UDrawer v-model:open="showPrintersDrawer" direction="right">
|
<UModal v-model:open="showPrintersDrawer">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h3 class="text-lg font-semibold">Gestión de Impresoras</h3>
|
<h3 class="text-lg font-semibold">Gestión de Impresoras</h3>
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4">
|
<template #body>
|
||||||
<PrintersList />
|
<PrintersList />
|
||||||
</div>
|
</template>
|
||||||
</UDrawer>
|
</UModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
23
server/api/templates/[id].delete.ts
Normal file
23
server/api/templates/[id].delete.ts
Normal file
@@ -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 }
|
||||||
|
})
|
||||||
23
server/api/templates/[id].get.ts
Normal file
23
server/api/templates/[id].get.ts
Normal file
@@ -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
|
||||||
|
})
|
||||||
28
server/api/templates/[id].put.ts
Normal file
28
server/api/templates/[id].put.ts
Normal file
@@ -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
|
||||||
|
})
|
||||||
23
server/api/templates/[id]/duplicate.post.ts
Normal file
23
server/api/templates/[id]/duplicate.post.ts
Normal file
@@ -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
|
||||||
|
})
|
||||||
6
server/api/templates/index.get.ts
Normal file
6
server/api/templates/index.get.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { getAllTemplates } from '../../utils/templates'
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
const templates = await getAllTemplates()
|
||||||
|
return templates
|
||||||
|
})
|
||||||
27
server/api/templates/index.post.ts
Normal file
27
server/api/templates/index.post.ts
Normal file
@@ -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
|
||||||
|
})
|
||||||
136
server/utils/templates.ts
Normal file
136
server/utils/templates.ts
Normal file
@@ -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<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
if (!existsSync(DATA_DIR)) {
|
||||||
|
await mkdir(DATA_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leer store de templates
|
||||||
|
export async function readTemplatesStore(): Promise<TemplatesStore> {
|
||||||
|
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<void> {
|
||||||
|
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<PrintTemplate[]> {
|
||||||
|
const store = await readTemplatesStore()
|
||||||
|
return store.templates
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplateById(id: string): Promise<PrintTemplate | null> {
|
||||||
|
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<PrintTemplate> {
|
||||||
|
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<PrintTemplate | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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<PrintTemplate | null> {
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user