151 lines
6.7 KiB
Vue
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>
|
|
|
|
|
|
|