1
0

Большое обновление

1. Создание личных проектов
2. Управление командой
3. Приглашение участников
4. Уведомления

и многое другое...
This commit is contained in:
2026-01-18 20:17:02 +07:00
parent 250eac70a7
commit 190b4d0a5e
51 changed files with 6179 additions and 426 deletions

View File

@@ -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