208 lines
5.9 KiB
Vue
208 lines
5.9 KiB
Vue
<template>
|
|
<Dialog
|
|
:visible="visible"
|
|
:header="task ? 'Edit Task' : 'New Task'"
|
|
:modal="true"
|
|
style="width: min(520px, 92vw)"
|
|
@update:visible="emit('update:visible', $event)"
|
|
>
|
|
<div class="field">
|
|
<label>Title</label>
|
|
<InputText v-model.trim="form.title" class="w-full" autofocus />
|
|
</div>
|
|
<div class="field">
|
|
<label>Description</label>
|
|
<Textarea v-model="form.description" rows="3" class="w-full" />
|
|
</div>
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<div class="field">
|
|
<label>Status</label>
|
|
<Select v-model="form.status" :options="statusOptions" optionLabel="label" optionValue="value" class="w-full" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Priority</label>
|
|
<Select v-model="form.priority" :options="priorityOptions" optionLabel="label" optionValue="value" class="w-full" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Assignee</label>
|
|
<Select
|
|
v-model="form.assigneeId"
|
|
:options="assigneeOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
showClear
|
|
placeholder="Unassigned"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
<div class="field">
|
|
<label>Due date</label>
|
|
<DatePicker v-model="form.dueDate" class="w-full" dateFormat="yy-mm-dd" showClear />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="savingError" class="mb-2">
|
|
<Message severity="error" variant="simple">{{ savingError }}</Message>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-between">
|
|
<Button
|
|
v-if="task && canDelete"
|
|
label="Delete"
|
|
icon="pi pi-trash"
|
|
severity="danger"
|
|
text
|
|
@click="onDelete"
|
|
/>
|
|
<div>
|
|
<Button label="Cancel" severity="secondary" text @click="emit('update:visible', false)" />
|
|
<Button v-if="canEdit" label="Save" :loading="saving" @click="onSave" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { createTask, updateTask, deleteTask } from '../../services/modules'
|
|
import { getMasterDataOptions } from '../../services/backend'
|
|
import { errorMessage } from '../../services/api'
|
|
import type { Task, TaskPriority, TaskStatus } from '../../types'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
task: Task | null
|
|
projectId: string
|
|
members: { userId: string; displayName: string }[]
|
|
canEdit: boolean
|
|
canDelete: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:visible': [value: boolean]
|
|
saved: []
|
|
deleted: []
|
|
}>()
|
|
|
|
const toast = useToast()
|
|
const saving = ref(false)
|
|
const savingError = ref('')
|
|
|
|
const statusOptions = ref<{ label: string; value: TaskStatus }[]>([])
|
|
const priorityOptions = ref<{ label: string; value: TaskPriority }[]>([])
|
|
|
|
const FALLBACK_STATUS: { label: string; value: TaskStatus }[] = [
|
|
{ label: 'To Do', value: 'Todo' },
|
|
{ label: 'In Progress', value: 'InProgress' },
|
|
{ label: 'Done', value: 'Done' },
|
|
{ label: 'Cancelled', value: 'Cancelled' },
|
|
]
|
|
|
|
const FALLBACK_PRIORITY: { label: string; value: TaskPriority }[] = [
|
|
{ label: 'Low', value: 'Low' },
|
|
{ label: 'Medium', value: 'Medium' },
|
|
{ label: 'High', value: 'High' },
|
|
]
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const [status, priority] = await Promise.all([
|
|
getMasterDataOptions('task_status'),
|
|
getMasterDataOptions('task_priority'),
|
|
])
|
|
statusOptions.value = (status.length ? status : FALLBACK_STATUS) as { label: string; value: TaskStatus }[]
|
|
priorityOptions.value = (priority.length ? priority : FALLBACK_PRIORITY) as { label: string; value: TaskPriority }[]
|
|
} catch {
|
|
statusOptions.value = FALLBACK_STATUS
|
|
priorityOptions.value = FALLBACK_PRIORITY
|
|
}
|
|
})
|
|
|
|
const assigneeOptions = computed(() => props.members.map((m) => ({ label: m.displayName, value: m.userId })))
|
|
|
|
const form = reactive({
|
|
title: '',
|
|
description: '',
|
|
status: 'Todo' as TaskStatus,
|
|
priority: 'Medium' as TaskPriority,
|
|
assigneeId: null as string | null,
|
|
dueDate: null as Date | null,
|
|
})
|
|
|
|
watch(
|
|
() => props.visible,
|
|
(visible) => {
|
|
if (visible) {
|
|
savingError.value = ''
|
|
if (props.task) {
|
|
form.title = props.task.title
|
|
form.description = props.task.description ?? ''
|
|
form.status = props.task.status
|
|
form.priority = props.task.priority
|
|
form.assigneeId = props.task.assigneeId
|
|
form.dueDate = props.task.dueDate ? new Date(props.task.dueDate) : null
|
|
} else {
|
|
form.title = ''
|
|
form.description = ''
|
|
form.status = 'Todo'
|
|
form.priority = 'Medium'
|
|
form.assigneeId = null
|
|
form.dueDate = null
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
async function onSave() {
|
|
if (!form.title.trim()) {
|
|
savingError.value = 'Title is required'
|
|
return
|
|
}
|
|
saving.value = true
|
|
savingError.value = ''
|
|
try {
|
|
const dueDate = form.dueDate ? form.dueDate.toISOString() : null
|
|
if (props.task) {
|
|
await updateTask(props.task.id, {
|
|
title: form.title.trim(),
|
|
description: form.description || null,
|
|
status: form.status,
|
|
priority: form.priority,
|
|
assigneeId: form.assigneeId,
|
|
dueDate,
|
|
})
|
|
} else {
|
|
await createTask(props.projectId, {
|
|
title: form.title.trim(),
|
|
description: form.description || null,
|
|
status: form.status,
|
|
priority: form.priority,
|
|
assigneeId: form.assigneeId,
|
|
dueDate,
|
|
})
|
|
}
|
|
toast.add({ severity: 'success', summary: 'Saved', life: 2000 })
|
|
emit('saved')
|
|
} catch (e) {
|
|
savingError.value = errorMessage(e)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function onDelete() {
|
|
if (!props.task) return
|
|
saving.value = true
|
|
try {
|
|
await deleteTask(props.task.id)
|
|
toast.add({ severity: 'success', summary: 'Task deleted', life: 2000 })
|
|
emit('deleted')
|
|
} catch (e) {
|
|
savingError.value = errorMessage(e)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|