Files
notely/frontend/src/components/TaskDetailModal.vue
2026-03-29 15:28:44 +01:00

151 lines
6.7 KiB
Vue

<template>
<teleport to="body">
<div class="modal fade show d-block" tabindex="-1" role="dialog" aria-modal="true" @click.self="emit('close')">
<div class="modal-dialog modal-xl modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ localTask.id ? "Task Detail" : "Create Task" }}</h5>
<button type="button" class="btn-close" aria-label="Close" @click="emit('close')"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-12 col-lg-7">
<label class="form-label">Title</label>
<input v-model="localTask.title" class="form-control" type="text" maxlength="255" />
<label class="form-label mt-3">Description</label>
<textarea v-model="localTask.description" class="form-control" rows="5" maxlength="2000"></textarea>
<label class="form-label mt-3">Category</label>
<select v-model="localTask.category_id" class="form-select">
<option value="">Uncategorized</option>
<option v-for="category in categoryOptions" :key="category.id" :value="category.id">{{ category.label }}</option>
</select>
<label class="form-label mt-3">Parent Task</label>
<select v-model="localTask.parent_task_id" class="form-select">
<option value="">No parent (top level)</option>
<option v-for="option in parentTaskOptions" :key="option.id" :value="option.id">{{ option.title }}</option>
</select>
</div>
<div class="col-12 col-lg-5">
<label class="form-label">Status</label>
<select v-model="localTask.status_id" class="form-select">
<option v-for="status in statuses" :key="status.id" :value="status.id">{{ status.name }}</option>
</select>
<div class="status-progress mt-3">
<div v-for="status in statuses" :key="status.id" class="progress-step" :class="stepClass(status)">
<span class="dot" :style="{ borderColor: status.color || '#7c8596', backgroundColor: isReached(status) ? status.color || '#7c8596' : 'transparent' }"></span>
<span>{{ status.name }}</span>
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-outline-secondary" :disabled="!localTask.id" @click="emit('transition', { taskId: localTask.id, direction: 'backward' })">Revert</button>
<button class="btn btn-outline-primary" :disabled="!localTask.id" @click="emit('transition', { taskId: localTask.id, direction: 'forward' })">Advance</button>
</div>
<div class="mt-4">
<h6>Subtasks</h6>
<div v-if="!subtasks.length" class="text-muted small">No subtasks yet.</div>
<button v-for="subtask in subtasks" :key="subtask.id" class="subtask-row" @click="emit('open-task', subtask)">
<span>{{ subtask.title }}</span>
<small>L{{ subtask.depth + 1 }}</small>
</button>
<button v-if="canAddSubtask" class="btn btn-sm btn-outline-primary mt-2" @click="emit('create-subtask', localTask)">Add Subtask</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" @click="emit('close')">Close</button>
<button v-if="localTask.id" type="button" class="btn btn-danger" @click="emit('delete-task', localTask)">Delete</button>
<button type="button" class="btn btn-primary" @click="saveTask">Save</button>
</div>
</div>
</div>
</div>
<div class="modal-backdrop fade show"></div>
</teleport>
</template>
<script setup>
import { computed, ref, watch } from "vue";
const props = defineProps({
task: {
type: Object,
default: () => ({}),
},
statuses: {
type: Array,
default: () => [],
},
categoryOptions: {
type: Array,
default: () => [],
},
parentTaskOptions: {
type: Array,
default: () => [],
},
subtasks: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["close", "save-task", "delete-task", "transition", "create-subtask", "open-task"]);
const localTask = ref({});
watch(
() => props.task,
(value) => {
localTask.value = {
title: "",
description: "",
category_id: "",
status_id: props.statuses[0]?.id || "",
parent_task_id: "",
note_links: [],
...value,
category_id: value?.category_id || "",
parent_task_id: value?.parent_task_id || "",
note_links: value?.note_links || [],
};
},
{ immediate: true },
);
const canAddSubtask = computed(() => !!localTask.value.id && (localTask.value.depth ?? 0) < 2);
const isReached = (status) => {
const current = props.statuses.find((item) => item.id === localTask.value.status_id)?.order ?? 0;
return status.order <= current;
};
const stepClass = (status) => {
const current = props.statuses.find((item) => item.id === localTask.value.status_id)?.order ?? 0;
return {
current: status.order === current,
done: status.order < current,
};
};
const saveTask = () => {
emit("save-task", {
...localTask.value,
category_id: localTask.value.category_id || null,
parent_task_id: localTask.value.parent_task_id || null,
});
};
</script>
<style scoped src="../assets/styles/scoped/components/TaskDetailModal.css"></style>