PHP Classes

File: resources/js/components/TodoTable.vue

Recommend this page to a friend!
  Packages of Niko Peikrishvili   Ticker   resources/js/components/TodoTable.vue   Download  
File: resources/js/components/TodoTable.vue
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Ticker
Application to track and manage to do tasks
Author: By
Last change:
Date: 2 months ago
Size: 16,185 bytes
 

Contents

Class file image Download
<script setup> import { ref, computed, onMounted, onUnmounted } from 'vue'; import { useTodos } from '@/composables/useTodos'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; const props = defineProps({ onOpenModal: { type: Function, required: true, }, }); const { todos, loading, statuses, updateTodo, deleteTodo, startWorking, stopWorking, } = useTodos(); const editingId = ref(null); const editContent = ref(''); const now = ref(new Date()); let timerInterval = null; const statusConfig = { backlog: { label: '???????', variant: 'secondary', icon: 'help-circle' }, todo: { label: '????????????', variant: 'outline', icon: 'circle' }, in_progress: { label: '?????????', variant: 'default', icon: 'timer' }, done: { label: '???????????', variant: 'success', icon: 'check-circle' }, }; const sections = [ { key: 'in_progress', label: '?????????', borderClass: 'border-yellow-300 dark:border-yellow-700', headerBg: 'bg-yellow-100 dark:bg-yellow-900/40', headerText: 'text-yellow-700 dark:text-yellow-400', rowBg: 'bg-yellow-50/50 dark:bg-yellow-900/20', dotColor: 'bg-yellow-500', pulse: true, }, { key: 'todo', label: '????????????', borderClass: 'border-blue-300 dark:border-blue-700', headerBg: 'bg-blue-100 dark:bg-blue-900/40', headerText: 'text-blue-700 dark:text-blue-400', rowBg: '', dotColor: 'bg-blue-500', pulse: false, }, { key: 'backlog', label: '???????', borderClass: 'border-gray-300 dark:border-gray-600', headerBg: 'bg-gray-100 dark:bg-gray-800/40', headerText: 'text-gray-700 dark:text-gray-400', rowBg: '', dotColor: 'bg-gray-400', pulse: false, }, { key: 'done', label: '???????????', borderClass: 'border-green-300 dark:border-green-700', headerBg: 'bg-green-100 dark:bg-green-900/40', headerText: 'text-green-700 dark:text-green-400', rowBg: 'bg-green-50/50 dark:bg-green-900/20', dotColor: 'bg-green-500', pulse: false, }, ]; const collapsedSections = ref({ in_progress: false, todo: false, backlog: false, done: false, }); const toggleSection = (key) => { collapsedSections.value[key] = !collapsedSections.value[key]; }; const todosForStatus = (status) => { return todos.value.filter(todo => todo.status === status); }; // Total count for footer const totalCount = computed(() => { return todos.value.length; }); // Check if any task is working to enable timer const hasWorkingTask = computed(() => { return todos.value.some(todo => todo.is_working); }); // Calculate elapsed time for a working task const getElapsedMinutes = (workingStartedAt) => { if (!workingStartedAt) return 0; const start = new Date(workingStartedAt); const diff = now.value - start; return Math.floor(diff / 60000); // Convert ms to minutes }; // Get total time including current session const getTotalWithCurrent = (todo) => { const base = todo.total_time_spent || 0; if (todo.is_working && todo.working_started_at) { return base + getElapsedMinutes(todo.working_started_at); } return base; }; const startEditing = (todo) => { editingId.value = todo.id; editContent.value = todo.content; }; const saveEdit = async (id) => { if (editContent.value.trim()) { await updateTodo(id, { content: editContent.value }); } editingId.value = null; }; const cancelEdit = () => { editingId.value = null; editContent.value = ''; }; const handleDelete = (id) => { if (confirm('????????? ????? ??????')) { deleteTodo(id); } }; const formatTime = (minutes) => { if (minutes === null || minutes === undefined) return null; if (minutes === 0) return '00:00'; const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; }; const getStatusVariant = (status) => { const variants = { backlog: 'secondary', todo: 'outline', in_progress: 'default', done: 'success', }; return variants[status] || 'secondary'; }; const getPriorityColor = (priority) => { switch (priority) { case 1: return 'bg-gray-300'; case 2: return 'bg-gray-400'; case 3: return 'bg-gray-400'; case 4: return 'bg-orange-500'; case 5: return 'bg-red-500'; default: return 'bg-gray-300'; } }; const priorityLabels = { 1: '?????? ??????', 2: '??????', 3: '???????', 4: '??????', 5: '?????? ??????', }; // Start/stop timer based on working tasks onMounted(() => { timerInterval = setInterval(() => { if (hasWorkingTask.value) { now.value = new Date(); } }, 1000); }); onUnmounted(() => { if (timerInterval) { clearInterval(timerInterval); } }); </script> <template> <div class="w-full space-y-3"> <!-- Loading State --> <div v-if="loading" class="rounded-md border p-8 text-center text-muted-foreground"> ?????????... </div> <!-- Empty State --> <div v-else-if="totalCount === 0" class="rounded-md border p-8 text-center text-muted-foreground"> ?????????? ?? ???????? </div> <template v-else> <template v-for="section in sections" :key="section.key"> <div v-if="todosForStatus(section.key).length > 0" class="rounded-md border overflow-hidden" :class="section.borderClass"> <!-- Collapsible Header --> <button class="w-full px-4 py-2 flex items-center gap-2 cursor-pointer select-none" :class="section.headerBg" @click="toggleSection(section.key)" > <svg class="w-3.5 h-3.5 transition-transform duration-200" :class="[section.headerText, collapsedSections[section.key] ? '-rotate-90' : '']" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> </svg> <span class="w-2 h-2 rounded-full" :class="[section.dotColor, section.pulse ? 'animate-pulse' : '']" ></span> <span class="text-sm font-semibold" :class="section.headerText"> {{ section.label }} ({{ todosForStatus(section.key).length }}) </span> </button> <!-- Table Body --> <Table v-show="!collapsedSections[section.key]"> <TableBody> <TableRow v-for="todo in todosForStatus(section.key)" :key="section.key + '-' + todo.id" :class="section.rowBg"> <TableCell class="w-[100px] font-mono text-xs text-muted-foreground">{{ todo.display_id }}</TableCell> <TableCell class="w-[60px]"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="sm" class="h-6 px-1" :title="priorityLabels[todo.priority || 3]"> <div class="flex gap-px"><span v-for="i in 5" :key="i" class="w-1.5 h-1.5 rounded-full" :class="i <= (todo.priority || 3) ? getPriorityColor(todo.priority || 3) : 'bg-gray-200'"></span></div> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="start"> <DropdownMenuLabel>??????????? ??????</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem v-for="p in [5, 4, 3, 2, 1]" :key="p" @click="updateTodo(todo.id, { priority: p })"> <div class="flex items-center gap-2"><div class="flex gap-px"><span v-for="i in 5" :key="i" class="w-1.5 h-1.5 rounded-full" :class="i <= p ? getPriorityColor(p) : 'bg-gray-200'"></span></div><span class="text-xs">{{ priorityLabels[p] }}</span></div> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </TableCell> <TableCell> <div v-if="editingId === todo.id" class="flex items-center gap-2"> <Input v-model="editContent" @keyup.enter="saveEdit(todo.id)" @keyup.esc="cancelEdit" class="h-8" autofocus /> <Button size="sm" variant="ghost" @click="saveEdit(todo.id)"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg></Button> <Button size="sm" variant="ghost" @click="cancelEdit"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg></Button> </div> <div v-else class="flex items-center gap-2"> <span :class="{ 'line-through text-muted-foreground': todo.status === 'done' }" class="cursor-pointer" @dblclick="startEditing(todo)">{{ todo.content }}</span> <span v-if="todo.is_working" class="flex items-center gap-1 text-xs text-yellow-600"><span class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></span>???????</span> </div> </TableCell> <TableCell class="w-[140px]"> <DropdownMenu> <DropdownMenuTrigger asChild><Button variant="ghost" size="sm" class="h-7 px-2"><Badge :variant="getStatusVariant(todo.status)">{{ statusConfig[todo.status]?.label || todo.status }}</Badge></Button></DropdownMenuTrigger> <DropdownMenuContent align="start"> <DropdownMenuLabel>???????? ??????</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem @click="updateTodo(todo.id, { status: 'backlog' })"><span class="w-2 h-2 rounded-full bg-gray-400 mr-2"></span>???????</DropdownMenuItem> <DropdownMenuItem @click="updateTodo(todo.id, { status: 'todo' })"><span class="w-2 h-2 rounded-full bg-blue-400 mr-2"></span>????????????</DropdownMenuItem> <DropdownMenuItem @click="updateTodo(todo.id, { status: 'in_progress' })"><span class="w-2 h-2 rounded-full bg-yellow-400 mr-2"></span>?????????</DropdownMenuItem> <DropdownMenuItem @click="updateTodo(todo.id, { status: 'done' })"><span class="w-2 h-2 rounded-full bg-green-400 mr-2"></span>???????????</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </TableCell> <TableCell class="w-[60px]"> <DropdownMenu> <DropdownMenuTrigger asChild><Button variant="ghost" size="sm" class="h-8 w-8 p-0"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" /></svg></Button></DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem v-if="todo.is_working" @click="stopWorking(todo.id)"><svg class="w-4 h-4 mr-2 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" /></svg>????????</DropdownMenuItem> <DropdownMenuItem v-if="todo.status !== 'done' && !todo.is_working" @click="startWorking(todo.id)"><svg class="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>???????</DropdownMenuItem> <DropdownMenuItem @click="startEditing(todo)"><svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>???????????</DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem @click="handleDelete(todo.id)" class="text-red-600"><svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>?????</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </TableCell> </TableRow> </TableBody> </Table> </div> </template> </template> <!-- Footer --> <div class="flex items-center justify-between text-sm text-muted-foreground"> <span>{{ totalCount }} ????????</span> </div> </div> </template>