Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2e9f9ed65 | ||
|
|
c5898fbf74 |
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.2.1'
|
app.version: '0.2.2'
|
||||||
|
|||||||
@@ -65,6 +65,20 @@
|
|||||||
@blur="touched.title = true"
|
@blur="touched.title = true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Project select (create mode with project list) -->
|
||||||
|
<div v-if="showProjectSelect" class="mt-4">
|
||||||
|
<MalioSelect
|
||||||
|
v-model="form.projectId"
|
||||||
|
:options="projectOptions"
|
||||||
|
label="Projet *"
|
||||||
|
empty-option-label="Sélectionner un projet"
|
||||||
|
min-width="w-full"
|
||||||
|
/>
|
||||||
|
<p v-if="touched.project && !form.projectId" class="mt-1 text-xs text-red-500">
|
||||||
|
Le projet est requis
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Two-column selects -->
|
<!-- Two-column selects -->
|
||||||
<div class="mt-4 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
|
<div class="mt-4 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
|
||||||
<MalioSelect
|
<MalioSelect
|
||||||
@@ -266,6 +280,8 @@ import type { TaskGroup } from '~/services/dto/task-group'
|
|||||||
import type { UserData } from '~/services/dto/user-data'
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
import { useTaskService } from '~/services/tasks'
|
import { useTaskService } from '~/services/tasks'
|
||||||
|
|
||||||
|
import type { Project } from '~/services/dto/project'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
task: Task | null
|
task: Task | null
|
||||||
@@ -276,6 +292,7 @@ const props = defineProps<{
|
|||||||
tags: TaskTag[]
|
tags: TaskTag[]
|
||||||
groups: TaskGroup[]
|
groups: TaskGroup[]
|
||||||
users: UserData[]
|
users: UserData[]
|
||||||
|
projects?: Project[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -318,10 +335,12 @@ const form = reactive({
|
|||||||
groupId: null as number | null,
|
groupId: null as number | null,
|
||||||
tagIds: [] as number[],
|
tagIds: [] as number[],
|
||||||
clientTicketId: null as number | null,
|
clientTicketId: null as number | null,
|
||||||
|
projectId: null as number | null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const touched = reactive({
|
const touched = reactive({
|
||||||
title: false,
|
title: false,
|
||||||
|
project: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusOptions = computed(() =>
|
const statusOptions = computed(() =>
|
||||||
@@ -340,8 +359,22 @@ const userOptions = computed(() =>
|
|||||||
props.users.map(u => ({ label: u.username, value: u.id }))
|
props.users.map(u => ({ label: u.username, value: u.id }))
|
||||||
)
|
)
|
||||||
|
|
||||||
const groupOptions = computed(() =>
|
const groupOptions = computed(() => {
|
||||||
props.groups.map(g => ({ label: g.title, value: g.id }))
|
let filtered = props.groups
|
||||||
|
if (showProjectSelect.value && form.projectId) {
|
||||||
|
filtered = filtered.filter(g => g.project?.id === form.projectId)
|
||||||
|
}
|
||||||
|
return filtered.map(g => ({ label: g.title, value: g.id }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const showProjectSelect = computed(() => !!props.projects?.length && !isEditing.value)
|
||||||
|
|
||||||
|
const projectOptions = computed(() =>
|
||||||
|
(props.projects ?? []).map(p => ({ label: p.name, value: p.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolvedProjectId = computed(() =>
|
||||||
|
showProjectSelect.value ? form.projectId : props.projectId
|
||||||
)
|
)
|
||||||
|
|
||||||
const canArchive = computed(() => {
|
const canArchive = computed(() => {
|
||||||
@@ -385,8 +418,10 @@ function populateForm(task: Task | null) {
|
|||||||
form.groupId = null
|
form.groupId = null
|
||||||
form.tagIds = []
|
form.tagIds = []
|
||||||
form.clientTicketId = null
|
form.clientTicketId = null
|
||||||
|
form.projectId = null
|
||||||
}
|
}
|
||||||
touched.title = false
|
touched.title = false
|
||||||
|
touched.project = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, async (open) => {
|
watch(() => props.modelValue, async (open) => {
|
||||||
@@ -394,9 +429,14 @@ watch(() => props.modelValue, async (open) => {
|
|||||||
confirmDeleteDocOpen.value = false
|
confirmDeleteDocOpen.value = false
|
||||||
documentToDelete.value = null
|
documentToDelete.value = null
|
||||||
populateForm(props.task)
|
populateForm(props.task)
|
||||||
try {
|
const pid = resolvedProjectId.value
|
||||||
clientTickets.value = await clientTicketService.getAll({ project: props.projectId })
|
if (pid) {
|
||||||
} catch {
|
try {
|
||||||
|
clientTickets.value = await clientTicketService.getAll({ project: pid })
|
||||||
|
} catch {
|
||||||
|
clientTickets.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
clientTickets.value = []
|
clientTickets.value = []
|
||||||
}
|
}
|
||||||
if (props.task?.project?.giteaOwner && props.task?.project?.giteaRepo && !giteaUrl.value) {
|
if (props.task?.project?.giteaOwner && props.task?.project?.giteaRepo && !giteaUrl.value) {
|
||||||
@@ -426,6 +466,22 @@ const clientTicketOptions = computed(() =>
|
|||||||
clientTickets.value.map(ct => ({ label: `CT-${String(ct.number).padStart(3, '0')} — ${ct.title}`, value: ct.id }))
|
clientTickets.value.map(ct => ({ label: `CT-${String(ct.number).padStart(3, '0')} — ${ct.title}`, value: ct.id }))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Reset group and reload client tickets when project changes in create mode
|
||||||
|
watch(() => form.projectId, async (pid) => {
|
||||||
|
if (!showProjectSelect.value) return
|
||||||
|
form.groupId = null
|
||||||
|
form.clientTicketId = null
|
||||||
|
if (pid) {
|
||||||
|
try {
|
||||||
|
clientTickets.value = await clientTicketService.getAll({ project: pid })
|
||||||
|
} catch {
|
||||||
|
clientTickets.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientTickets.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const isAdmin = computed(() => authStore.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
const isAdmin = computed(() => authStore.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
||||||
|
|
||||||
@@ -541,7 +597,9 @@ async function handleUnarchive() {
|
|||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
touched.title = true
|
touched.title = true
|
||||||
|
touched.project = true
|
||||||
if (!form.title.trim()) return
|
if (!form.title.trim()) return
|
||||||
|
if (showProjectSelect.value && !form.projectId) return
|
||||||
|
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
@@ -553,7 +611,7 @@ async function handleSubmit() {
|
|||||||
priority: form.priorityId ? `/api/task_priorities/${form.priorityId}` : null,
|
priority: form.priorityId ? `/api/task_priorities/${form.priorityId}` : null,
|
||||||
assignee: form.assigneeId ? `/api/users/${form.assigneeId}` : null,
|
assignee: form.assigneeId ? `/api/users/${form.assigneeId}` : null,
|
||||||
group: form.groupId ? `/api/task_groups/${form.groupId}` : null,
|
group: form.groupId ? `/api/task_groups/${form.groupId}` : null,
|
||||||
project: `/api/projects/${props.projectId}`,
|
project: `/api/projects/${resolvedProjectId.value}`,
|
||||||
tags: form.tagIds.map(id => `/api/task_tags/${id}`),
|
tags: form.tagIds.map(id => `/api/task_tags/${id}`),
|
||||||
clientTicket: form.clientTicketId ? `/api/client_tickets/${form.clientTicketId}` : null,
|
clientTicket: form.clientTicketId ? `/api/client_tickets/${form.clientTicketId}` : null,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,8 @@
|
|||||||
"allEfforts": "Tous les efforts",
|
"allEfforts": "Tous les efforts",
|
||||||
"allAssignees": "Tous",
|
"allAssignees": "Tous",
|
||||||
"noTasks": "Aucune tâche",
|
"noTasks": "Aucune tâche",
|
||||||
"backlog": "Backlog"
|
"backlog": "Backlog",
|
||||||
|
"createTask": "Créer une tâche"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Tableau de bord",
|
"title": "Tableau de bord",
|
||||||
|
|||||||
@@ -123,9 +123,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="h-full flex-1 flex flex-col min-h-0">
|
<div class="h-full flex-1 flex flex-col min-h-0 min-w-0">
|
||||||
<AppTopNav :user="auth.user" />
|
<AppTopNav :user="auth.user" />
|
||||||
<main class="flex flex-1 flex-col overflow-y-auto bg-white px-4 pb-24 sm:px-8 lg:px-16">
|
<main class="flex flex-1 flex-col overflow-y-auto overflow-x-hidden bg-white px-4 pb-24 sm:px-8 lg:px-16">
|
||||||
<div aria-hidden="true" class="pointer-events-none sticky top-0 z-30 h-8 flex-shrink-0 bg-white sm:h-12" />
|
<div aria-hidden="true" class="pointer-events-none sticky top-0 z-30 h-8 flex-shrink-0 bg-white sm:h-12" />
|
||||||
<slot/>
|
<slot/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -214,6 +214,11 @@ async function onDropBacklog(event: DragEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Modal
|
// Modal
|
||||||
|
function openTaskCreate() {
|
||||||
|
selectedTask.value = null
|
||||||
|
taskModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function openTaskEdit(task: Task) {
|
function openTaskEdit(task: Task) {
|
||||||
selectedTask.value = task
|
selectedTask.value = task
|
||||||
taskModalOpen.value = true
|
taskModalOpen.value = true
|
||||||
@@ -229,28 +234,37 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<!-- Header + Filters -->
|
<!-- Header + Filters -->
|
||||||
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
|
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
|
||||||
<div class="flex gap-1">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-center rounded-md p-1.5 transition-colors"
|
class="flex items-center gap-1.5 rounded-lg bg-primary-500 px-3 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-secondary-500"
|
||||||
:class="viewMode === 'kanban' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
|
@click="openTaskCreate"
|
||||||
:title="$t('myTasks.viewKanban')"
|
|
||||||
@click="viewMode = 'kanban'"
|
|
||||||
>
|
>
|
||||||
<Icon name="mdi:view-column-outline" size="18" />
|
<Icon name="mdi:plus" size="18" />
|
||||||
</button>
|
{{ $t('myTasks.createTask') }}
|
||||||
<button
|
|
||||||
class="flex items-center justify-center rounded-md p-1.5 transition-colors"
|
|
||||||
:class="viewMode === 'list' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
|
|
||||||
:title="$t('myTasks.viewList')"
|
|
||||||
@click="viewMode = 'list'"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:view-list-outline" size="18" />
|
|
||||||
</button>
|
</button>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-center rounded-md p-1.5 transition-colors"
|
||||||
|
:class="viewMode === 'kanban' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
|
||||||
|
:title="$t('myTasks.viewKanban')"
|
||||||
|
@click="viewMode = 'kanban'"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:view-column-outline" size="18" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-center rounded-md p-1.5 transition-colors"
|
||||||
|
:class="viewMode === 'list' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
|
||||||
|
:title="$t('myTasks.viewList')"
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:view-list-outline" size="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -314,11 +328,11 @@ onMounted(() => {
|
|||||||
|
|
||||||
<!-- Kanban View -->
|
<!-- Kanban View -->
|
||||||
<div v-if="viewMode === 'kanban'">
|
<div v-if="viewMode === 'kanban'">
|
||||||
<div class="mt-6 flex gap-4 overflow-x-auto pb-4">
|
<div class="mt-6 flex gap-3 overflow-x-auto pb-4">
|
||||||
<div
|
<div
|
||||||
v-for="status in sortedStatuses"
|
v-for="status in sortedStatuses"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
class="flex w-72 shrink-0 flex-col rounded-lg transition-colors"
|
class="flex min-w-36 flex-1 flex-col rounded-lg transition-colors"
|
||||||
:class="dragOverStatusId === status.id ? 'bg-neutral-200' : 'bg-neutral-50'"
|
:class="dragOverStatusId === status.id ? 'bg-neutral-200' : 'bg-neutral-50'"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@dragenter.prevent="onDragEnter(status.id)"
|
@dragenter.prevent="onDragEnter(status.id)"
|
||||||
@@ -446,6 +460,7 @@ onMounted(() => {
|
|||||||
:tags="tags"
|
:tags="tags"
|
||||||
:groups="selectedTask?.project?.id ? groups.filter(g => g.project?.id === selectedTask?.project?.id) : groups"
|
:groups="selectedTask?.project?.id ? groups.filter(g => g.project?.id === selectedTask?.project?.id) : groups"
|
||||||
:users="users"
|
:users="users"
|
||||||
|
:projects="projects"
|
||||||
@saved="onSaved"
|
@saved="onSaved"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
|
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ project?.name ?? '' }}</h1>
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ project?.name ?? '' }}</h1>
|
||||||
@@ -62,11 +62,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kanban -->
|
<!-- Kanban -->
|
||||||
<div class="mt-6 flex gap-4 overflow-x-auto pb-4">
|
<div class="mt-6 flex gap-3 overflow-x-auto pb-4">
|
||||||
<div
|
<div
|
||||||
v-for="status in statuses"
|
v-for="status in statuses"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
class="flex w-72 shrink-0 flex-col rounded-lg transition-colors"
|
class="flex min-w-36 flex-1 flex-col rounded-lg transition-colors"
|
||||||
:class="dragOverStatusId === status.id ? 'bg-neutral-200' : 'bg-neutral-50'"
|
:class="dragOverStatusId === status.id ? 'bg-neutral-200' : 'bg-neutral-50'"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@dragenter.prevent="onDragEnter(status.id)"
|
@dragenter.prevent="onDragEnter(status.id)"
|
||||||
|
|||||||
Reference in New Issue
Block a user