Добавление логики
1. Получения конфигурациия с бека 2. Время закрытия задачи 3. Изменение фронта под новую локигу конфигурации 4. Обновление структуры бд
This commit is contained in:
@@ -6,6 +6,13 @@ if ($method === 'POST') {
|
|||||||
$data = RestApi::getInput();
|
$data = RestApi::getInput();
|
||||||
$action = $data['action'] ?? null;
|
$action = $data['action'] ?? null;
|
||||||
|
|
||||||
|
// Получение конфигурации приложения
|
||||||
|
if ($action === 'get_config') {
|
||||||
|
RestApi::response(['success' => true, 'data' => [
|
||||||
|
'COLUMN_DONE_ID' => COLUMN_DONE_ID
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
// Авторизация
|
// Авторизация
|
||||||
if ($action === 'auth_login') {
|
if ($action === 'auth_login') {
|
||||||
$account = new Account();
|
$account = new Account();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Task extends BaseEntity {
|
|||||||
public $order;
|
public $order;
|
||||||
public $column_id;
|
public $column_id;
|
||||||
public $date;
|
public $date;
|
||||||
|
public $date_closed;
|
||||||
public $id_account;
|
public $id_account;
|
||||||
public $title;
|
public $title;
|
||||||
public $descript;
|
public $descript;
|
||||||
@@ -99,15 +100,18 @@ class Task extends BaseEntity {
|
|||||||
return $errors;
|
return $errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка что задача существует
|
// Проверка что задача существует и получаем текущую колонку
|
||||||
$task = Database::get($this->db_name, ['id'], ['id' => $this->id]);
|
$task = Database::get($this->db_name, ['id', 'column_id'], ['id' => $this->id]);
|
||||||
if (!$task) {
|
if (!$task) {
|
||||||
$this->addError('task', 'Задача не найдена');
|
$this->addError('task', 'Задача не найдена');
|
||||||
return $this->getErrors();
|
return $this->getErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем в БД
|
$old_column_id = (int)$task['column_id'];
|
||||||
Database::update($this->db_name, [
|
$new_column_id = (int)$this->column_id;
|
||||||
|
|
||||||
|
// Формируем данные для обновления
|
||||||
|
$update_data = [
|
||||||
'id_department' => $this->id_department,
|
'id_department' => $this->id_department,
|
||||||
'id_label' => $this->id_label,
|
'id_label' => $this->id_label,
|
||||||
'order' => $this->order,
|
'order' => $this->order,
|
||||||
@@ -117,7 +121,17 @@ class Task extends BaseEntity {
|
|||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'descript' => $this->descript ?: null,
|
'descript' => $this->descript ?: null,
|
||||||
'descript_full' => $this->descript_full ?: null
|
'descript_full' => $this->descript_full ?: null
|
||||||
], [
|
];
|
||||||
|
|
||||||
|
// Обновляем date_closed при смене колонки
|
||||||
|
if ($new_column_id === COLUMN_DONE_ID && $old_column_id !== COLUMN_DONE_ID) {
|
||||||
|
$update_data['date_closed'] = date('Y-m-d H:i:s');
|
||||||
|
} elseif ($old_column_id === COLUMN_DONE_ID && $new_column_id !== COLUMN_DONE_ID) {
|
||||||
|
$update_data['date_closed'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем в БД
|
||||||
|
Database::update($this->db_name, $update_data, [
|
||||||
'id' => $this->id
|
'id' => $this->id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -151,7 +165,9 @@ class Task extends BaseEntity {
|
|||||||
public static function updateOrder($id, $column_id, $to_index) {
|
public static function updateOrder($id, $column_id, $to_index) {
|
||||||
|
|
||||||
// Проверка что задача существует
|
// Проверка что задача существует
|
||||||
self::check_task($id);
|
$task = self::check_task($id);
|
||||||
|
$old_column_id = (int)$task['column_id'];
|
||||||
|
$new_column_id = (int)$column_id;
|
||||||
|
|
||||||
// Получаем все карточки целевой колонки (кроме перемещаемой)
|
// Получаем все карточки целевой колонки (кроме перемещаемой)
|
||||||
$cards = Database::select('cards_task', ['id', 'order'], [
|
$cards = Database::select('cards_task', ['id', 'order'], [
|
||||||
@@ -165,10 +181,24 @@ class Task extends BaseEntity {
|
|||||||
|
|
||||||
// Пересчитываем order для всех карточек
|
// Пересчитываем order для всех карточек
|
||||||
foreach ($cards as $index => $card) {
|
foreach ($cards as $index => $card) {
|
||||||
Database::update('cards_task', [
|
$update_data = [
|
||||||
'order' => $index,
|
'order' => $index,
|
||||||
'column_id' => $column_id
|
'column_id' => $column_id
|
||||||
], [
|
];
|
||||||
|
|
||||||
|
// Только для перемещаемой карточки обновляем date_closed
|
||||||
|
if ($card['id'] == $id) {
|
||||||
|
// Перемещаем В колонку "Готово" — устанавливаем дату закрытия
|
||||||
|
if ($new_column_id === COLUMN_DONE_ID && $old_column_id !== COLUMN_DONE_ID) {
|
||||||
|
$update_data['date_closed'] = date('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
// Перемещаем ИЗ колонки "Готово" — обнуляем дату
|
||||||
|
elseif ($old_column_id === COLUMN_DONE_ID && $new_column_id !== COLUMN_DONE_ID) {
|
||||||
|
$update_data['date_closed'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::update('cards_task', $update_data, [
|
||||||
'id' => $card['id']
|
'id' => $card['id']
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -193,6 +223,7 @@ class Task extends BaseEntity {
|
|||||||
'column_id',
|
'column_id',
|
||||||
'date',
|
'date',
|
||||||
'date_create',
|
'date_create',
|
||||||
|
'date_closed',
|
||||||
'file_img',
|
'file_img',
|
||||||
'title',
|
'title',
|
||||||
'descript',
|
'descript',
|
||||||
@@ -253,13 +284,13 @@ class Task extends BaseEntity {
|
|||||||
return $task;
|
return $task;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Установка статуса архивации задачи (только для задач в колонке 4)
|
// Установка статуса архивации задачи (только для задач в колонке "Готово")
|
||||||
public static function setArchive($id, $archive = 1) {
|
public static function setArchive($id, $archive = 1) {
|
||||||
// Проверка что задача существует
|
// Проверка что задача существует
|
||||||
$task = self::check_task($id);
|
$task = self::check_task($id);
|
||||||
|
|
||||||
// Архивировать можно только задачи в колонке 4
|
// Архивировать можно только задачи в колонке "Готово"
|
||||||
if ($archive && $task['column_id'] != 4) {
|
if ($archive && (int)$task['column_id'] !== COLUMN_DONE_ID) {
|
||||||
RestApi::response([
|
RestApi::response([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'errors' => ['column' => 'Архивировать можно только задачи из колонки "Готово"']
|
'errors' => ['column' => 'Архивировать можно только задачи из колонки "Готово"']
|
||||||
|
|||||||
@@ -23,12 +23,15 @@
|
|||||||
define('DB_PORT', 3306);
|
define('DB_PORT', 3306);
|
||||||
define('DB_CHARSET', 'utf8mb4');
|
define('DB_CHARSET', 'utf8mb4');
|
||||||
|
|
||||||
|
// ID колонки "Готово" (для фиксации date_closed и архивации)
|
||||||
|
define('COLUMN_DONE_ID', 4);
|
||||||
|
|
||||||
// Инициализация подключения к БД
|
// Инициализация подключения к БД
|
||||||
Database::init();
|
Database::init();
|
||||||
|
|
||||||
$routes = [
|
$routes = [
|
||||||
'/api/user' => 'api/user.php',
|
'/api/user' => __DIR__ . '/../api/user.php',
|
||||||
'/api/task' => 'api/task.php',
|
'/api/task' => __DIR__ . '/../api/task.php',
|
||||||
];
|
];
|
||||||
|
|
||||||
$publicActions = ['auth_login', 'check_session'];
|
$publicActions = ['auth_login', 'check_session'];
|
||||||
|
|||||||
@@ -125,3 +125,25 @@ export const taskImageApi = {
|
|||||||
export const usersApi = {
|
export const usersApi = {
|
||||||
getAll: () => request('/api/user', { credentials: 'include' })
|
getAll: () => request('/api/user', { credentials: 'include' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== CONFIG ====================
|
||||||
|
export const configApi = {
|
||||||
|
get: () => request('/api/user', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: 'get_config' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка конфига с сервера и мерж с window.APP_CONFIG
|
||||||
|
export const loadServerConfig = async () => {
|
||||||
|
try {
|
||||||
|
const result = await configApi.get()
|
||||||
|
if (result.success && result.data) {
|
||||||
|
window.APP_CONFIG = { ...window.APP_CONFIG, ...result.data }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки конфига:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,6 +86,7 @@ const columnsWithCards = computed(() => {
|
|||||||
assignee: card.avatar_img,
|
assignee: card.avatar_img,
|
||||||
dueDate: card.date,
|
dueDate: card.date,
|
||||||
dateCreate: card.date_create,
|
dateCreate: card.date_create,
|
||||||
|
dateClosed: card.date_closed,
|
||||||
files: card.files || (card.file_img || []).map(f => ({
|
files: card.files || (card.file_img || []).map(f => ({
|
||||||
name: f.name,
|
name: f.name,
|
||||||
url: f.url,
|
url: f.url,
|
||||||
@@ -133,9 +134,18 @@ const handleDropCard = async ({ cardId, fromColumnId, toColumnId, toIndex }) =>
|
|||||||
const card = localCards.value.find(c => c.id === cardId)
|
const card = localCards.value.find(c => c.id === cardId)
|
||||||
if (!card) return
|
if (!card) return
|
||||||
|
|
||||||
|
const doneColumnId = window.APP_CONFIG.COLUMN_DONE_ID
|
||||||
|
|
||||||
// Локально обновляем для мгновенного отклика
|
// Локально обновляем для мгновенного отклика
|
||||||
card.column_id = toColumnId
|
card.column_id = toColumnId
|
||||||
|
|
||||||
|
// Обновляем date_closed при перемещении в/из колонки "Готово"
|
||||||
|
if (toColumnId === doneColumnId && fromColumnId !== doneColumnId) {
|
||||||
|
card.date_closed = new Date().toISOString()
|
||||||
|
} else if (fromColumnId === doneColumnId && toColumnId !== doneColumnId) {
|
||||||
|
card.date_closed = null
|
||||||
|
}
|
||||||
|
|
||||||
// Получаем карточки целевой колонки (без перемещаемой)
|
// Получаем карточки целевой колонки (без перемещаемой)
|
||||||
const columnCards = localCards.value
|
const columnCards = localCards.value
|
||||||
.filter(c => c.column_id === toColumnId && c.id !== cardId)
|
.filter(c => c.column_id === toColumnId && c.id !== cardId)
|
||||||
|
|||||||
@@ -52,9 +52,12 @@
|
|||||||
<span v-if="card.dateCreate" class="date-create">
|
<span v-if="card.dateCreate" class="date-create">
|
||||||
Создано: {{ formatDateWithYear(card.dateCreate) }}
|
Создано: {{ formatDateWithYear(card.dateCreate) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="card.dueDate && Number(columnId) !== 4" class="due-date" :class="dueDateStatus">
|
<span v-if="card.dueDate && Number(columnId) !== doneColumnId" class="due-date" :class="dueDateStatus">
|
||||||
{{ daysLeftText }}
|
{{ daysLeftText }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="Number(columnId) === doneColumnId && card.dateClosed" class="date-closed">
|
||||||
|
Закрыто: {{ closedDateText }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -155,9 +158,28 @@ const isAvatarUrl = (value) => {
|
|||||||
return value && (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/'))
|
return value && (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Можно ли архивировать (только если колонка 4)
|
// Форматирование даты закрытия (относительный формат)
|
||||||
|
const closedDateText = computed(() => {
|
||||||
|
if (!props.card.dateClosed) return ''
|
||||||
|
const closed = new Date(props.card.dateClosed)
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
closed.setHours(0, 0, 0, 0)
|
||||||
|
const daysAgo = Math.round((today - closed) / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (daysAgo === 0) return 'Сегодня'
|
||||||
|
if (daysAgo === 1) return 'Вчера'
|
||||||
|
if (daysAgo >= 2 && daysAgo <= 4) return `${daysAgo} дня назад`
|
||||||
|
if (daysAgo >= 5 && daysAgo <= 14) return `${daysAgo} дней назад`
|
||||||
|
return formatDateWithYear(props.card.dateClosed)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ID колонки "Готово" из конфига
|
||||||
|
const doneColumnId = window.APP_CONFIG.COLUMN_DONE_ID
|
||||||
|
|
||||||
|
// Можно ли архивировать (только если колонка "Готово")
|
||||||
const canArchive = computed(() => {
|
const canArchive = computed(() => {
|
||||||
return Number(props.columnId) === 4
|
return Number(props.columnId) === doneColumnId
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleArchive = () => {
|
const handleArchive = () => {
|
||||||
@@ -297,6 +319,11 @@ const handleArchive = () => {
|
|||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-closed {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--green, #00d4aa);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-archive-card {
|
.btn-archive-card {
|
||||||
display: none;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -69,14 +69,12 @@
|
|||||||
|
|
||||||
<FormField label="Исполнитель">
|
<FormField label="Исполнитель">
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
v-if="!usersLoading"
|
|
||||||
v-model="form.userId"
|
v-model="form.userId"
|
||||||
:options="userOptions"
|
:options="userOptions"
|
||||||
searchable
|
searchable
|
||||||
placeholder="Без исполнителя"
|
placeholder="Без исполнителя"
|
||||||
empty-label="Без исполнителя"
|
empty-label="Без исполнителя"
|
||||||
/>
|
/>
|
||||||
<div v-else class="users-loading">Загрузка...</div>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -181,7 +179,7 @@ import SelectDropdown from './ui/SelectDropdown.vue'
|
|||||||
import TagsSelect from './ui/TagsSelect.vue'
|
import TagsSelect from './ui/TagsSelect.vue'
|
||||||
import FileUploader from './ui/FileUploader.vue'
|
import FileUploader from './ui/FileUploader.vue'
|
||||||
import ImagePreview from './ui/ImagePreview.vue'
|
import ImagePreview from './ui/ImagePreview.vue'
|
||||||
import { usersApi, taskImageApi, getFullUrl } from '../api'
|
import { taskImageApi, getFullUrl } from '../api'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
@@ -194,14 +192,16 @@ const props = defineProps({
|
|||||||
labels: {
|
labels: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'save', 'delete', 'archive'])
|
const emit = defineEmits(['close', 'save', 'delete', 'archive'])
|
||||||
|
|
||||||
const isNew = ref(true)
|
const isNew = ref(true)
|
||||||
const users = ref([])
|
|
||||||
const usersLoading = ref(false)
|
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@@ -237,7 +237,7 @@ const labelOptions = computed(() => {
|
|||||||
|
|
||||||
// Преобразование users в формат для SelectDropdown
|
// Преобразование users в формат для SelectDropdown
|
||||||
const userOptions = computed(() => {
|
const userOptions = computed(() => {
|
||||||
return users.value.map(user => ({
|
return props.users.map(user => ({
|
||||||
value: user.id,
|
value: user.id,
|
||||||
label: user.name,
|
label: user.name,
|
||||||
subtitle: user.telegram,
|
subtitle: user.telegram,
|
||||||
@@ -309,23 +309,9 @@ const cancelClose = () => {
|
|||||||
showUnsavedDialog.value = false
|
showUnsavedDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
|
||||||
usersLoading.value = true
|
|
||||||
try {
|
|
||||||
const data = await usersApi.getAll()
|
|
||||||
if (data.success) {
|
|
||||||
users.value = data.data
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка загрузки пользователей:', error)
|
|
||||||
} finally {
|
|
||||||
usersLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAvatarByUserId = (userId) => {
|
const getAvatarByUserId = (userId) => {
|
||||||
if (!userId) return null
|
if (!userId) return null
|
||||||
const user = users.value.find(u => u.id === userId)
|
const user = props.users.find(u => u.id === userId)
|
||||||
return user ? user.avatar_url : null
|
return user ? user.avatar_url : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,8 +342,6 @@ watch(() => props.show, async (newVal) => {
|
|||||||
isNew.value = !props.card
|
isNew.value = !props.card
|
||||||
clearFiles()
|
clearFiles()
|
||||||
|
|
||||||
await fetchUsers()
|
|
||||||
|
|
||||||
if (props.card) {
|
if (props.card) {
|
||||||
form.title = props.card.title || ''
|
form.title = props.card.title || ''
|
||||||
form.description = props.card.description || ''
|
form.description = props.card.description || ''
|
||||||
@@ -456,9 +440,9 @@ const handleDelete = () => {
|
|||||||
showDeleteDialog.value = true
|
showDeleteDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Можно ли архивировать (только если колонка 4)
|
// Можно ли архивировать (только если колонка "Готово")
|
||||||
const canArchive = computed(() => {
|
const canArchive = computed(() => {
|
||||||
return Number(props.columnId) === 4
|
return Number(props.columnId) === window.APP_CONFIG.COLUMN_DONE_ID
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleArchive = () => {
|
const handleArchive = () => {
|
||||||
@@ -593,11 +577,6 @@ onUpdated(refreshIcons)
|
|||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.users-loading {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon.btn-delete {
|
.btn-icon.btn-delete {
|
||||||
border: 1px solid var(--red);
|
border: 1px solid var(--red);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div class="dropdown-list">
|
<div class="dropdown-list" ref="listRef">
|
||||||
<!-- Опция "не выбрано" -->
|
<!-- Опция "не выбрано" -->
|
||||||
<button
|
<button
|
||||||
v-if="allowEmpty"
|
v-if="allowEmpty"
|
||||||
@@ -114,6 +114,7 @@ const emit = defineEmits(['update:modelValue', 'change'])
|
|||||||
|
|
||||||
const dropdownRef = ref(null)
|
const dropdownRef = ref(null)
|
||||||
const searchInputRef = ref(null)
|
const searchInputRef = ref(null)
|
||||||
|
const listRef = ref(null)
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const openUp = ref(false)
|
const openUp = ref(false)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
@@ -134,12 +135,23 @@ const filteredOptions = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Прокрутка к активному элементу
|
||||||
|
const scrollToActive = () => {
|
||||||
|
if (listRef.value && props.modelValue) {
|
||||||
|
const activeItem = listRef.value.querySelector('.dropdown-item.active')
|
||||||
|
if (activeItem) {
|
||||||
|
activeItem.scrollIntoView({ block: 'center', behavior: 'instant' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Переключение dropdown
|
// Переключение dropdown
|
||||||
const toggleDropdown = async () => {
|
const toggleDropdown = async () => {
|
||||||
isOpen.value = !isOpen.value
|
isOpen.value = !isOpen.value
|
||||||
if (isOpen.value) {
|
if (isOpen.value) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updatePosition()
|
updatePosition()
|
||||||
|
scrollToActive()
|
||||||
searchInputRef.value?.focus()
|
searchInputRef.value?.focus()
|
||||||
refreshIcons()
|
refreshIcons()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import MainApp from './views/MainApp.vue'
|
import MainApp from './views/MainApp.vue'
|
||||||
import LoginPage from './views/LoginPage.vue'
|
import LoginPage from './views/LoginPage.vue'
|
||||||
import TeamPage from './views/TeamPage.vue'
|
import TeamPage from './views/TeamPage.vue'
|
||||||
import { authApi } from './api'
|
import { authApi, loadServerConfig } from './api'
|
||||||
|
|
||||||
|
// Флаг загрузки конфига (один раз за сессию)
|
||||||
|
let configLoaded = false
|
||||||
|
|
||||||
// Проверка авторизации
|
// Проверка авторизации
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
@@ -50,6 +53,11 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
// Уже авторизован — на главную
|
// Уже авторизован — на главную
|
||||||
next('/')
|
next('/')
|
||||||
} else {
|
} else {
|
||||||
|
// Загружаем конфиг с сервера один раз для защищённых страниц
|
||||||
|
if (to.meta.requiresAuth && isAuth && !configLoaded) {
|
||||||
|
await loadServerConfig()
|
||||||
|
configLoaded = true
|
||||||
|
}
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
:column-id="editingColumnId"
|
:column-id="editingColumnId"
|
||||||
:departments="departments"
|
:departments="departments"
|
||||||
:labels="labels"
|
:labels="labels"
|
||||||
|
:users="users"
|
||||||
@close="closePanel"
|
@close="closePanel"
|
||||||
@save="handleSaveTask"
|
@save="handleSaveTask"
|
||||||
@delete="handleDeleteTask"
|
@delete="handleDeleteTask"
|
||||||
@@ -84,7 +85,7 @@ import Sidebar from '../components/Sidebar.vue'
|
|||||||
import Header from '../components/Header.vue'
|
import Header from '../components/Header.vue'
|
||||||
import Board from '../components/Board.vue'
|
import Board from '../components/Board.vue'
|
||||||
import TaskPanel from '../components/TaskPanel.vue'
|
import TaskPanel from '../components/TaskPanel.vue'
|
||||||
import { departmentsApi, labelsApi, columnsApi, cardsApi } from '../api'
|
import { departmentsApi, labelsApi, columnsApi, cardsApi, usersApi } from '../api'
|
||||||
|
|
||||||
// Активный фильтр по отделу (null = все)
|
// Активный фильтр по отделу (null = все)
|
||||||
// Восстанавливаем из localStorage
|
// Восстанавливаем из localStorage
|
||||||
@@ -106,21 +107,24 @@ const departments = ref([])
|
|||||||
const labels = ref([])
|
const labels = ref([])
|
||||||
const columns = ref([])
|
const columns = ref([])
|
||||||
const cards = ref([])
|
const cards = ref([])
|
||||||
|
const users = ref([])
|
||||||
|
|
||||||
// Загрузка всех данных из API параллельно
|
// Загрузка всех данных из API параллельно
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const [departmentsData, labelsData, columnsData, cardsData] = await Promise.all([
|
const [departmentsData, labelsData, columnsData, cardsData, usersData] = await Promise.all([
|
||||||
departmentsApi.getAll(),
|
departmentsApi.getAll(),
|
||||||
labelsApi.getAll(),
|
labelsApi.getAll(),
|
||||||
columnsApi.getAll(),
|
columnsApi.getAll(),
|
||||||
cardsApi.getAll()
|
cardsApi.getAll(),
|
||||||
|
usersApi.getAll()
|
||||||
])
|
])
|
||||||
|
|
||||||
if (departmentsData.success) departments.value = departmentsData.data
|
if (departmentsData.success) departments.value = departmentsData.data
|
||||||
if (labelsData.success) labels.value = labelsData.data
|
if (labelsData.success) labels.value = labelsData.data
|
||||||
if (columnsData.success) columns.value = columnsData.data
|
if (columnsData.success) columns.value = columnsData.data
|
||||||
if (cardsData.success) cards.value = cardsData.data
|
if (cardsData.success) cards.value = cardsData.data
|
||||||
|
if (usersData.success) users.value = usersData.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки данных:', error)
|
console.error('Ошибка загрузки данных:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
Target Server Version : 90200 (9.2.0)
|
Target Server Version : 90200 (9.2.0)
|
||||||
File Encoding : 65001
|
File Encoding : 65001
|
||||||
|
|
||||||
Date: 13/01/2026 08:18:18
|
Date: 13/01/2026 09:07:01
|
||||||
*/
|
*/
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
@@ -45,7 +45,7 @@ CREATE TABLE `accounts_session` (
|
|||||||
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
`user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
`user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`) USING BTREE
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
) ENGINE = InnoDB AUTO_INCREMENT = 28 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for cards_task
|
-- Table structure for cards_task
|
||||||
@@ -61,6 +61,7 @@ CREATE TABLE `cards_task` (
|
|||||||
`archive` tinyint NULL DEFAULT NULL,
|
`archive` tinyint NULL DEFAULT NULL,
|
||||||
`date` datetime NULL DEFAULT NULL,
|
`date` datetime NULL DEFAULT NULL,
|
||||||
`date_create` datetime NULL DEFAULT NULL,
|
`date_create` datetime NULL DEFAULT NULL,
|
||||||
|
`date_closed` datetime NULL DEFAULT NULL,
|
||||||
`file_img` json NULL,
|
`file_img` json NULL,
|
||||||
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
`descript` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
`descript` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
|||||||
Reference in New Issue
Block a user