- TaskForm: add start_time/end_time fields; on save emits optional block payload so PlannerView creates a PlannedBlock automatically - PlannerView: handleSave accepts block param, calls createBlock after task creation when time is provided - CalendarBlock: planned blocks with task_id get draggable="true" + @dragstart emitting blockDragStart event - CalendarGrid: forward blockDragStart to useCalendarDnD - useCalendarDnD: onBlockDragStart stores block_id + duration in dataTransfer; onDrop handles both move-existing-block and create-new-block paths Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
222 lines
7.3 KiB
Vue
222 lines
7.3 KiB
Vue
<script setup lang="ts">
|
||
import { ref, watch, onMounted } from 'vue'
|
||
import Dialog from '@/components/ui/Dialog.vue'
|
||
import Input from '@/components/ui/Input.vue'
|
||
import Textarea from '@/components/ui/Textarea.vue'
|
||
import Select from '@/components/ui/Select.vue'
|
||
import Button from '@/components/ui/Button.vue'
|
||
import { useDevopsStore } from '@/stores/devops'
|
||
import { useProjectsStore } from '@/stores/projects'
|
||
import type { Task } from '@/types'
|
||
import type { TaskCreatePayload, TaskUpdatePayload } from '@/api/endpoints/tasks'
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
open: boolean
|
||
task?: Task | null
|
||
defaultDate?: string
|
||
}>(),
|
||
{ task: null }
|
||
)
|
||
|
||
const emit = defineEmits<{
|
||
close: []
|
||
save: [payload: TaskCreatePayload | TaskUpdatePayload, block?: { start_at: string; end_at: string }]
|
||
}>()
|
||
|
||
const devopsStore = useDevopsStore()
|
||
const projectsStore = useProjectsStore()
|
||
|
||
onMounted(() => {
|
||
projectsStore.fetchProjects()
|
||
})
|
||
|
||
const form = ref({
|
||
title: '',
|
||
notes: '',
|
||
planned_date: '',
|
||
start_time: '',
|
||
end_time: '',
|
||
estimate_hours: 1,
|
||
status: 'todo' as Task['status'],
|
||
priority: 3 as Task['priority'],
|
||
project_id: undefined as string | undefined,
|
||
azure_work_item_id: undefined as string | undefined,
|
||
})
|
||
|
||
watch(
|
||
() => props.open,
|
||
(open) => {
|
||
if (open) {
|
||
if (props.task) {
|
||
form.value = {
|
||
title: props.task.title,
|
||
notes: props.task.notes ?? '',
|
||
planned_date: props.task.planned_date ?? '',
|
||
start_time: '',
|
||
end_time: '',
|
||
estimate_hours: props.task.estimate_hours ?? 1,
|
||
status: props.task.status,
|
||
priority: props.task.priority,
|
||
project_id: props.task.project_id ?? undefined,
|
||
azure_work_item_id: props.task.azure_work_item_id ?? undefined,
|
||
}
|
||
} else {
|
||
form.value = {
|
||
title: '',
|
||
notes: '',
|
||
planned_date: props.defaultDate ?? '',
|
||
start_time: '',
|
||
end_time: '',
|
||
estimate_hours: 1,
|
||
status: 'todo',
|
||
priority: 3,
|
||
project_id: undefined,
|
||
azure_work_item_id: undefined,
|
||
}
|
||
}
|
||
// Load ADO work items if integration exists
|
||
if (devopsStore.integration && !devopsStore.workItems.length) {
|
||
devopsStore.fetchWorkItems('open')
|
||
}
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
const saving = ref(false)
|
||
|
||
async function handleSave() {
|
||
if (!form.value.title.trim()) return
|
||
saving.value = true
|
||
try {
|
||
const payload: TaskCreatePayload = {
|
||
title: form.value.title,
|
||
notes: form.value.notes || undefined,
|
||
planned_date: form.value.planned_date,
|
||
estimate_hours: form.value.estimate_hours,
|
||
status: form.value.status,
|
||
priority: form.value.priority,
|
||
project_id: form.value.project_id || null,
|
||
azure_work_item_id: form.value.azure_work_item_id || null,
|
||
}
|
||
|
||
let block: { start_at: string; end_at: string } | undefined
|
||
if (form.value.planned_date && form.value.start_time && form.value.end_time) {
|
||
block = {
|
||
start_at: new Date(`${form.value.planned_date}T${form.value.start_time}:00`).toISOString(),
|
||
end_at: new Date(`${form.value.planned_date}T${form.value.end_time}:00`).toISOString(),
|
||
}
|
||
}
|
||
|
||
emit('save', payload, block)
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Dialog
|
||
:open="open"
|
||
:title="task ? 'Edit Task' : 'New Task'"
|
||
max-width="max-w-md"
|
||
@close="emit('close')"
|
||
>
|
||
<form class="space-y-4" @submit.prevent="handleSave">
|
||
<!-- Title -->
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Title *</label>
|
||
<Input v-model="form.title" placeholder="Task title..." :disabled="saving" />
|
||
</div>
|
||
|
||
<!-- Notes -->
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Notes</label>
|
||
<Textarea v-model="form.notes" placeholder="Additional notes..." :disabled="saving" />
|
||
</div>
|
||
|
||
<!-- Date + Estimate row -->
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Planned Date</label>
|
||
<Input v-model="form.planned_date" type="date" :disabled="saving" />
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Estimate (h)</label>
|
||
<Input
|
||
v-model="form.estimate_hours"
|
||
type="number"
|
||
min="0.25"
|
||
max="24"
|
||
step="0.25"
|
||
:disabled="saving"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Time slot row (optional) -->
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Start time <span class="text-muted-foreground font-normal">(optional)</span></label>
|
||
<Input v-model="form.start_time" type="time" :disabled="saving" />
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">End time</label>
|
||
<Input v-model="form.end_time" type="time" :disabled="saving" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status + Priority row -->
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Status</label>
|
||
<Select v-model="form.status" :disabled="saving">
|
||
<option value="todo">Todo</option>
|
||
<option value="doing">Doing</option>
|
||
<option value="done">Done</option>
|
||
<option value="cancelled">Cancelled</option>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Priority</label>
|
||
<Select v-model="form.priority" :disabled="saving">
|
||
<option value="1">1 - Low</option>
|
||
<option value="2">2 - Normal</option>
|
||
<option value="3">3 - Medium</option>
|
||
<option value="4">4 - High</option>
|
||
<option value="5">5 - Critical</option>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Project -->
|
||
<div v-if="projectsStore.projects.length" class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Project</label>
|
||
<Select v-model="form.project_id" :disabled="saving" placeholder="Select project...">
|
||
<option value="">None</option>
|
||
<option v-for="proj in projectsStore.projects" :key="proj.id" :value="proj.id">
|
||
{{ proj.display_name }}{{ proj.job_number ? ` (${proj.job_number})` : '' }}
|
||
</option>
|
||
</Select>
|
||
</div>
|
||
|
||
<!-- ADO Work Item -->
|
||
<div v-if="devopsStore.workItems.length" class="space-y-1.5">
|
||
<label class="text-sm font-medium text-foreground">Azure DevOps Work Item</label>
|
||
<Select v-model="form.azure_work_item_id" :disabled="saving" placeholder="Link work item...">
|
||
<option v-for="wi in devopsStore.workItems" :key="wi.id" :value="wi.id">
|
||
#{{ wi.ado_id }} – {{ wi.title }}
|
||
</option>
|
||
</Select>
|
||
</div>
|
||
</form>
|
||
|
||
<template #footer>
|
||
<Button variant="outline" :disabled="saving" @click="emit('close')">Cancel</Button>
|
||
<Button :loading="saving" @click="handleSave">
|
||
{{ task ? 'Update' : 'Create' }}
|
||
</Button>
|
||
</template>
|
||
</Dialog>
|
||
</template>
|