Add project Schedule page with interactive Gantt chart
Adds a Schedule tab (Kitsu-style production schedule) under each project: tasks grouped by type with drag-to-reschedule bars, Day/Week/ Month zoom, manual date-range control, weekend shading, a frozen task column, and a two-tier month/date axis header. Requires a new start_date field on Task (start_date was previously missing; only deadline existed) and shadcn DatePicker inputs replace native date inputs on the Schedule toolbar and TaskDetailPanel's Start Date/ Deadline fields. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -123,12 +123,26 @@
|
||||
<Badge variant="outline">{{ formatTaskType(task.task_type) }}</Badge>
|
||||
</p>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Start Date</Label>
|
||||
<div class="mt-1">
|
||||
<DatePicker
|
||||
v-model="localStartDate"
|
||||
placeholder="Set start date"
|
||||
@update:model-value="(val) => handleDateChange('start_date', val || '')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label class="text-muted-foreground">Deadline</Label>
|
||||
<p class="text-sm mt-1 flex items-center gap-2" :class="getDeadlineClass(task.deadline, task.status)">
|
||||
<Calendar class="h-3 w-3" />
|
||||
{{ task.deadline ? formatDate(task.deadline) : 'No deadline' }}
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<DatePicker
|
||||
v-model="localDeadline"
|
||||
placeholder="Set deadline"
|
||||
@update:model-value="(val) => handleDateChange('deadline', val || '')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -284,10 +298,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { Play, Upload, UserPlus, Calendar, User, Info, MessageSquare, Paperclip } from 'lucide-vue-next'
|
||||
import { Play, Upload, UserPlus, User, Info, MessageSquare, Paperclip } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { DatePicker } from '@/components/ui/date-picker'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import DetailPanelLoading from '@/components/shared/DetailPanelLoading.vue'
|
||||
import DetailPanelError from '@/components/shared/DetailPanelError.vue'
|
||||
@@ -348,6 +363,8 @@ const task = ref<Task | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const localStatus = ref('')
|
||||
const localStartDate = ref('')
|
||||
const localDeadline = ref('')
|
||||
const notes = ref<ProductionNote[]>([])
|
||||
const attachments = ref<TaskAttachment[]>([])
|
||||
const submissions = ref<Submission[]>([])
|
||||
@@ -381,6 +398,8 @@ async function loadTask() {
|
||||
try {
|
||||
task.value = await taskService.getTask(props.taskId)
|
||||
localStatus.value = task.value.status
|
||||
localStartDate.value = task.value.start_date || ''
|
||||
localDeadline.value = task.value.deadline || ''
|
||||
} catch (err: any) {
|
||||
console.error('Error loading task:', err)
|
||||
error.value = err.response?.data?.detail || 'Failed to load task'
|
||||
@@ -440,6 +459,30 @@ async function handleStatusChange(newStatus: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDateChange(field: 'start_date' | 'deadline', value: string) {
|
||||
if (!task.value) return
|
||||
|
||||
const previous = task.value[field]
|
||||
try {
|
||||
const updated = await taskService.updateTask(props.taskId, { [field]: value || null } as any)
|
||||
task.value[field] = updated[field]
|
||||
emit('taskUpdated')
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: `Task ${field === 'start_date' ? 'start date' : 'deadline'} updated successfully`
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error(`Error updating ${field}:`, error)
|
||||
if (field === 'start_date') localStartDate.value = previous || ''
|
||||
else localDeadline.value = previous || ''
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.response?.data?.detail || `Failed to update ${field === 'start_date' ? 'start date' : 'deadline'}`,
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQuickAction(action: 'start' | 'submit') {
|
||||
if (!task.value) return
|
||||
|
||||
@@ -519,19 +562,6 @@ function formatDate(dateString: string): string {
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
|
||||
function getDeadlineClass(deadline: string | undefined, status: string): string {
|
||||
if (!deadline || status === 'approved') return 'text-muted-foreground'
|
||||
|
||||
const now = new Date()
|
||||
const deadlineDate = new Date(deadline)
|
||||
const daysUntil = Math.ceil((deadlineDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (daysUntil < 0) return 'text-destructive'
|
||||
if (daysUntil <= 3) return 'text-orange-600'
|
||||
if (daysUntil <= 7) return 'text-yellow-600'
|
||||
return 'text-foreground'
|
||||
}
|
||||
|
||||
watch(() => props.taskId, () => {
|
||||
loadTask()
|
||||
loadNotes()
|
||||
|
||||
Reference in New Issue
Block a user