@innertia-solutions/innertia-nuxt 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/Attention.vue +269 -0
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useFile.js +189 -0
- package/composables/useModal.js +71 -0
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
IconPlus, IconTrash, IconChevronDown, IconChevronUp,
|
|
5
|
+
IconGripVertical, IconArrowRight, IconAlertTriangle,
|
|
6
|
+
} from '@tabler/icons-vue'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* WorkflowForm — editor estructurado de WorkflowDefinition.
|
|
10
|
+
* Alternativa al canvas Builder.vue para flujos lineales o usuarios no técnicos.
|
|
11
|
+
*
|
|
12
|
+
* Usa useWorkflowForm internamente o acepta modelValue externo.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
/** Definición inicial { steps:[], transitions:[] } */
|
|
17
|
+
modelValue: { type: Object, default: null },
|
|
18
|
+
/** Solo lectura */
|
|
19
|
+
readonly: { type: Boolean, default: false },
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits(['update:modelValue'])
|
|
23
|
+
|
|
24
|
+
// ── Composable de edición ─────────────────────────────────────────────────────
|
|
25
|
+
const {
|
|
26
|
+
definition, isDirty, isValid,
|
|
27
|
+
addStep, updateStep, deleteStep, reorderSteps,
|
|
28
|
+
addTransition, updateTransition, deleteTransition,
|
|
29
|
+
addRestriction, removeRestriction,
|
|
30
|
+
transitionsFrom,
|
|
31
|
+
} = useWorkflowForm(props.modelValue)
|
|
32
|
+
|
|
33
|
+
// Sync hacia arriba
|
|
34
|
+
watch(definition, (val) => emit('update:modelValue', val), { deep: true })
|
|
35
|
+
|
|
36
|
+
// Sync desde arriba (si el parent cambia modelValue externamente)
|
|
37
|
+
watch(() => props.modelValue, (val) => {
|
|
38
|
+
if (val && JSON.stringify(val) !== JSON.stringify(definition.value)) {
|
|
39
|
+
definition.value = JSON.parse(JSON.stringify(val))
|
|
40
|
+
}
|
|
41
|
+
}, { deep: true })
|
|
42
|
+
|
|
43
|
+
// ── Constantes de tipos ───────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const STEP_TYPES = [
|
|
46
|
+
{ value: 'start', label: 'Inicio', color: 'emerald', description: 'Punto de entrada — solo uno permitido' },
|
|
47
|
+
{ value: 'in_progress', label: 'En progreso', color: 'blue', description: 'Paso activo del proceso' },
|
|
48
|
+
{ value: 'pause_internal', label: 'Pausa interna', color: 'amber', description: 'Pausado por el sistema' },
|
|
49
|
+
{ value: 'pause_external', label: 'Pausa externa', color: 'orange', description: 'Esperando acción externa' },
|
|
50
|
+
{ value: 'finished', label: 'Finalizado', color: 'violet', description: 'Estado terminal exitoso' },
|
|
51
|
+
{ value: 'cancelled', label: 'Cancelado', color: 'red', description: 'Estado terminal cancelado' },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const RESTRICTION_TYPES = [
|
|
55
|
+
{ value: 'role', label: 'Rol', fields: ['roles'] },
|
|
56
|
+
{ value: 'required_fields', label: 'Campos requeridos', fields: ['fields'] },
|
|
57
|
+
{ value: 'approval', label: 'Aprobación', fields: ['role'] },
|
|
58
|
+
{ value: 'checklist', label: 'Checklist completo', fields: ['checklist'] },
|
|
59
|
+
{ value: 'document', label: 'Documento requerido', fields: ['document'] },
|
|
60
|
+
{ value: 'min_items', label: 'Mínimo de ítems', fields: ['relation', 'min'] },
|
|
61
|
+
{ value: 'date', label: 'Fecha mínima', fields: ['after'] },
|
|
62
|
+
{ value: 'custom', label: 'Personalizado', fields: ['entity', 'field', 'value'] },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const stepTypeColor = (type) => STEP_TYPES.find(t => t.value === type)?.color ?? 'slate'
|
|
66
|
+
const stepTypeLabel = (type) => STEP_TYPES.find(t => t.value === type)?.label ?? type
|
|
67
|
+
|
|
68
|
+
// ── Steps: nuevo step ─────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const showNewStep = ref(false)
|
|
71
|
+
const newStep = ref({ key: '', label: '', type: 'in_progress', description: '' })
|
|
72
|
+
const newStepError = ref('')
|
|
73
|
+
|
|
74
|
+
const submitNewStep = () => {
|
|
75
|
+
newStepError.value = ''
|
|
76
|
+
try {
|
|
77
|
+
addStep({ ...newStep.value })
|
|
78
|
+
newStep.value = { key: '', label: '', type: 'in_progress', description: '' }
|
|
79
|
+
showNewStep.value = false
|
|
80
|
+
} catch (e) {
|
|
81
|
+
newStepError.value = e.message
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Steps: edición inline ─────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const editingStep = ref(null) // key del step en edición
|
|
88
|
+
const stepDraft = ref({})
|
|
89
|
+
|
|
90
|
+
const startEditStep = (step) => {
|
|
91
|
+
editingStep.value = step.key
|
|
92
|
+
stepDraft.value = { ...step }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const saveStep = () => {
|
|
96
|
+
try {
|
|
97
|
+
updateStep(editingStep.value, stepDraft.value)
|
|
98
|
+
editingStep.value = null
|
|
99
|
+
} catch (e) {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cancelEditStep = () => { editingStep.value = null }
|
|
103
|
+
|
|
104
|
+
// ── Steps: reorder (drag handle simple con botones up/down) ──────────────────
|
|
105
|
+
|
|
106
|
+
const moveStepUp = (idx) => { if (idx > 0) reorderSteps(idx, idx - 1) }
|
|
107
|
+
const moveStepDown = (idx) => { if (idx < definition.value.steps.length - 1) reorderSteps(idx, idx + 1) }
|
|
108
|
+
|
|
109
|
+
// ── Transiciones: panel expandible por step ──────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const expandedStepKey = ref(null)
|
|
112
|
+
const toggleStep = (key) => {
|
|
113
|
+
expandedStepKey.value = expandedStepKey.value === key ? null : key
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Transiciones: nuevo ───────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const showingAddTransition = ref(null) // step.key que está mostrando el form
|
|
119
|
+
const newTransition = ref({ to: '', label: '', description: '' })
|
|
120
|
+
const newTransitionError = ref('')
|
|
121
|
+
|
|
122
|
+
const openAddTransition = (fromKey) => {
|
|
123
|
+
showingAddTransition.value = fromKey
|
|
124
|
+
newTransition.value = { to: '', label: '', description: '' }
|
|
125
|
+
newTransitionError.value = ''
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const submitTransition = (fromKey) => {
|
|
129
|
+
newTransitionError.value = ''
|
|
130
|
+
try {
|
|
131
|
+
addTransition({
|
|
132
|
+
from: fromKey,
|
|
133
|
+
to: newTransition.value.to,
|
|
134
|
+
label: newTransition.value.label,
|
|
135
|
+
description: newTransition.value.description,
|
|
136
|
+
restrictions: [],
|
|
137
|
+
})
|
|
138
|
+
showingAddTransition.value = null
|
|
139
|
+
} catch (e) {
|
|
140
|
+
newTransitionError.value = e.message
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Restricciones ─────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
const expandedTransition = ref(null) // `${from}__${to}`
|
|
147
|
+
const toggleTransition = (from, to) => {
|
|
148
|
+
const key = `${from}__${to}`
|
|
149
|
+
expandedTransition.value = expandedTransition.value === key ? null : key
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const isTransitionExpanded = (from, to) => expandedTransition.value === `${from}__${to}`
|
|
153
|
+
|
|
154
|
+
const addRestrictionTo = (from, to) => {
|
|
155
|
+
addRestriction(from, to, { type: 'role', roles: [], message: '' })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Validaciones visuales ────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
const warnings = computed(() => {
|
|
161
|
+
const w = []
|
|
162
|
+
const steps = definition.value.steps ?? []
|
|
163
|
+
const transitions = definition.value.transitions ?? []
|
|
164
|
+
|
|
165
|
+
if (!steps.some(s => s.type === 'start'))
|
|
166
|
+
w.push('Falta un step de tipo Inicio')
|
|
167
|
+
if (!steps.some(s => ['finished', 'cancelled'].includes(s.type)))
|
|
168
|
+
w.push('Falta al menos un step terminal (Finalizado o Cancelado)')
|
|
169
|
+
|
|
170
|
+
const startSteps = steps.filter(s => s.type === 'start')
|
|
171
|
+
if (startSteps.length > 1)
|
|
172
|
+
w.push('Solo puede haber un step de tipo Inicio')
|
|
173
|
+
|
|
174
|
+
// Steps sin transiciones salientes (excepto terminales)
|
|
175
|
+
for (const step of steps) {
|
|
176
|
+
if (['finished', 'cancelled'].includes(step.type)) continue
|
|
177
|
+
if (!transitions.some(t => t.from === step.key))
|
|
178
|
+
w.push(`Step "${step.label}" no tiene transiciones salientes`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return w
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// ── Computed para el selector de destino ─────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
const availableTargets = (fromKey) =>
|
|
187
|
+
definition.value.steps.filter(s => {
|
|
188
|
+
if (s.key === fromKey) return false
|
|
189
|
+
// No ofrecer si ya existe esa transición
|
|
190
|
+
return !definition.value.transitions.some(t => t.from === fromKey && t.to === s.key)
|
|
191
|
+
})
|
|
192
|
+
</script>
|
|
193
|
+
|
|
194
|
+
<template>
|
|
195
|
+
<div class="flex flex-col gap-4">
|
|
196
|
+
|
|
197
|
+
<!-- ── Warnings de validación ─────────────────────────────────────────── -->
|
|
198
|
+
<div v-if="warnings.length" class="rounded-card border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-3 space-y-1">
|
|
199
|
+
<div v-for="w in warnings" :key="w" class="flex items-start gap-2 text-xs text-amber-700 dark:text-amber-400">
|
|
200
|
+
<IconAlertTriangle :size="13" class="shrink-0 mt-0.5" />
|
|
201
|
+
{{ w }}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- ── STEPS ──────────────────────────────────────────────────────────── -->
|
|
206
|
+
<div class="space-y-2">
|
|
207
|
+
<div class="flex items-center justify-between">
|
|
208
|
+
<h3 class="text-sm font-semibold text-foreground">Steps</h3>
|
|
209
|
+
<button
|
|
210
|
+
v-if="!readonly"
|
|
211
|
+
type="button"
|
|
212
|
+
@click="showNewStep = !showNewStep"
|
|
213
|
+
class="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
|
|
214
|
+
>
|
|
215
|
+
<IconPlus :size="13" /> Agregar step
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Form nuevo step -->
|
|
220
|
+
<Transition
|
|
221
|
+
enter-active-class="transition-all duration-150 ease-out"
|
|
222
|
+
enter-from-class="opacity-0 -translate-y-1"
|
|
223
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
224
|
+
leave-active-class="transition-all duration-100"
|
|
225
|
+
leave-from-class="opacity-100"
|
|
226
|
+
leave-to-class="opacity-0"
|
|
227
|
+
>
|
|
228
|
+
<div v-if="showNewStep" class="rounded-card border border-primary/30 bg-primary/5 p-3 space-y-3">
|
|
229
|
+
<p class="text-xs font-medium text-foreground">Nuevo step</p>
|
|
230
|
+
<div class="grid grid-cols-2 gap-2">
|
|
231
|
+
<div class="space-y-1">
|
|
232
|
+
<label class="text-[11px] text-muted-foreground">Clave</label>
|
|
233
|
+
<input
|
|
234
|
+
v-model="newStep.key"
|
|
235
|
+
placeholder="ej. revision"
|
|
236
|
+
@keydown.enter="submitNewStep"
|
|
237
|
+
class="w-full h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="space-y-1">
|
|
241
|
+
<label class="text-[11px] text-muted-foreground">Label</label>
|
|
242
|
+
<input
|
|
243
|
+
v-model="newStep.label"
|
|
244
|
+
placeholder="ej. Revisión"
|
|
245
|
+
@keydown.enter="submitNewStep"
|
|
246
|
+
class="w-full h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="space-y-1">
|
|
251
|
+
<label class="text-[11px] text-muted-foreground">Tipo</label>
|
|
252
|
+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-1.5">
|
|
253
|
+
<button
|
|
254
|
+
v-for="t in STEP_TYPES"
|
|
255
|
+
:key="t.value"
|
|
256
|
+
type="button"
|
|
257
|
+
@click="newStep.type = t.value"
|
|
258
|
+
:class="[
|
|
259
|
+
'flex items-center gap-1.5 px-2 py-1.5 rounded-control border text-xs transition-colors text-left',
|
|
260
|
+
newStep.type === t.value
|
|
261
|
+
? 'border-primary bg-primary/10 text-primary font-medium'
|
|
262
|
+
: 'border-card-line text-muted-foreground hover:border-primary/50',
|
|
263
|
+
]"
|
|
264
|
+
>
|
|
265
|
+
<span :class="[
|
|
266
|
+
'w-2 h-2 rounded-full shrink-0',
|
|
267
|
+
t.color === 'emerald' ? 'bg-emerald-500' : t.color === 'blue' ? 'bg-blue-500' :
|
|
268
|
+
t.color === 'amber' ? 'bg-amber-500' : t.color === 'orange' ? 'bg-orange-500' :
|
|
269
|
+
t.color === 'violet' ? 'bg-violet-500' : 'bg-red-500'
|
|
270
|
+
]" />
|
|
271
|
+
{{ t.label }}
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="space-y-1">
|
|
276
|
+
<label class="text-[11px] text-muted-foreground">Descripción (opcional)</label>
|
|
277
|
+
<input
|
|
278
|
+
v-model="newStep.description"
|
|
279
|
+
placeholder="Describe qué sucede en este step"
|
|
280
|
+
class="w-full h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
<p v-if="newStepError" class="text-xs text-red-500">{{ newStepError }}</p>
|
|
284
|
+
<div class="flex justify-end gap-2">
|
|
285
|
+
<button type="button" @click="showNewStep = false" class="px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
|
|
286
|
+
<button type="button" @click="submitNewStep" :disabled="!newStep.key || !newStep.label" class="px-3 py-1.5 text-xs bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors disabled:opacity-50">Agregar</button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</Transition>
|
|
290
|
+
|
|
291
|
+
<!-- Lista de steps -->
|
|
292
|
+
<div class="rounded-card border border-card-line overflow-hidden divide-y divide-card-line">
|
|
293
|
+
|
|
294
|
+
<div v-if="!definition.steps.length" class="px-4 py-6 text-center text-sm text-muted-foreground italic">
|
|
295
|
+
No hay steps. Agrega el primero.
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div
|
|
299
|
+
v-for="(step, idx) in definition.steps"
|
|
300
|
+
:key="step.key"
|
|
301
|
+
class="bg-card"
|
|
302
|
+
>
|
|
303
|
+
<!-- Step row -->
|
|
304
|
+
<div class="flex items-center gap-2 px-3 py-2.5">
|
|
305
|
+
|
|
306
|
+
<!-- Reorder buttons -->
|
|
307
|
+
<div v-if="!readonly" class="flex flex-col gap-0.5 shrink-0">
|
|
308
|
+
<button type="button" @click="moveStepUp(idx)" :disabled="idx === 0" class="text-muted-foreground hover:text-foreground disabled:opacity-20 transition-colors">
|
|
309
|
+
<IconChevronUp :size="12" />
|
|
310
|
+
</button>
|
|
311
|
+
<button type="button" @click="moveStepDown(idx)" :disabled="idx === definition.steps.length - 1" class="text-muted-foreground hover:text-foreground disabled:opacity-20 transition-colors">
|
|
312
|
+
<IconChevronDown :size="12" />
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<!-- Type dot -->
|
|
317
|
+
<span :class="[
|
|
318
|
+
'w-2.5 h-2.5 rounded-full shrink-0',
|
|
319
|
+
stepTypeColor(step.type) === 'emerald' ? 'bg-emerald-500' :
|
|
320
|
+
stepTypeColor(step.type) === 'blue' ? 'bg-blue-500' :
|
|
321
|
+
stepTypeColor(step.type) === 'amber' ? 'bg-amber-500' :
|
|
322
|
+
stepTypeColor(step.type) === 'orange' ? 'bg-orange-500' :
|
|
323
|
+
stepTypeColor(step.type) === 'violet' ? 'bg-violet-500' :
|
|
324
|
+
stepTypeColor(step.type) === 'red' ? 'bg-red-500' : 'bg-slate-400'
|
|
325
|
+
]" />
|
|
326
|
+
|
|
327
|
+
<!-- Info -->
|
|
328
|
+
<div class="flex-1 min-w-0">
|
|
329
|
+
<template v-if="editingStep === step.key">
|
|
330
|
+
<div class="flex items-center gap-2">
|
|
331
|
+
<input v-model="stepDraft.label" @keydown.enter="saveStep" @keydown.escape="cancelEditStep" class="flex-1 h-7 px-2 text-xs rounded-control border border-primary bg-card text-foreground focus:outline-none" />
|
|
332
|
+
<input v-model="stepDraft.key" @keydown.enter="saveStep" @keydown.escape="cancelEditStep" class="w-28 h-7 px-2 text-xs font-mono rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary" />
|
|
333
|
+
<select v-model="stepDraft.type" class="h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary">
|
|
334
|
+
<option v-for="t in STEP_TYPES" :key="t.value" :value="t.value">{{ t.label }}</option>
|
|
335
|
+
</select>
|
|
336
|
+
<button type="button" @click="saveStep" class="text-xs font-medium text-primary hover:underline shrink-0">OK</button>
|
|
337
|
+
<button type="button" @click="cancelEditStep" class="text-xs text-muted-foreground hover:underline shrink-0">×</button>
|
|
338
|
+
</div>
|
|
339
|
+
</template>
|
|
340
|
+
<template v-else>
|
|
341
|
+
<div class="flex items-center gap-2">
|
|
342
|
+
<span class="text-sm font-medium text-foreground truncate">{{ step.label }}</span>
|
|
343
|
+
<span class="text-xs font-mono text-muted-foreground hidden sm:inline">{{ step.key }}</span>
|
|
344
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded-badge bg-muted text-muted-foreground shrink-0">{{ stepTypeLabel(step.type) }}</span>
|
|
345
|
+
</div>
|
|
346
|
+
<p v-if="step.description" class="text-xs text-muted-foreground truncate mt-0.5">{{ step.description }}</p>
|
|
347
|
+
</template>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<!-- Transiciones badge -->
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
@click="toggleStep(step.key)"
|
|
354
|
+
class="shrink-0 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-control hover:bg-muted"
|
|
355
|
+
>
|
|
356
|
+
<IconArrowRight :size="12" />
|
|
357
|
+
<span class="tabular-nums">{{ transitionsFrom(step.key).length }}</span>
|
|
358
|
+
<IconChevronDown :size="12" :class="['transition-transform', expandedStepKey === step.key ? 'rotate-180' : '']" />
|
|
359
|
+
</button>
|
|
360
|
+
|
|
361
|
+
<!-- Edit / Delete -->
|
|
362
|
+
<template v-if="!readonly">
|
|
363
|
+
<button v-if="editingStep !== step.key" type="button" @click="startEditStep(step)" class="shrink-0 text-xs text-muted-foreground hover:text-foreground transition-colors p-1">
|
|
364
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5">
|
|
365
|
+
<path d="M13.488 2.513a1.75 1.75 0 0 0-2.475 0L3.76 9.768a1.75 1.75 0 0 0-.455.88l-.5 2.5a.75.75 0 0 0 .875.876l2.5-.5a1.75 1.75 0 0 0 .88-.455l7.255-7.254a1.75 1.75 0 0 0 0-2.474Z" />
|
|
366
|
+
</svg>
|
|
367
|
+
</button>
|
|
368
|
+
<button type="button" @click="deleteStep(step.key)" class="shrink-0 text-red-400 hover:text-red-600 transition-colors p-1">
|
|
369
|
+
<IconTrash :size="14" />
|
|
370
|
+
</button>
|
|
371
|
+
</template>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<!-- Panel de transiciones del step (expandible) -->
|
|
375
|
+
<Transition
|
|
376
|
+
enter-active-class="transition-all duration-150 ease-out"
|
|
377
|
+
enter-from-class="opacity-0"
|
|
378
|
+
enter-to-class="opacity-100"
|
|
379
|
+
leave-active-class="transition-all duration-100"
|
|
380
|
+
leave-from-class="opacity-100"
|
|
381
|
+
leave-to-class="opacity-0"
|
|
382
|
+
>
|
|
383
|
+
<div v-if="expandedStepKey === step.key" class="border-t border-card-line bg-surface px-3 py-3 space-y-2">
|
|
384
|
+
|
|
385
|
+
<!-- Lista de transiciones -->
|
|
386
|
+
<div
|
|
387
|
+
v-for="t in transitionsFrom(step.key)"
|
|
388
|
+
:key="t.to"
|
|
389
|
+
class="rounded-control border border-card-line bg-card overflow-hidden"
|
|
390
|
+
>
|
|
391
|
+
<!-- Transition row -->
|
|
392
|
+
<div class="flex items-center gap-2 px-3 py-2">
|
|
393
|
+
<IconArrowRight :size="13" class="text-muted-foreground-2 shrink-0" />
|
|
394
|
+
<div class="flex-1 min-w-0">
|
|
395
|
+
<span class="text-xs font-medium text-foreground">
|
|
396
|
+
{{ definition.steps.find(s => s.key === t.to)?.label ?? t.to }}
|
|
397
|
+
</span>
|
|
398
|
+
<span v-if="t.label" class="text-xs text-muted-foreground ml-1.5 italic">— {{ t.label }}</span>
|
|
399
|
+
</div>
|
|
400
|
+
<button
|
|
401
|
+
type="button"
|
|
402
|
+
@click="toggleTransition(step.key, t.to)"
|
|
403
|
+
class="shrink-0 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-muted transition-colors"
|
|
404
|
+
>
|
|
405
|
+
<span v-if="t.restrictions?.length" class="font-medium">{{ t.restrictions.length }} restricción{{ t.restrictions.length !== 1 ? 'es' : '' }}</span>
|
|
406
|
+
<span v-else class="italic">sin restricciones</span>
|
|
407
|
+
<IconChevronDown :size="11" :class="['transition-transform', isTransitionExpanded(step.key, t.to) ? 'rotate-180' : '']" />
|
|
408
|
+
</button>
|
|
409
|
+
<button v-if="!readonly" type="button" @click="deleteTransition(step.key, t.to)" class="shrink-0 text-red-400 hover:text-red-600 transition-colors p-1">
|
|
410
|
+
<IconTrash :size="12" />
|
|
411
|
+
</button>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<!-- Restrictions panel -->
|
|
415
|
+
<div v-if="isTransitionExpanded(step.key, t.to)" class="border-t border-card-line bg-surface px-3 py-2.5 space-y-2">
|
|
416
|
+
|
|
417
|
+
<div
|
|
418
|
+
v-for="(r, ridx) in (t.restrictions ?? [])"
|
|
419
|
+
:key="ridx"
|
|
420
|
+
class="rounded border border-card-line bg-card p-2 space-y-1.5"
|
|
421
|
+
>
|
|
422
|
+
<div class="flex items-center gap-2">
|
|
423
|
+
<select
|
|
424
|
+
v-model="r.type"
|
|
425
|
+
:disabled="readonly"
|
|
426
|
+
@change="updateTransition(step.key, t.to, { restrictions: t.restrictions })"
|
|
427
|
+
class="flex-1 h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50"
|
|
428
|
+
>
|
|
429
|
+
<option v-for="rt in RESTRICTION_TYPES" :key="rt.value" :value="rt.value">{{ rt.label }}</option>
|
|
430
|
+
</select>
|
|
431
|
+
<button v-if="!readonly" type="button" @click="removeRestriction(step.key, t.to, ridx)" class="text-red-400 hover:text-red-600 transition-colors shrink-0">
|
|
432
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3"><path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" /></svg>
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
435
|
+
<input
|
|
436
|
+
v-model="r.message"
|
|
437
|
+
:disabled="readonly"
|
|
438
|
+
placeholder="Mensaje de error al usuario…"
|
|
439
|
+
@blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })"
|
|
440
|
+
class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50"
|
|
441
|
+
/>
|
|
442
|
+
<!-- Campos específicos por tipo -->
|
|
443
|
+
<input v-if="r.type === 'role'" v-model="r.roles" :disabled="readonly" placeholder="admin, manager" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
444
|
+
<input v-if="r.type === 'required_fields'" v-model="r.fields" :disabled="readonly" placeholder="campo1, campo2" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
445
|
+
<input v-if="r.type === 'checklist'" v-model="r.checklist" :disabled="readonly" placeholder="nombre del checklist" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
446
|
+
<input v-if="r.type === 'document'" v-model="r.document" :disabled="readonly" placeholder="nombre del documento" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
447
|
+
<input v-if="r.type === 'approval'" v-model="r.role" :disabled="readonly" placeholder="rol que debe aprobar" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
448
|
+
<div v-if="r.type === 'min_items'" class="flex gap-1.5">
|
|
449
|
+
<input v-model="r.relation" :disabled="readonly" placeholder="relación (ej. attachments)" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="flex-1 h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
450
|
+
<input v-model.number="r.min" :disabled="readonly" type="number" placeholder="min" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-16 h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
451
|
+
</div>
|
|
452
|
+
<input v-if="r.type === 'date'" v-model="r.after" :disabled="readonly" type="date" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<p v-if="!t.restrictions?.length" class="text-[11px] text-muted-foreground italic">Sin restricciones — cualquier usuario puede transicionar</p>
|
|
456
|
+
|
|
457
|
+
<button
|
|
458
|
+
v-if="!readonly"
|
|
459
|
+
type="button"
|
|
460
|
+
@click="addRestrictionTo(step.key, t.to)"
|
|
461
|
+
class="text-xs text-primary hover:underline"
|
|
462
|
+
>
|
|
463
|
+
+ Agregar restricción
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Form nueva transición -->
|
|
469
|
+
<div v-if="showingAddTransition === step.key" class="rounded-control border border-primary/30 bg-primary/5 p-2.5 space-y-2">
|
|
470
|
+
<div class="flex items-center gap-2">
|
|
471
|
+
<select
|
|
472
|
+
v-model="newTransition.to"
|
|
473
|
+
class="flex-1 h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
|
|
474
|
+
>
|
|
475
|
+
<option value="" disabled>Destino…</option>
|
|
476
|
+
<option v-for="s in availableTargets(step.key)" :key="s.key" :value="s.key">{{ s.label }}</option>
|
|
477
|
+
</select>
|
|
478
|
+
<input
|
|
479
|
+
v-model="newTransition.label"
|
|
480
|
+
placeholder="Label (opcional)"
|
|
481
|
+
class="flex-1 h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
484
|
+
<p v-if="newTransitionError" class="text-xs text-red-500">{{ newTransitionError }}</p>
|
|
485
|
+
<div class="flex justify-end gap-2">
|
|
486
|
+
<button type="button" @click="showingAddTransition = null" class="px-2.5 py-1 text-xs text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
|
|
487
|
+
<button type="button" @click="submitTransition(step.key)" :disabled="!newTransition.to" class="px-2.5 py-1 text-xs bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors disabled:opacity-50">Agregar</button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<!-- Botón agregar transición -->
|
|
492
|
+
<button
|
|
493
|
+
v-if="!readonly && showingAddTransition !== step.key && !['finished','cancelled'].includes(step.type)"
|
|
494
|
+
type="button"
|
|
495
|
+
@click="openAddTransition(step.key)"
|
|
496
|
+
class="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
|
497
|
+
>
|
|
498
|
+
<IconPlus :size="12" /> Agregar transición
|
|
499
|
+
</button>
|
|
500
|
+
|
|
501
|
+
<p v-if="['finished','cancelled'].includes(step.type)" class="text-xs text-muted-foreground italic">
|
|
502
|
+
Los steps terminales no tienen transiciones salientes.
|
|
503
|
+
</p>
|
|
504
|
+
</div>
|
|
505
|
+
</Transition>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<!-- ── Footer con estado ──────────────────────────────────────────────── -->
|
|
511
|
+
<div class="flex items-center gap-3 text-xs text-muted-foreground pt-1 border-t border-card-line">
|
|
512
|
+
<span>{{ definition.steps.length }} steps · {{ definition.transitions.length }} transiciones</span>
|
|
513
|
+
<span
|
|
514
|
+
v-if="isValid"
|
|
515
|
+
class="inline-flex items-center gap-1 text-emerald-600 dark:text-emerald-400"
|
|
516
|
+
>
|
|
517
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5">
|
|
518
|
+
<path fill-rule="evenodd" d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd" />
|
|
519
|
+
</svg>
|
|
520
|
+
Definición válida
|
|
521
|
+
</span>
|
|
522
|
+
<span v-if="isDirty && !readonly" class="text-amber-500">· cambios sin guardar</span>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
</div>
|
|
526
|
+
</template>
|