cc-dashboard/web/src/components/tasks/TaskForm.vue
Vadym Samoilenko 433512fc78 feat: task time slots, calendar block drag-to-move
- 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>
2026-05-06 21:19:49 +01:00

222 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>