Files
Lesstime/frontend/components/admin/WorkflowDrawer.vue

262 lines
8.7 KiB
Vue

<template>
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('workflows.editWorkflow') : $t('workflows.addWorkflow')">
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<MalioInputText
v-model="form.name"
:label="$t('workflows.name')"
input-class="w-full"
:error="touched.name && !form.name.trim() ? $t('workflows.name') + ' requis' : ''"
@blur="touched.name = true"
/>
<div class="flex items-center gap-2">
<input
id="isDefault"
v-model="form.isDefault"
type="checkbox"
class="h-4 w-4 rounded border-neutral-300 text-primary-500 focus:ring-primary-500"
/>
<label for="isDefault" class="text-sm font-medium text-neutral-700">
{{ $t('workflows.isDefault') }}
</label>
</div>
<div class="mt-2">
<div class="flex items-center justify-between">
<h3 class="text-sm font-bold text-neutral-900">{{ $t('workflows.statuses') }}</h3>
<MalioButton
type="button"
icon-name="mdi:plus"
icon-position="left"
button-class="w-auto px-3 py-1 text-xs"
:label="$t('workflows.addStatus')"
@click="addStatus"
/>
</div>
<div class="mt-3 flex flex-col gap-3">
<div
v-for="(s, idx) in form.statuses"
:key="idx"
class="rounded border border-neutral-200 p-3"
>
<div class="flex items-end gap-2">
<MalioInputText
v-model="s.label"
label="Libellé"
input-class="w-full"
/>
<select
v-model="s.category"
class="h-10 rounded border border-neutral-300 px-2 text-sm"
aria-label="Catégorie"
>
<option v-for="c in categoryOptions" :key="c.value" :value="c.value">
{{ c.label }}
</option>
</select>
<button
type="button"
class="h-10 px-2 text-red-600 hover:text-red-800"
aria-label="Supprimer"
@click="removeStatus(idx)"
>
<Icon name="mdi:delete" size="20" />
</button>
</div>
<div class="mt-2 flex items-center gap-3">
<ColorPicker v-model="s.color" />
<label class="ml-auto flex items-center gap-1 text-xs text-neutral-700">
<input v-model="s.isFinal" type="checkbox" class="h-3 w-3" />
{{ $t('archive.statusFinal') }}
</label>
<label class="flex flex-col text-xs text-neutral-700">
Position
<input
v-model.number="s.position"
type="number"
class="mt-1 h-9 w-16 rounded border border-neutral-300 px-2 text-sm"
/>
</label>
</div>
</div>
</div>
</div>
<div class="mt-4 flex justify-end">
<MalioButton
label="Enregistrer"
button-class="w-auto px-6"
:disabled="isSubmitting"
@click="handleSubmit"
/>
</div>
</form>
</MalioDrawer>
</template>
<script setup lang="ts">
import type { Workflow, StatusCategory } from '~/services/dto/workflow'
import type { TaskStatusWrite } from '~/services/dto/task-status'
import { useWorkflowService } from '~/services/workflows'
import { useTaskStatusService } from '~/services/task-statuses'
const { t } = useI18n()
const props = defineProps<{
modelValue: boolean
item: Workflow | null
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved'): void
}>()
const isOpen = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v),
})
const isEditing = computed(() => !!props.item)
const isSubmitting = ref(false)
type StatusForm = {
id?: number
label: string
color: string
position: number
isFinal: boolean
category: StatusCategory
}
const form = reactive<{
name: string
isDefault: boolean
statuses: StatusForm[]
}>({
name: '',
isDefault: false,
statuses: [],
})
const touched = reactive({ name: false })
const categoryOptions: { value: StatusCategory, label: string }[] = [
{ value: 'todo', label: t('workflows.categories.todo') },
{ value: 'in_progress', label: t('workflows.categories.in_progress') },
{ value: 'blocked', label: t('workflows.categories.blocked') },
{ value: 'review', label: t('workflows.categories.review') },
{ value: 'done', label: t('workflows.categories.done') },
]
watch(() => props.modelValue, (open) => {
if (!open) return
if (props.item) {
form.name = props.item.name
form.isDefault = props.item.isDefault
form.statuses = props.item.statuses.map(s => ({
id: s.id,
label: s.label,
color: s.color,
position: s.position,
isFinal: s.isFinal,
category: s.category,
}))
} else {
form.name = ''
form.isDefault = false
form.statuses = []
}
touched.name = false
})
function addStatus() {
form.statuses.push({
label: '',
color: '#222783',
position: form.statuses.length,
isFinal: false,
category: 'todo',
})
}
function removeStatus(idx: number) {
form.statuses.splice(idx, 1)
}
const workflowService = useWorkflowService()
const statusService = useTaskStatusService()
async function handleSubmit() {
touched.name = true
if (!form.name.trim()) return
isSubmitting.value = true
try {
if (isEditing.value && props.item) {
await workflowService.update(props.item.id, {
name: form.name.trim(),
isDefault: form.isDefault,
position: props.item.position,
})
await syncStatuses(props.item)
} else {
const created = await workflowService.create({
name: form.name.trim(),
isDefault: form.isDefault,
position: 0,
})
for (const s of form.statuses) {
const payload: TaskStatusWrite = {
label: s.label,
color: s.color,
position: s.position,
isFinal: s.isFinal,
category: s.category,
workflow: `/api/workflows/${created.id}`,
}
await statusService.create(payload)
}
}
emit('saved')
isOpen.value = false
} finally {
isSubmitting.value = false
}
}
async function syncStatuses(workflow: Workflow) {
const existingIds = new Set(workflow.statuses.map(s => s.id))
const keptIds = new Set<number>()
for (const s of form.statuses) {
if (s.id) {
keptIds.add(s.id)
await statusService.update(s.id, {
label: s.label,
color: s.color,
position: s.position,
isFinal: s.isFinal,
category: s.category,
})
} else {
await statusService.create({
label: s.label,
color: s.color,
position: s.position,
isFinal: s.isFinal,
category: s.category,
workflow: `/api/workflows/${workflow.id}`,
})
}
}
for (const id of existingIds) {
if (id && !keptIds.has(id)) {
await statusService.remove(id)
}
}
}
</script>