Initial commit: AMS Frontend - Vue 3 + PrimeVue
This commit is contained in:
893
src/views/TaskDetailView.vue
Normal file
893
src/views/TaskDetailView.vue
Normal file
@@ -0,0 +1,893 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api } from '../api'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import AttachmentList from '../components/AttachmentList.vue'
|
||||
import CommentList from '../components/CommentList.vue'
|
||||
import TaskChangelog from '../components/TaskChangelog.vue'
|
||||
import TaskCommits from '../components/TaskCommits.vue'
|
||||
import SpeechToText from '../components/SpeechToText.vue'
|
||||
|
||||
interface Reminder {
|
||||
enabled: boolean
|
||||
datetime: string
|
||||
interval: 'once' | 'daily' | 'hourly' | 'minutes'
|
||||
intervalValue?: number
|
||||
lastNotified?: string
|
||||
}
|
||||
|
||||
interface Task {
|
||||
_id: string
|
||||
title: string
|
||||
description?: string
|
||||
status: string
|
||||
priority: string
|
||||
assignee?: string
|
||||
project?: string
|
||||
labels: string[]
|
||||
reminder?: Reminder
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
_id: string
|
||||
name: string
|
||||
emoji: string
|
||||
}
|
||||
|
||||
interface Project {
|
||||
_id: string
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
interface Label {
|
||||
_id: string
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const taskId = computed(() => route.params.id as string)
|
||||
const task = ref<Task | null>(null)
|
||||
const editForm = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
assignee: '',
|
||||
project: '',
|
||||
labels: [] as string[]
|
||||
})
|
||||
const agents = ref<Agent[]>([])
|
||||
const projects = ref<Project[]>([])
|
||||
const labels = ref<Label[]>([])
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
// Reminder state
|
||||
const showReminderForm = ref(false)
|
||||
const reminderDate = ref('')
|
||||
const reminderTime = ref('')
|
||||
const reminderInterval = ref<'once' | 'daily' | 'hourly' | 'minutes'>('once')
|
||||
const reminderMinutes = ref(30)
|
||||
const savingReminder = ref(false)
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'backlog', label: 'Backlog', color: '#64748b' },
|
||||
{ value: 'todo', label: 'To Do', color: '#3b82f6' },
|
||||
{ value: 'in_progress', label: 'In Progress', color: '#f59e0b' },
|
||||
{ value: 'review', label: 'Review', color: '#8b5cf6' },
|
||||
{ value: 'done', label: 'Done', color: '#22c55e' }
|
||||
]
|
||||
|
||||
const priorityOptions = [
|
||||
{ value: 'low', label: 'Niedrig', color: '#64748b' },
|
||||
{ value: 'medium', label: 'Mittel', color: '#3b82f6' },
|
||||
{ value: 'high', label: 'Hoch', color: '#f59e0b' },
|
||||
{ value: 'urgent', label: 'Dringend', color: '#ef4444' }
|
||||
]
|
||||
|
||||
const intervalOptions = [
|
||||
{ value: 'once', label: 'Einmalig' },
|
||||
{ value: 'daily', label: 'Täglich' },
|
||||
{ value: 'hourly', label: 'Stündlich' },
|
||||
{ value: 'minutes', label: 'Alle X Minuten' }
|
||||
]
|
||||
|
||||
const hasReminder = computed(() => task.value?.reminder?.enabled)
|
||||
|
||||
const reminderDisplay = computed(() => {
|
||||
if (!task.value?.reminder) return null
|
||||
const r = task.value.reminder
|
||||
const dt = new Date(r.datetime)
|
||||
const dateStr = dt.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
const timeStr = dt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
|
||||
|
||||
let intervalStr = ''
|
||||
switch (r.interval) {
|
||||
case 'once': intervalStr = 'Einmalig'; break
|
||||
case 'daily': intervalStr = 'Täglich'; break
|
||||
case 'hourly': intervalStr = 'Stündlich'; break
|
||||
case 'minutes': intervalStr = `Alle ${r.intervalValue} Min`; break
|
||||
}
|
||||
|
||||
return { date: dateStr, time: timeStr, interval: intervalStr, enabled: r.enabled }
|
||||
})
|
||||
|
||||
// Status-Reihenfolge für "Nächster Status" Button
|
||||
const statusOrder = ['backlog', 'todo', 'in_progress', 'review', 'done']
|
||||
const initialStatus = ref('')
|
||||
|
||||
function getNextStatus(current: string): string | null {
|
||||
const idx = statusOrder.indexOf(current)
|
||||
if (idx < 0 || idx >= statusOrder.length - 1) return null
|
||||
return statusOrder[idx + 1]
|
||||
}
|
||||
|
||||
const nextStatusLabel = computed(() => {
|
||||
const next = getNextStatus(editForm.value.status)
|
||||
if (!next) return null
|
||||
return statusOptions.find(o => o.value === next)?.label || next
|
||||
})
|
||||
|
||||
async function advanceStatus() {
|
||||
const next = getNextStatus(editForm.value.status)
|
||||
if (!next) return
|
||||
|
||||
try {
|
||||
await api.put(`/tasks/${taskId.value}`, { status: next })
|
||||
editForm.value.status = next
|
||||
if (task.value) task.value.status = next
|
||||
toast.add({ severity: 'success', summary: 'Status geändert', detail: `→ ${statusOptions.find(o => o.value === next)?.label}`, life: 2000 })
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unbekannter Fehler'
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: message, life: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
async function goToNextTask() {
|
||||
if (!task.value?.project || !initialStatus.value) return
|
||||
|
||||
try {
|
||||
const res = await api.get<{ tasks: Task[] }>('/tasks')
|
||||
const sameTasks = res.tasks
|
||||
.filter(t => t.project === task.value!.project && t.status === initialStatus.value && t._id !== taskId.value)
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
|
||||
if (sameTasks.length > 0) {
|
||||
router.push(`/tasks/${sameTasks[0]._id}`)
|
||||
} else {
|
||||
toast.add({ severity: 'info', summary: 'Kein weiterer Task', detail: 'Kein weiterer Task mit gleichem Status im Projekt', life: 3000 })
|
||||
}
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Tasks konnten nicht geladen werden', life: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTask() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [taskRes, agentsRes, projectsRes, labelsRes] = await Promise.all([
|
||||
api.get<{ task: Task }>(`/tasks/${taskId.value}`),
|
||||
api.get<{ agents: Agent[] }>('/agents'),
|
||||
api.get<{ projects: Project[] }>('/tasks/projects/list'),
|
||||
api.get<{ labels: Label[] }>('/labels')
|
||||
])
|
||||
task.value = taskRes.task
|
||||
agents.value = agentsRes.agents
|
||||
projects.value = projectsRes.projects
|
||||
labels.value = labelsRes.labels
|
||||
|
||||
// Fill edit form
|
||||
initialStatus.value = taskRes.task.status
|
||||
editForm.value = {
|
||||
title: taskRes.task.title,
|
||||
description: taskRes.task.description || '',
|
||||
status: taskRes.task.status,
|
||||
priority: taskRes.task.priority,
|
||||
assignee: taskRes.task.assignee || '',
|
||||
project: taskRes.task.project || '',
|
||||
labels: taskRes.task.labels || []
|
||||
}
|
||||
|
||||
// Fill reminder form if exists
|
||||
if (taskRes.task.reminder) {
|
||||
const dt = new Date(taskRes.task.reminder.datetime)
|
||||
reminderDate.value = dt.toISOString().split('T')[0]
|
||||
reminderTime.value = dt.toTimeString().slice(0, 5)
|
||||
reminderInterval.value = taskRes.task.reminder.interval
|
||||
reminderMinutes.value = taskRes.task.reminder.intervalValue || 30
|
||||
}
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Task nicht gefunden', life: 5000 })
|
||||
router.push('/tasks')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTask() {
|
||||
if (!editForm.value.title.trim()) {
|
||||
toast.add({ severity: 'warn', summary: 'Hinweis', detail: 'Titel ist erforderlich', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await api.put(`/tasks/${taskId.value}`, editForm.value)
|
||||
toast.add({ severity: 'success', summary: 'Gespeichert', detail: 'Task aktualisiert', life: 3000 })
|
||||
if (task.value) {
|
||||
task.value = { ...task.value, ...editForm.value }
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.message, life: 5000 })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveReminder() {
|
||||
if (!reminderDate.value || !reminderTime.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Hinweis', detail: 'Datum und Uhrzeit sind erforderlich', life: 3000 })
|
||||
return
|
||||
}
|
||||
|
||||
savingReminder.value = true
|
||||
try {
|
||||
const datetime = new Date(`${reminderDate.value}T${reminderTime.value}`)
|
||||
|
||||
await api.patch(`/tasks/${taskId.value}/reminder`, {
|
||||
datetime: datetime.toISOString(),
|
||||
interval: reminderInterval.value,
|
||||
intervalValue: reminderInterval.value === 'minutes' ? reminderMinutes.value : undefined
|
||||
})
|
||||
|
||||
// Update local task
|
||||
if (task.value) {
|
||||
task.value.reminder = {
|
||||
enabled: true,
|
||||
datetime: datetime.toISOString(),
|
||||
interval: reminderInterval.value,
|
||||
intervalValue: reminderMinutes.value
|
||||
}
|
||||
}
|
||||
|
||||
showReminderForm.value = false
|
||||
toast.add({ severity: 'success', summary: 'Reminder gesetzt', detail: 'Du wirst erinnert', life: 3000 })
|
||||
} catch (err: any) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.message, life: 5000 })
|
||||
} finally {
|
||||
savingReminder.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteReminder() {
|
||||
if (!confirm('Reminder löschen?')) return
|
||||
|
||||
try {
|
||||
await api.delete(`/tasks/${taskId.value}/reminder`)
|
||||
if (task.value) {
|
||||
task.value.reminder = undefined
|
||||
}
|
||||
toast.add({ severity: 'success', summary: 'Entfernt', detail: 'Reminder gelöscht', life: 3000 })
|
||||
} catch (err: any) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.message, life: 5000 })
|
||||
}
|
||||
}
|
||||
|
||||
function openReminderForm() {
|
||||
// Set default values
|
||||
if (!reminderDate.value) {
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
reminderDate.value = tomorrow.toISOString().split('T')[0]
|
||||
reminderTime.value = '09:00'
|
||||
}
|
||||
showReminderForm.value = true
|
||||
}
|
||||
|
||||
function getStatusColor(status: string) {
|
||||
return statusOptions.find(s => s.value === status)?.color || '#64748b'
|
||||
}
|
||||
|
||||
function getPriorityColor(priority: string) {
|
||||
return priorityOptions.find(p => p.value === priority)?.color || '#64748b'
|
||||
}
|
||||
|
||||
onMounted(loadTask)
|
||||
|
||||
// Wenn sich die Route ändert (z.B. "Nächster Task"), Task neu laden
|
||||
watch(() => route.params.id, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
loadTask()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="back-link">
|
||||
<router-link to="/tasks">← Zurück zu Tasks</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
<i class="pi pi-spin pi-spinner" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
|
||||
<div v-else-if="task" class="task-detail-layout">
|
||||
<!-- LEFT: Edit Form -->
|
||||
<div class="edit-panel">
|
||||
<h2><i class="pi pi-pencil"></i> Task bearbeiten</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Titel</label>
|
||||
<div class="input-with-stt">
|
||||
<input v-model="editForm.title" type="text" placeholder="Task-Titel" />
|
||||
<SpeechToText @transcribed="(t: string) => editForm.title += (editForm.title ? ' ' : '') + t" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Beschreibung</label>
|
||||
<div class="input-with-stt">
|
||||
<textarea v-model="editForm.description" rows="4" placeholder="Beschreibung (optional)"></textarea>
|
||||
<SpeechToText @transcribed="(t: string) => editForm.description += (editForm.description ? ' ' : '') + t" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select v-model="editForm.status">
|
||||
<option v-for="opt in statusOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="status-indicator" :style="{ background: getStatusColor(editForm.status) }"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Priorität</label>
|
||||
<select v-model="editForm.priority">
|
||||
<option v-for="opt in priorityOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="status-indicator" :style="{ background: getPriorityColor(editForm.priority) }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Agent</label>
|
||||
<select v-model="editForm.assignee">
|
||||
<option value="">Nicht zugewiesen</option>
|
||||
<option v-for="agent in agents" :key="agent._id" :value="agent._id">
|
||||
{{ agent.emoji }} {{ agent.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Projekt</label>
|
||||
<select v-model="editForm.project">
|
||||
<option value="">Kein Projekt</option>
|
||||
<option v-for="project in projects" :key="project._id" :value="project._id">
|
||||
{{ project.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Labels</label>
|
||||
<select v-model="editForm.labels" multiple class="multi-select">
|
||||
<option v-for="label in labels" :key="label._id" :value="label._id">
|
||||
{{ label.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="labels-preview">
|
||||
<span v-for="labelId in editForm.labels" :key="labelId"
|
||||
class="label-badge"
|
||||
:style="{ background: labels.find(l => l._id === labelId)?.color }">
|
||||
{{ labels.find(l => l._id === labelId)?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="saveTask" class="save-btn" :disabled="saving">
|
||||
<i class="pi" :class="saving ? 'pi-spin pi-spinner' : 'pi-check'"></i>
|
||||
{{ saving ? 'Speichert...' : 'Änderungen speichern' }}
|
||||
</button>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="quick-actions">
|
||||
<button
|
||||
v-if="nextStatusLabel"
|
||||
@click="advanceStatus"
|
||||
class="quick-btn next-status"
|
||||
>
|
||||
<i class="pi pi-arrow-right"></i>
|
||||
→ {{ nextStatusLabel }}
|
||||
</button>
|
||||
<button
|
||||
v-if="task?.project"
|
||||
@click="goToNextTask"
|
||||
class="quick-btn next-task"
|
||||
>
|
||||
<i class="pi pi-forward"></i>
|
||||
Nächster Task
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Reminder Section -->
|
||||
<div class="reminder-section">
|
||||
<h3><i class="pi pi-bell"></i> Reminder</h3>
|
||||
|
||||
<!-- Current Reminder Display -->
|
||||
<div v-if="hasReminder && reminderDisplay" class="reminder-display">
|
||||
<div class="reminder-info">
|
||||
<span class="reminder-datetime">
|
||||
<i class="pi pi-calendar"></i> {{ reminderDisplay.date }}
|
||||
<i class="pi pi-clock"></i> {{ reminderDisplay.time }}
|
||||
</span>
|
||||
<span class="reminder-interval">{{ reminderDisplay.interval }}</span>
|
||||
</div>
|
||||
<div class="reminder-actions">
|
||||
<button @click="openReminderForm" class="icon-btn" title="Bearbeiten">
|
||||
<i class="pi pi-pencil"></i>
|
||||
</button>
|
||||
<button @click="deleteReminder" class="icon-btn delete" title="Löschen">
|
||||
<i class="pi pi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Reminder -->
|
||||
<div v-else-if="!showReminderForm" class="no-reminder">
|
||||
<button @click="openReminderForm" class="add-reminder-btn">
|
||||
<i class="pi pi-plus"></i> Reminder hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Reminder Form -->
|
||||
<div v-if="showReminderForm" class="reminder-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Datum</label>
|
||||
<input v-model="reminderDate" type="date" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Uhrzeit</label>
|
||||
<input v-model="reminderTime" type="time" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Wiederholung</label>
|
||||
<select v-model="reminderInterval">
|
||||
<option v-for="opt in intervalOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="reminderInterval === 'minutes'" class="form-group">
|
||||
<label>Alle X Minuten</label>
|
||||
<input v-model.number="reminderMinutes" type="number" min="1" max="1440" />
|
||||
</div>
|
||||
|
||||
<div class="reminder-form-actions">
|
||||
<button @click="showReminderForm = false" class="cancel-btn">Abbrechen</button>
|
||||
<button @click="saveReminder" class="save-reminder-btn" :disabled="savingReminder">
|
||||
<i class="pi" :class="savingReminder ? 'pi-spin pi-spinner' : 'pi-bell'"></i>
|
||||
{{ savingReminder ? 'Speichert...' : 'Reminder setzen' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GitLab Commits Section -->
|
||||
<TaskCommits
|
||||
v-if="editForm.project"
|
||||
:taskId="taskId"
|
||||
:projectId="editForm.project"
|
||||
/>
|
||||
|
||||
<!-- Attachments Section (Universal Component) -->
|
||||
<div class="attachments-section">
|
||||
<AttachmentList parentType="task" :parentId="taskId" :enablePaste="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: Comments + Changelog -->
|
||||
<div class="comments-panel">
|
||||
<CommentList parentType="task" :parentId="taskId" />
|
||||
<TaskChangelog
|
||||
:taskId="taskId"
|
||||
:agents="agents"
|
||||
:labels="labels"
|
||||
:projects="projects"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-link {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: #818cf8;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
/* Two-Column Layout */
|
||||
.task-detail-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.task-detail-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Left Panel: Edit Form */
|
||||
.edit-panel {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.edit-panel h2 {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.input-with-stt {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.input-with-stt input,
|
||||
.input-with-stt textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.form-group select option {
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 38px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.save-btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-btn.next-status {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.quick-btn.next-status:hover {
|
||||
background: rgba(34, 197, 94, 0.25);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.quick-btn.next-task {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.quick-btn.next-task:hover {
|
||||
background: rgba(59, 130, 246, 0.25);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.save-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Reminder Section */
|
||||
.reminder-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.reminder-section h3 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.reminder-display {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.reminder-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.reminder-datetime {
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.reminder-datetime i {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.reminder-interval {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.reminder-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.no-reminder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-reminder-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border: 1px dashed rgba(99, 102, 241, 0.4);
|
||||
border-radius: 8px;
|
||||
color: #818cf8;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.add-reminder-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.reminder-form {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.reminder-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.save-reminder-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-reminder-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.attachments-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Right Panel: Comments */
|
||||
.comments-panel {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Icon buttons */
|
||||
.icon-btn {
|
||||
padding: 0.375rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.icon-btn.delete:hover {
|
||||
color: #f87171;
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.multi-select {
|
||||
min-height: 100px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.multi-select option {
|
||||
padding: 0.5rem;
|
||||
margin: 0.25rem 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.labels-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.label-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user