Большое обновление
1. Создание личных проектов 2. Управление командой 3. Приглашение участников 4. Уведомления и многое другое...
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
v-model="form.title"
|
||||
placeholder="Введите название задачи"
|
||||
ref="titleInputRef"
|
||||
:readonly="!canEdit"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@@ -12,11 +13,12 @@
|
||||
<TextInput
|
||||
v-model="form.description"
|
||||
placeholder="Краткое описание в одну строку..."
|
||||
:readonly="!canEdit"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Подробное описание">
|
||||
<template #actions>
|
||||
<template v-if="canEdit" #actions>
|
||||
<div class="format-buttons">
|
||||
<button type="button" class="format-btn" @mousedown.prevent="applyFormat('bold')" title="Жирный (Ctrl+B)">
|
||||
<i data-lucide="bold"></i>
|
||||
@@ -33,6 +35,7 @@
|
||||
v-model="form.details"
|
||||
placeholder="Подробное описание задачи, заметки, ссылки..."
|
||||
:show-toolbar="false"
|
||||
:disabled="!canEdit"
|
||||
ref="detailsEditorRef"
|
||||
/>
|
||||
</FormField>
|
||||
@@ -41,6 +44,7 @@
|
||||
<TagsSelect
|
||||
v-model="form.departmentId"
|
||||
:options="departmentOptions"
|
||||
:disabled="!canEdit"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@@ -48,12 +52,13 @@
|
||||
<TagsSelect
|
||||
v-model="form.labelId"
|
||||
:options="labelOptions"
|
||||
:disabled="!canEdit"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div class="field-row" :class="{ mobile: isMobile }">
|
||||
<FormField label="Срок выполнения">
|
||||
<DatePicker v-model="form.dueDate" />
|
||||
<DatePicker v-model="form.dueDate" :disabled="!canEdit" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Исполнитель">
|
||||
@@ -63,18 +68,21 @@
|
||||
searchable
|
||||
placeholder="Без исполнителя"
|
||||
empty-label="Без исполнителя"
|
||||
:disabled="!canEdit"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
v-if="canEdit || attachedFiles.length > 0"
|
||||
label="Прикреплённые файлы"
|
||||
hint="Разрешены: PNG, JPEG, JPG, ZIP, RAR (до 10 МБ)"
|
||||
:hint="canEdit ? 'Разрешены: PNG, JPEG, JPG, ZIP, RAR (до 10 МБ)' : ''"
|
||||
:error="fileError"
|
||||
>
|
||||
<FileUploader
|
||||
:files="attachedFiles"
|
||||
:get-full-url="getFullUrl"
|
||||
:read-only="!canEdit"
|
||||
@add="handleFileAdd"
|
||||
@remove="handleFileRemove"
|
||||
@preview="$emit('preview-image', $event)"
|
||||
@@ -126,6 +134,10 @@ const props = defineProps({
|
||||
users: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -173,13 +185,32 @@ const labelOptions = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
// Данные исполнителя из карточки (может быть удалённый участник)
|
||||
const cardAssignee = ref(null)
|
||||
|
||||
const userOptions = computed(() => {
|
||||
return props.users.map(user => ({
|
||||
value: user.id,
|
||||
const options = props.users.map(user => ({
|
||||
value: user.id_user, // id_user - это id пользователя из accounts
|
||||
label: user.name,
|
||||
subtitle: user.telegram,
|
||||
avatar: getFullUrl(user.avatar_url)
|
||||
}))
|
||||
|
||||
// Если текущий исполнитель не в списке участников — добавляем как виртуальную опцию
|
||||
if (cardAssignee.value && form.userId) {
|
||||
const exists = options.some(opt => Number(opt.value) === Number(form.userId))
|
||||
if (!exists) {
|
||||
options.unshift({
|
||||
value: form.userId,
|
||||
label: cardAssignee.value.name || 'Удалённый участник',
|
||||
subtitle: '',
|
||||
avatar: cardAssignee.value.avatar ? getFullUrl(cardAssignee.value.avatar) : null,
|
||||
disabled: true // Нельзя выбрать повторно
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
// Change tracking
|
||||
@@ -216,6 +247,7 @@ const resetForm = () => {
|
||||
form.labelId = 2 // Нормально по умолчанию
|
||||
form.dueDate = new Date().toISOString().split('T')[0]
|
||||
form.userId = null
|
||||
cardAssignee.value = null
|
||||
clearFiles()
|
||||
}
|
||||
|
||||
@@ -234,6 +266,16 @@ const loadFromCard = (card) => {
|
||||
form.dueDate = card.dueDate || ''
|
||||
form.userId = card.accountId || null
|
||||
|
||||
// Сохраняем данные исполнителя для случая если он удалён из проекта
|
||||
if (card.accountId && card.assignee) {
|
||||
cardAssignee.value = {
|
||||
avatar: card.assignee,
|
||||
name: null // Имя неизвестно, будет показываться аватарка
|
||||
}
|
||||
} else {
|
||||
cardAssignee.value = null
|
||||
}
|
||||
|
||||
if (card.files && card.files.length > 0) {
|
||||
attachedFiles.value = card.files.map(f => ({
|
||||
name: f.name,
|
||||
@@ -256,8 +298,15 @@ const applyFormat = (command) => {
|
||||
|
||||
const getAvatarByUserId = (userId) => {
|
||||
if (!userId) return null
|
||||
const user = props.users.find(u => u.id === userId)
|
||||
return user ? user.avatar_url : null
|
||||
const user = props.users.find(u => Number(u.id_user) === Number(userId))
|
||||
if (user) return user.avatar_url
|
||||
|
||||
// Fallback: если это тот же удалённый участник (userId не изменился), используем сохранённый аватар
|
||||
if (cardAssignee.value && Number(userId) === Number(initialForm.value.userId)) {
|
||||
return cardAssignee.value.avatar
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// File handlers
|
||||
|
||||
Reference in New Issue
Block a user