|
| 1 | +<script setup lang="ts"> |
| 2 | +import { onMounted, onUnmounted, computed } from 'vue'; |
| 3 | +import { router, usePage } from '@inertiajs/vue3'; |
| 4 | +import { CommandPalette } from '@/packages/ui/src/CommandPalette'; |
| 5 | +import { useCommandPalette } from '@/utils/useCommandPalette'; |
| 6 | +import { useProjectsStore } from '@/utils/useProjects'; |
| 7 | +import { useClientsStore } from '@/utils/useClients'; |
| 8 | +import { useTagsStore } from '@/utils/useTags'; |
| 9 | +import { useTimeEntriesMutations } from '@/utils/useTimeEntriesMutations'; |
| 10 | +import { getOrganizationCurrencyString } from '@/utils/money'; |
| 11 | +import { isAllowedToPerformPremiumAction } from '@/utils/billing'; |
| 12 | +import { canCreateProjects } from '@/utils/permissions'; |
| 13 | +import type { |
| 14 | + CreateClientBody, |
| 15 | + CreateProjectBody, |
| 16 | + CreateTimeEntryBody, |
| 17 | + Project, |
| 18 | + Client, |
| 19 | + Tag, |
| 20 | +} from '@/packages/api/src'; |
| 21 | +import type { User } from '@/types/models'; |
| 22 | +import type { Role } from '@/types/jetstream'; |
| 23 | +
|
| 24 | +// Import modals |
| 25 | +import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue'; |
| 26 | +import ClientCreateModal from '@/Components/Common/Client/ClientCreateModal.vue'; |
| 27 | +import TaskCreateModal from '@/Components/Common/Task/TaskCreateModal.vue'; |
| 28 | +import TagCreateModal from '@/packages/ui/src/Tag/TagCreateModal.vue'; |
| 29 | +import MemberInviteModal from '@/Components/Common/Member/MemberInviteModal.vue'; |
| 30 | +import TimeEntryCreateModal from '@/packages/ui/src/TimeEntry/TimeEntryCreateModal.vue'; |
| 31 | +
|
| 32 | +// Import dropdowns for active timer selectors |
| 33 | +import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue'; |
| 34 | +import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue'; |
| 35 | +
|
| 36 | +// Dialog components for selectors |
| 37 | +import DialogModal from '@/packages/ui/src/DialogModal.vue'; |
| 38 | +import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue'; |
| 39 | +
|
| 40 | +const { |
| 41 | + isOpen, |
| 42 | + searchTerm, |
| 43 | + groups, |
| 44 | + entityResults, |
| 45 | + togglePalette, |
| 46 | + showCreateProjectModal, |
| 47 | + showCreateClientModal, |
| 48 | + showCreateTaskModal, |
| 49 | + showCreateTagModal, |
| 50 | + showInviteMemberModal, |
| 51 | + showCreateTimeEntryModal, |
| 52 | + showProjectSelector, |
| 53 | + showTaskSelector, |
| 54 | + showTagsSelector, |
| 55 | + currentTimeEntry, |
| 56 | + updateTimer, |
| 57 | + projects, |
| 58 | + clients, |
| 59 | + tasks, |
| 60 | + tags, |
| 61 | +} = useCommandPalette(); |
| 62 | +
|
| 63 | +// Stores for creating entities |
| 64 | +const projectsStore = useProjectsStore(); |
| 65 | +const clientsStore = useClientsStore(); |
| 66 | +const tagsStore = useTagsStore(); |
| 67 | +
|
| 68 | +// Time entry mutations |
| 69 | +const { createTimeEntry: createTimeEntryMutation } = useTimeEntriesMutations(); |
| 70 | +
|
| 71 | +// Get available roles from page props (for member invite modal) |
| 72 | +const page = usePage<{ |
| 73 | + availableRoles?: Role[]; |
| 74 | + auth: { |
| 75 | + user: User; |
| 76 | + }; |
| 77 | +}>(); |
| 78 | +
|
| 79 | +const availableRoles = computed(() => page.props.availableRoles ?? []); |
| 80 | +
|
| 81 | +// Active clients for dropdowns |
| 82 | +const activeClients = computed(() => clients.value.filter((c) => !c.is_archived)); |
| 83 | +
|
| 84 | +// Keyboard shortcut handler |
| 85 | +function handleKeyDown(e: KeyboardEvent) { |
| 86 | + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { |
| 87 | + e.preventDefault(); |
| 88 | + togglePalette(); |
| 89 | + } |
| 90 | +} |
| 91 | +
|
| 92 | +onMounted(() => { |
| 93 | + document.addEventListener('keydown', handleKeyDown); |
| 94 | +}); |
| 95 | +
|
| 96 | +onUnmounted(() => { |
| 97 | + document.removeEventListener('keydown', handleKeyDown); |
| 98 | +}); |
| 99 | +
|
| 100 | +// Project creation |
| 101 | +async function createProject(project: CreateProjectBody): Promise<Project | undefined> { |
| 102 | + const openedFromCommandPalette = showCreateProjectModal.value; |
| 103 | + const newProject = await projectsStore.createProject(project); |
| 104 | + showCreateProjectModal.value = false; |
| 105 | + if (newProject && openedFromCommandPalette) { |
| 106 | + router.visit(route('projects.show', { project: newProject.id })); |
| 107 | + } |
| 108 | + return newProject; |
| 109 | +} |
| 110 | +
|
| 111 | +async function createClient(client: CreateClientBody): Promise<Client | undefined> { |
| 112 | + const openedFromCommandPalette = showCreateClientModal.value; |
| 113 | + const newClient = await clientsStore.createClient(client); |
| 114 | + if (newClient && openedFromCommandPalette) { |
| 115 | + showCreateClientModal.value = false; |
| 116 | + router.visit(route('clients')); |
| 117 | + } |
| 118 | + return newClient; |
| 119 | +} |
| 120 | +
|
| 121 | +async function createTag(name: string): Promise<Tag | undefined> { |
| 122 | + const openedFromCommandPalette = showCreateTagModal.value; |
| 123 | + const newTag = await tagsStore.createTag(name); |
| 124 | + if (newTag && openedFromCommandPalette) { |
| 125 | + showCreateTagModal.value = false; |
| 126 | + router.visit(route('tags')); |
| 127 | + } |
| 128 | + return newTag; |
| 129 | +} |
| 130 | +
|
| 131 | +async function createTimeEntry(timeEntry: Omit<CreateTimeEntryBody, 'member_id'>) { |
| 132 | + await createTimeEntryMutation(timeEntry); |
| 133 | + showCreateTimeEntryModal.value = false; |
| 134 | +} |
| 135 | +
|
| 136 | +async function handleProjectTaskSelect() { |
| 137 | + showProjectSelector.value = false; |
| 138 | + showTaskSelector.value = false; |
| 139 | + await updateTimer(); |
| 140 | +} |
| 141 | +
|
| 142 | +async function handleTagsSelect() { |
| 143 | + showTagsSelector.value = false; |
| 144 | + await updateTimer(); |
| 145 | +} |
| 146 | +
|
| 147 | +const firstProjectId = computed(() => projects.value[0]?.id ?? ''); |
| 148 | +</script> |
| 149 | + |
| 150 | +<template> |
| 151 | + <!-- Command Palette Dialog --> |
| 152 | + <CommandPalette |
| 153 | + v-model:open="isOpen" |
| 154 | + v-model:search-term="searchTerm" |
| 155 | + :groups="groups" |
| 156 | + :entity-results="entityResults" /> |
| 157 | + |
| 158 | + <!-- Project Create Modal --> |
| 159 | + <ProjectCreateModal |
| 160 | + v-model:show="showCreateProjectModal" |
| 161 | + :create-project="createProject" |
| 162 | + :create-client="createClient" |
| 163 | + :clients="activeClients" |
| 164 | + :currency="getOrganizationCurrencyString()" |
| 165 | + :enable-estimated-time="isAllowedToPerformPremiumAction()" /> |
| 166 | + |
| 167 | + <!-- Client Create Modal --> |
| 168 | + <ClientCreateModal v-model:show="showCreateClientModal" /> |
| 169 | + |
| 170 | + <!-- Task Create Modal --> |
| 171 | + <TaskCreateModal |
| 172 | + v-if="firstProjectId" |
| 173 | + v-model:show="showCreateTaskModal" |
| 174 | + :project-id="firstProjectId" /> |
| 175 | + |
| 176 | + <!-- Tag Create Modal --> |
| 177 | + <TagCreateModal v-model:show="showCreateTagModal" :create-tag="createTag" /> |
| 178 | + |
| 179 | + <!-- Member Invite Modal --> |
| 180 | + <MemberInviteModal v-model:show="showInviteMemberModal" :available-roles="availableRoles" /> |
| 181 | + |
| 182 | + <!-- Time Entry Create Modal --> |
| 183 | + <TimeEntryCreateModal |
| 184 | + v-model:show="showCreateTimeEntryModal" |
| 185 | + :create-time-entry="createTimeEntry" |
| 186 | + :create-project="createProject" |
| 187 | + :create-client="createClient" |
| 188 | + :create-tag="createTag" |
| 189 | + :projects="projects" |
| 190 | + :tasks="tasks" |
| 191 | + :tags="tags" |
| 192 | + :clients="activeClients" |
| 193 | + :currency="getOrganizationCurrencyString()" |
| 194 | + :enable-estimated-time="isAllowedToPerformPremiumAction()" |
| 195 | + :can-create-project="canCreateProjects()" /> |
| 196 | + |
| 197 | + <!-- Project Selector Dialog for Active Timer --> |
| 198 | + <DialogModal :show="showProjectSelector" closeable @close="showProjectSelector = false"> |
| 199 | + <template #title>Set Project</template> |
| 200 | + <template #content> |
| 201 | + <TimeTrackerProjectTaskDropdown |
| 202 | + v-model:project="currentTimeEntry.project_id" |
| 203 | + v-model:task="currentTimeEntry.task_id" |
| 204 | + :projects="projects" |
| 205 | + :tasks="tasks" |
| 206 | + :clients="activeClients" |
| 207 | + :create-project="createProject" |
| 208 | + :create-client="createClient" |
| 209 | + :can-create-project="canCreateProjects()" |
| 210 | + :currency="getOrganizationCurrencyString()" |
| 211 | + :enable-estimated-time="isAllowedToPerformPremiumAction()" |
| 212 | + size="xlarge" |
| 213 | + class="w-full" /> |
| 214 | + </template> |
| 215 | + <template #footer> |
| 216 | + <SecondaryButton @click="showProjectSelector = false"> Cancel </SecondaryButton> |
| 217 | + <SecondaryButton class="ms-3" @click="handleProjectTaskSelect"> Save </SecondaryButton> |
| 218 | + </template> |
| 219 | + </DialogModal> |
| 220 | + |
| 221 | + <!-- Task Selector Dialog for Active Timer --> |
| 222 | + <DialogModal :show="showTaskSelector" closeable @close="showTaskSelector = false"> |
| 223 | + <template #title>Set Task</template> |
| 224 | + <template #content> |
| 225 | + <TimeTrackerProjectTaskDropdown |
| 226 | + v-model:project="currentTimeEntry.project_id" |
| 227 | + v-model:task="currentTimeEntry.task_id" |
| 228 | + :projects="projects" |
| 229 | + :tasks="tasks" |
| 230 | + :clients="activeClients" |
| 231 | + :create-project="createProject" |
| 232 | + :create-client="createClient" |
| 233 | + :can-create-project="canCreateProjects()" |
| 234 | + :currency="getOrganizationCurrencyString()" |
| 235 | + :enable-estimated-time="isAllowedToPerformPremiumAction()" |
| 236 | + size="xlarge" |
| 237 | + class="w-full" /> |
| 238 | + </template> |
| 239 | + <template #footer> |
| 240 | + <SecondaryButton @click="showTaskSelector = false"> Cancel </SecondaryButton> |
| 241 | + <SecondaryButton class="ms-3" @click="handleProjectTaskSelect"> Save </SecondaryButton> |
| 242 | + </template> |
| 243 | + </DialogModal> |
| 244 | + |
| 245 | + <!-- Tags Selector Dialog for Active Timer --> |
| 246 | + <DialogModal :show="showTagsSelector" closeable @close="showTagsSelector = false"> |
| 247 | + <template #title>Set Tags</template> |
| 248 | + <template #content> |
| 249 | + <TagDropdown v-model="currentTimeEntry.tags" :tags="tags" :create-tag="createTag"> |
| 250 | + <template #trigger> |
| 251 | + <div |
| 252 | + class="w-full p-3 border border-card-border rounded-lg cursor-pointer hover:bg-tertiary transition"> |
| 253 | + <span |
| 254 | + v-if="currentTimeEntry.tags.length === 0" |
| 255 | + class="text-muted-foreground"> |
| 256 | + Click to select tags... |
| 257 | + </span> |
| 258 | + <span v-else> {{ currentTimeEntry.tags.length }} tag(s) selected </span> |
| 259 | + </div> |
| 260 | + </template> |
| 261 | + </TagDropdown> |
| 262 | + </template> |
| 263 | + <template #footer> |
| 264 | + <SecondaryButton @click="showTagsSelector = false"> Cancel </SecondaryButton> |
| 265 | + <SecondaryButton class="ms-3" @click="handleTagsSelect"> Save </SecondaryButton> |
| 266 | + </template> |
| 267 | + </DialogModal> |
| 268 | +</template> |
0 commit comments