@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,454 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch, markRaw, nextTick } from 'vue'
|
|
3
|
+
import { VueFlow, Panel } from '@vue-flow/core'
|
|
4
|
+
import { Background } from '@vue-flow/background'
|
|
5
|
+
import { Controls } from '@vue-flow/controls'
|
|
6
|
+
import StepNode from './nodes/StepNode.vue'
|
|
7
|
+
import TransitionEdge from './edges/TransitionEdge.vue'
|
|
8
|
+
import { definitionToFlow, flowToDefinition, autoLayoutWorkflow } from '~/composables/useWorkflowLayout.js'
|
|
9
|
+
|
|
10
|
+
// ── Props & emits ─────────────────────────────────────────────────────────────
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
modelValue: { type: Object, default: () => ({ steps: [], transitions: [] }) },
|
|
13
|
+
readonly: { type: Boolean, default: false },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits(['update:modelValue', 'node-click', 'edge-click', 'connect'])
|
|
17
|
+
|
|
18
|
+
// ── Vue Flow types ────────────────────────────────────────────────────────────
|
|
19
|
+
const nodeTypes = markRaw({ step: StepNode })
|
|
20
|
+
const edgeTypes = markRaw({ transition: TransitionEdge })
|
|
21
|
+
|
|
22
|
+
// ── Local nodes/edges refs (controlled) ──────────────────────────────────────
|
|
23
|
+
const flowNodes = ref([])
|
|
24
|
+
const flowEdges = ref([])
|
|
25
|
+
|
|
26
|
+
let _skipEmit = false
|
|
27
|
+
|
|
28
|
+
watch(
|
|
29
|
+
() => props.modelValue,
|
|
30
|
+
(def) => {
|
|
31
|
+
_skipEmit = true
|
|
32
|
+
const { nodes: n, edges: e } = definitionToFlow(def)
|
|
33
|
+
flowNodes.value = n
|
|
34
|
+
flowEdges.value = e
|
|
35
|
+
nextTick(() => { _skipEmit = false })
|
|
36
|
+
},
|
|
37
|
+
{ immediate: true, deep: true },
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const emitDefinition = () => {
|
|
41
|
+
if (props.readonly || _skipEmit) return
|
|
42
|
+
emit('update:modelValue', flowToDefinition(flowNodes.value, flowEdges.value))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── View mode ─────────────────────────────────────────────────────────────────
|
|
46
|
+
const viewMode = ref('flow')
|
|
47
|
+
|
|
48
|
+
// ── Re-layout ─────────────────────────────────────────────────────────────────
|
|
49
|
+
const relayout = () => {
|
|
50
|
+
const steps = flowNodes.value.map(n => n.data)
|
|
51
|
+
const transitions = flowEdges.value.map(e => ({ from: e.source, to: e.target }))
|
|
52
|
+
const positions = autoLayoutWorkflow(steps, transitions)
|
|
53
|
+
flowNodes.value = flowNodes.value.map(n => ({
|
|
54
|
+
...n,
|
|
55
|
+
position: positions[n.id] ?? n.position,
|
|
56
|
+
}))
|
|
57
|
+
// Forzar re-render del canvas con nueva key
|
|
58
|
+
flowKey.value++
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const flowKey = ref(0)
|
|
62
|
+
|
|
63
|
+
// ── Vue Flow events ───────────────────────────────────────────────────────────
|
|
64
|
+
const onNodesChange = (changes) => {
|
|
65
|
+
// Vue Flow muta los nodes internamente; sólo emitimos tras posición/cambio
|
|
66
|
+
nextTick(emitDefinition)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const onEdgesChange = () => {
|
|
70
|
+
nextTick(emitDefinition)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const onConnect = (params) => {
|
|
74
|
+
if (props.readonly) return
|
|
75
|
+
flowEdges.value = [
|
|
76
|
+
...flowEdges.value,
|
|
77
|
+
{
|
|
78
|
+
id: `${params.source}__${params.target}`,
|
|
79
|
+
source: params.source,
|
|
80
|
+
target: params.target,
|
|
81
|
+
type: 'transition',
|
|
82
|
+
label: '',
|
|
83
|
+
data: { from: params.source, to: params.target, label: '', restrictions: [] },
|
|
84
|
+
markerEnd: { type: 'arrowclosed' },
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
emit('connect', params)
|
|
88
|
+
nextTick(emitDefinition)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Config panel ──────────────────────────────────────────────────────────────
|
|
92
|
+
const selectedItem = ref(null)
|
|
93
|
+
const showPanel = ref(false)
|
|
94
|
+
|
|
95
|
+
const onNodeClick = ({ node }) => {
|
|
96
|
+
selectedItem.value = { type: 'step', id: node.id, data: { ...node.data } }
|
|
97
|
+
showPanel.value = true
|
|
98
|
+
emit('node-click', node)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const onEdgeClick = ({ edge }) => {
|
|
102
|
+
selectedItem.value = { type: 'transition', id: edge.id, data: { ...edge.data, label: edge.label ?? '' } }
|
|
103
|
+
showPanel.value = true
|
|
104
|
+
emit('edge-click', edge)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const closePanel = () => { showPanel.value = false; selectedItem.value = null }
|
|
108
|
+
|
|
109
|
+
const savePanel = () => {
|
|
110
|
+
if (!selectedItem.value || props.readonly) return
|
|
111
|
+
if (selectedItem.value.type === 'step') {
|
|
112
|
+
flowNodes.value = flowNodes.value.map(n =>
|
|
113
|
+
n.id === selectedItem.value.id
|
|
114
|
+
? { ...n, id: selectedItem.value.data.key, data: { ...selectedItem.value.data } }
|
|
115
|
+
: n
|
|
116
|
+
)
|
|
117
|
+
// Update edges if key changed
|
|
118
|
+
if (selectedItem.value.id !== selectedItem.value.data.key) {
|
|
119
|
+
const oldKey = selectedItem.value.id
|
|
120
|
+
const newKey = selectedItem.value.data.key
|
|
121
|
+
flowEdges.value = flowEdges.value.map(e => ({
|
|
122
|
+
...e,
|
|
123
|
+
source: e.source === oldKey ? newKey : e.source,
|
|
124
|
+
target: e.target === oldKey ? newKey : e.target,
|
|
125
|
+
id: e.id.replace(oldKey, newKey),
|
|
126
|
+
}))
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
flowEdges.value = flowEdges.value.map(e =>
|
|
130
|
+
e.id === selectedItem.value.id
|
|
131
|
+
? { ...e, data: { ...selectedItem.value.data }, label: selectedItem.value.data.label ?? '' }
|
|
132
|
+
: e
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
emitDefinition()
|
|
136
|
+
closePanel()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const deleteSelected = () => {
|
|
140
|
+
if (!selectedItem.value || props.readonly) return
|
|
141
|
+
if (selectedItem.value.type === 'step') {
|
|
142
|
+
flowNodes.value = flowNodes.value.filter(n => n.id !== selectedItem.value.id)
|
|
143
|
+
flowEdges.value = flowEdges.value.filter(e =>
|
|
144
|
+
e.source !== selectedItem.value.id && e.target !== selectedItem.value.id
|
|
145
|
+
)
|
|
146
|
+
} else {
|
|
147
|
+
flowEdges.value = flowEdges.value.filter(e => e.id !== selectedItem.value.id)
|
|
148
|
+
}
|
|
149
|
+
emitDefinition()
|
|
150
|
+
closePanel()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Add step ──────────────────────────────────────────────────────────────────
|
|
154
|
+
const STEP_TYPES = ['start', 'in_progress', 'pause_internal', 'pause_external', 'finished', 'cancelled']
|
|
155
|
+
const STEP_LABELS = {
|
|
156
|
+
start: 'Inicio',
|
|
157
|
+
in_progress: 'En progreso',
|
|
158
|
+
pause_internal: 'Pausa interna',
|
|
159
|
+
pause_external: 'Pausa externa',
|
|
160
|
+
finished: 'Finalizado',
|
|
161
|
+
cancelled: 'Cancelado',
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const showAddStep = ref(false)
|
|
165
|
+
const newStep = ref({ key: '', label: '', type: 'in_progress', description: '' })
|
|
166
|
+
|
|
167
|
+
const addStep = () => {
|
|
168
|
+
if (!newStep.value.key || !newStep.value.label) return
|
|
169
|
+
const col = flowNodes.value.length
|
|
170
|
+
flowNodes.value = [
|
|
171
|
+
...flowNodes.value,
|
|
172
|
+
{
|
|
173
|
+
id: newStep.value.key,
|
|
174
|
+
type: 'step',
|
|
175
|
+
position: { x: col * 310, y: 0 },
|
|
176
|
+
data: { ...newStep.value },
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
emitDefinition()
|
|
180
|
+
newStep.value = { key: '', label: '', type: 'in_progress', description: '' }
|
|
181
|
+
showAddStep.value = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Restrictions ──────────────────────────────────────────────────────────────
|
|
185
|
+
const RESTRICTION_TYPES = [
|
|
186
|
+
{ value: 'role', label: 'Rol' },
|
|
187
|
+
{ value: 'required_fields', label: 'Campos requeridos' },
|
|
188
|
+
{ value: 'approval', label: 'Aprobación' },
|
|
189
|
+
{ value: 'checklist', label: 'Checklist' },
|
|
190
|
+
{ value: 'document', label: 'Documento' },
|
|
191
|
+
{ value: 'min_items', label: 'Mínimo ítems' },
|
|
192
|
+
{ value: 'date', label: 'Fecha' },
|
|
193
|
+
{ value: 'custom', label: 'Personalizado' },
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
const addRestriction = () => {
|
|
197
|
+
if (!selectedItem.value?.data) return
|
|
198
|
+
selectedItem.value.data.restrictions = [
|
|
199
|
+
...(selectedItem.value.data.restrictions ?? []),
|
|
200
|
+
{ type: 'role', roles: [], message: '' },
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const removeRestriction = (idx) => {
|
|
205
|
+
selectedItem.value.data.restrictions.splice(idx, 1)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── List view ─────────────────────────────────────────────────────────────────
|
|
209
|
+
const stepsWithTransitions = computed(() =>
|
|
210
|
+
(props.modelValue.steps ?? []).map(step => ({
|
|
211
|
+
...step,
|
|
212
|
+
outgoing: (props.modelValue.transitions ?? []).filter(t => t.from === step.key),
|
|
213
|
+
}))
|
|
214
|
+
)
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<div class="flex flex-col h-full min-h-0">
|
|
219
|
+
|
|
220
|
+
<!-- Toolbar -->
|
|
221
|
+
<div class="flex items-center justify-between gap-3 px-3 py-2 border-b border-card-line bg-surface shrink-0">
|
|
222
|
+
<div class="inline-flex items-center rounded-control border border-card-line bg-card overflow-hidden">
|
|
223
|
+
<button
|
|
224
|
+
v-for="mode in [{ id: 'flow', label: 'Flujo' }, { id: 'list', label: 'Lista' }]"
|
|
225
|
+
:key="mode.id"
|
|
226
|
+
type="button"
|
|
227
|
+
@click="viewMode = mode.id"
|
|
228
|
+
:class="[
|
|
229
|
+
'px-3 py-1.5 text-xs font-medium transition-colors',
|
|
230
|
+
viewMode === mode.id
|
|
231
|
+
? 'bg-primary text-primary-foreground'
|
|
232
|
+
: 'text-muted-foreground hover:bg-muted-hover',
|
|
233
|
+
]"
|
|
234
|
+
>
|
|
235
|
+
{{ mode.label }}
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div v-if="viewMode === 'flow'" class="flex items-center gap-2">
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
@click="relayout"
|
|
243
|
+
class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-control transition-colors"
|
|
244
|
+
>
|
|
245
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
|
|
246
|
+
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" clip-rule="evenodd" />
|
|
247
|
+
</svg>
|
|
248
|
+
Re-layout
|
|
249
|
+
</button>
|
|
250
|
+
<button
|
|
251
|
+
v-if="!readonly"
|
|
252
|
+
type="button"
|
|
253
|
+
@click="showAddStep = true"
|
|
254
|
+
class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors"
|
|
255
|
+
>
|
|
256
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
|
|
257
|
+
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z" />
|
|
258
|
+
</svg>
|
|
259
|
+
Agregar step
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Content -->
|
|
265
|
+
<div class="flex flex-1 min-h-0 relative overflow-hidden">
|
|
266
|
+
|
|
267
|
+
<!-- ── FLOW VIEW ────────────────────────────────────────────────────── -->
|
|
268
|
+
<div v-if="viewMode === 'flow'" class="flex-1 min-w-0 h-full">
|
|
269
|
+
<VueFlow
|
|
270
|
+
:key="flowKey"
|
|
271
|
+
v-model:nodes="flowNodes"
|
|
272
|
+
v-model:edges="flowEdges"
|
|
273
|
+
:node-types="nodeTypes"
|
|
274
|
+
:edge-types="edgeTypes"
|
|
275
|
+
:nodes-draggable="!readonly"
|
|
276
|
+
:nodes-connectable="!readonly"
|
|
277
|
+
:elements-selectable="true"
|
|
278
|
+
fit-view-on-init
|
|
279
|
+
class="h-full bg-background"
|
|
280
|
+
@nodes-change="onNodesChange"
|
|
281
|
+
@edges-change="onEdgesChange"
|
|
282
|
+
@connect="onConnect"
|
|
283
|
+
@node-click="onNodeClick"
|
|
284
|
+
@edge-click="onEdgeClick"
|
|
285
|
+
>
|
|
286
|
+
<Background pattern-color="var(--color-card-line)" :gap="20" />
|
|
287
|
+
<Controls :show-interactive="!readonly" />
|
|
288
|
+
</VueFlow>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<!-- ── LIST VIEW ─────────────────────────────────────────────────────── -->
|
|
292
|
+
<div v-if="viewMode === 'list'" class="flex-1 overflow-y-auto p-4 space-y-3 h-full">
|
|
293
|
+
<div
|
|
294
|
+
v-for="step in stepsWithTransitions"
|
|
295
|
+
:key="step.key"
|
|
296
|
+
class="bg-card rounded-card border border-card-line overflow-hidden"
|
|
297
|
+
>
|
|
298
|
+
<div class="flex items-center gap-3 px-4 py-3 border-b border-card-line/50">
|
|
299
|
+
<span :class="[
|
|
300
|
+
'w-2 h-2 rounded-full shrink-0',
|
|
301
|
+
{ 'bg-emerald-500': step.type === 'start', 'bg-blue-500': step.type === 'in_progress',
|
|
302
|
+
'bg-amber-500': step.type === 'pause_internal', 'bg-orange-500': step.type === 'pause_external',
|
|
303
|
+
'bg-violet-500': step.type === 'finished', 'bg-red-500': step.type === 'cancelled' },
|
|
304
|
+
]" />
|
|
305
|
+
<div class="flex-1 min-w-0">
|
|
306
|
+
<p class="text-sm font-medium text-foreground truncate">{{ step.label }}</p>
|
|
307
|
+
<p class="text-xs text-muted-foreground font-mono">{{ step.key }} · {{ STEP_LABELS[step.type] }}</p>
|
|
308
|
+
</div>
|
|
309
|
+
<span class="text-xs text-muted-foreground">
|
|
310
|
+
{{ step.outgoing.length }} transición{{ step.outgoing.length !== 1 ? 'es' : '' }}
|
|
311
|
+
</span>
|
|
312
|
+
</div>
|
|
313
|
+
<div v-if="step.outgoing.length" class="divide-y divide-card-line/50">
|
|
314
|
+
<div v-for="t in step.outgoing" :key="t.to" class="flex items-center gap-3 px-4 py-2.5 text-sm">
|
|
315
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4 text-muted-foreground-2 shrink-0">
|
|
316
|
+
<path fill-rule="evenodd" d="M2 10a.75.75 0 0 1 .75-.75h12.59l-2.1-1.95a.75.75 0 1 1 1.02-1.1l3.5 3.25a.75.75 0 0 1 0 1.1l-3.5 3.25a.75.75 0 1 1-1.02-1.1l2.1-1.95H2.75A.75.75 0 0 1 2 10Z" clip-rule="evenodd" />
|
|
317
|
+
</svg>
|
|
318
|
+
<span class="flex-1 text-foreground">{{ t.label || t.to }}</span>
|
|
319
|
+
<span v-if="t.restrictions?.length" class="text-xs px-1.5 py-0.5 rounded-badge bg-muted text-muted-foreground">
|
|
320
|
+
{{ t.restrictions.length }} restricción{{ t.restrictions.length !== 1 ? 'es' : '' }}
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
<div v-else class="px-4 py-2.5 text-xs text-muted-foreground italic">Sin transiciones salientes</div>
|
|
325
|
+
</div>
|
|
326
|
+
<div v-if="!stepsWithTransitions.length" class="flex items-center justify-center py-16 text-muted-foreground text-sm">
|
|
327
|
+
No hay steps definidos
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<!-- ── CONFIG PANEL ──────────────────────────────────────────────────── -->
|
|
332
|
+
<Transition
|
|
333
|
+
enter-active-class="transition-all duration-200 ease-out"
|
|
334
|
+
enter-from-class="translate-x-full opacity-0"
|
|
335
|
+
enter-to-class="translate-x-0 opacity-100"
|
|
336
|
+
leave-active-class="transition-all duration-150 ease-in"
|
|
337
|
+
leave-from-class="translate-x-0 opacity-100"
|
|
338
|
+
leave-to-class="translate-x-full opacity-0"
|
|
339
|
+
>
|
|
340
|
+
<div
|
|
341
|
+
v-if="showPanel && selectedItem"
|
|
342
|
+
class="absolute right-0 top-0 bottom-0 w-80 bg-card border-l border-card-line shadow-xl z-10 flex flex-col overflow-hidden"
|
|
343
|
+
>
|
|
344
|
+
<div class="flex items-center justify-between px-4 py-3 border-b border-card-line shrink-0">
|
|
345
|
+
<p class="text-sm font-medium text-foreground">
|
|
346
|
+
{{ selectedItem.type === 'step' ? 'Configurar step' : 'Configurar transición' }}
|
|
347
|
+
</p>
|
|
348
|
+
<button type="button" @click="closePanel" class="p-1 text-muted-foreground hover:text-foreground transition-colors">
|
|
349
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
|
350
|
+
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
|
351
|
+
</svg>
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
|
356
|
+
<template v-if="selectedItem.type === 'step'">
|
|
357
|
+
<div class="space-y-1">
|
|
358
|
+
<label class="text-xs font-medium text-foreground">Clave (key)</label>
|
|
359
|
+
<input v-model="selectedItem.data.key" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
360
|
+
</div>
|
|
361
|
+
<div class="space-y-1">
|
|
362
|
+
<label class="text-xs font-medium text-foreground">Label</label>
|
|
363
|
+
<input v-model="selectedItem.data.label" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
364
|
+
</div>
|
|
365
|
+
<div class="space-y-1">
|
|
366
|
+
<label class="text-xs font-medium text-foreground">Tipo</label>
|
|
367
|
+
<select v-model="selectedItem.data.type" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50">
|
|
368
|
+
<option v-for="t in STEP_TYPES" :key="t" :value="t">{{ STEP_LABELS[t] }} ({{ t }})</option>
|
|
369
|
+
</select>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="space-y-1">
|
|
372
|
+
<label class="text-xs font-medium text-foreground">Descripción</label>
|
|
373
|
+
<textarea v-model="selectedItem.data.description" :disabled="readonly" rows="2" class="w-full px-2.5 py-1.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50 resize-none" />
|
|
374
|
+
</div>
|
|
375
|
+
</template>
|
|
376
|
+
|
|
377
|
+
<template v-else-if="selectedItem.type === 'transition'">
|
|
378
|
+
<div class="space-y-1">
|
|
379
|
+
<label class="text-xs font-medium text-foreground">Label</label>
|
|
380
|
+
<input v-model="selectedItem.data.label" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
|
|
381
|
+
</div>
|
|
382
|
+
<div class="space-y-1">
|
|
383
|
+
<label class="text-xs font-medium text-foreground">Descripción</label>
|
|
384
|
+
<textarea v-model="selectedItem.data.description" :disabled="readonly" rows="2" class="w-full px-2.5 py-1.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50 resize-none" />
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<div class="space-y-2">
|
|
388
|
+
<div class="flex items-center justify-between">
|
|
389
|
+
<label class="text-xs font-medium text-foreground">Restricciones</label>
|
|
390
|
+
<button v-if="!readonly" type="button" @click="addRestriction" class="text-xs text-primary hover:underline">+ Agregar</button>
|
|
391
|
+
</div>
|
|
392
|
+
<div v-for="(r, idx) in selectedItem.data.restrictions ?? []" :key="idx" class="rounded-control border border-card-line bg-surface p-2.5 space-y-2">
|
|
393
|
+
<div class="flex items-center gap-2">
|
|
394
|
+
<select v-model="r.type" :disabled="readonly" class="flex-1 h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50">
|
|
395
|
+
<option v-for="rt in RESTRICTION_TYPES" :key="rt.value" :value="rt.value">{{ rt.label }}</option>
|
|
396
|
+
</select>
|
|
397
|
+
<button v-if="!readonly" type="button" @click="removeRestriction(idx)" class="text-red-400 hover:text-red-600 transition-colors">
|
|
398
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><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>
|
|
399
|
+
</button>
|
|
400
|
+
</div>
|
|
401
|
+
<input v-model="r.message" :disabled="readonly" placeholder="Mensaje de error…" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
|
|
402
|
+
<input v-if="r.type === 'role'" v-model="r.roles" :disabled="readonly" placeholder="admin, manager" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
|
|
403
|
+
<input v-if="r.type === 'required_fields'" v-model="r.fields" :disabled="readonly" placeholder="campo1, campo2" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
|
|
404
|
+
<input v-if="r.type === 'checklist'" v-model="r.checklist" :disabled="readonly" placeholder="nombre del checklist" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
|
|
405
|
+
<input v-if="r.type === 'document'" v-model="r.document" :disabled="readonly" placeholder="nombre del documento" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
|
|
406
|
+
<input v-if="r.type === 'approval'" v-model="r.role" :disabled="readonly" placeholder="rol que debe aprobar" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
|
|
407
|
+
</div>
|
|
408
|
+
<p v-if="!selectedItem.data.restrictions?.length" class="text-xs text-muted-foreground italic">Sin restricciones</p>
|
|
409
|
+
</div>
|
|
410
|
+
</template>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<div class="flex items-center gap-2 px-4 py-3 border-t border-card-line shrink-0">
|
|
414
|
+
<button v-if="!readonly" type="button" @click="deleteSelected" class="px-3 py-1.5 text-xs font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-control transition-colors">Eliminar</button>
|
|
415
|
+
<div class="flex-1" />
|
|
416
|
+
<button type="button" @click="closePanel" class="px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
|
|
417
|
+
<button v-if="!readonly" type="button" @click="savePanel" class="px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors">Guardar</button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</Transition>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<!-- Modal: Agregar step -->
|
|
424
|
+
<Teleport to="body">
|
|
425
|
+
<Transition enter-active-class="transition-opacity duration-150" enter-from-class="opacity-0" enter-to-class="opacity-100" leave-active-class="transition-opacity duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
|
|
426
|
+
<div v-if="showAddStep" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40" @click.self="showAddStep = false">
|
|
427
|
+
<div class="bg-card rounded-modal border border-card-line shadow-xl w-full max-w-sm p-5 space-y-4">
|
|
428
|
+
<p class="text-sm font-semibold text-foreground">Nuevo step</p>
|
|
429
|
+
<div class="space-y-3">
|
|
430
|
+
<div class="space-y-1">
|
|
431
|
+
<label class="text-xs font-medium text-foreground">Clave (key)</label>
|
|
432
|
+
<input v-model="newStep.key" placeholder="ej. revision" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary" />
|
|
433
|
+
</div>
|
|
434
|
+
<div class="space-y-1">
|
|
435
|
+
<label class="text-xs font-medium text-foreground">Label</label>
|
|
436
|
+
<input v-model="newStep.label" placeholder="ej. Revisión" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary" />
|
|
437
|
+
</div>
|
|
438
|
+
<div class="space-y-1">
|
|
439
|
+
<label class="text-xs font-medium text-foreground">Tipo</label>
|
|
440
|
+
<select v-model="newStep.type" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary">
|
|
441
|
+
<option v-for="t in STEP_TYPES" :key="t" :value="t">{{ STEP_LABELS[t] }} ({{ t }})</option>
|
|
442
|
+
</select>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="flex justify-end gap-2">
|
|
446
|
+
<button type="button" @click="showAddStep = false" class="px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
|
|
447
|
+
<button type="button" @click="addStep" :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">Crear</button>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</Transition>
|
|
452
|
+
</Teleport>
|
|
453
|
+
</div>
|
|
454
|
+
</template>
|