Bicycle/App/media/js/accounts.js
2026-08-15 19:40:29 +03:00

317 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

$(function () {
var accountModalEl = document.getElementById('account-modal');
var creditModalEl = document.getElementById('credit-modal');
var $form = $('#account-form');
var $error = $('#account-form-error');
var $rate = $('#account-rate-field');
var $goalFields = $('#account-goal-fields');
var $colorHidden = $('#acc-color-hidden');
var hasColorOverride = false;
if (!accountModalEl) {
return;
}
var accountModal = new bootstrap.Modal(accountModalEl);
var creditModal = creditModalEl ? new bootstrap.Modal(creditModalEl) : null;
var archiveEl = document.getElementById('archive-modal');
var archiveModal = archiveEl ? new bootstrap.Modal(archiveEl) : null;
// Плитка «Кредит» в модалке счёта переключает на модалку кредита (см. design-хендофф) —
// Bootstrap не любит два открытых .modal одновременно (общий backdrop/scroll-lock), поэтому
// ждём полного закрытия account-modal (событие hidden.bs.modal) и только потом открываем credit-modal.
var openCreditAfterHide = false;
$(accountModalEl).on('hidden.bs.modal', function () {
$('#acc-color-picker').hide();
if (openCreditAfterHide) {
openCreditAfterHide = false;
creditModal.show();
}
});
function applyPreview(color, icon) {
$('#acc-color-swatch').css('background', color);
$('#acc-icon-preview').css('background', 'color-mix(in srgb,' + color + ' 20%, transparent)').css('color', color);
$('#acc-icon-preview-use').attr('href', '#' + icon);
}
function setType(type, isEdit) {
var $tile = $('#acc-type-tiles .pm-tile[data-type="' + type + '"]');
$('#acc-type-tiles .pm-tile').removeClass('selected');
$tile.addClass('selected');
$('#acc-type-hidden').val(type);
applyPreview(hasColorOverride ? $colorHidden.val() : $tile.data('color'), $tile.data('icon'));
var isSavings = type === 'savings';
$rate.toggle(isSavings && !isEdit);
$rate.find('[name=rate_percent]').prop('required', isSavings && !isEdit);
$goalFields.toggle(isSavings);
if (!isEdit) {
var includeVal = isSavings ? '0' : '1';
$form.find('[name=include_in_total][value="' + includeVal + '"]').prop('checked', true);
}
}
function resetForm() {
$form[0].reset();
$form.find('[name=id]').val('');
$error.hide().text('');
hasColorOverride = false;
$colorHidden.val('');
setType('bank', false);
$form.find('[name=include_in_total][value="1"]').prop('checked', true);
$('#acc-type-tiles').removeClass('locked');
$('#acc-type-lock-hint').hide();
}
$(document).on('click', '.js-account-add', function () {
resetForm();
$form.data('mode', 'create').attr('action', '/accounts/create');
$('#account-modal-title').text('Новый счёт');
accountModal.show();
});
$(document).on('click', '.js-account-archive-open', function () {
if (archiveModal) {
archiveModal.show();
}
});
$(document).on('click', '.js-account-restore', function () {
var $row = $(this).closest('.archive-row');
var csrf = $form.find('[name=csrf_token]').val();
$.post('/accounts/restore/' + $row.data('id'), {csrf_token: csrf})
.done(function () { window.location.reload(); })
.fail(function () { window.alert('Не удалось восстановить счёт'); });
});
// Общая логика заполнения модалки счёта на редактирование — используется и для room-card
// (обычный счёт), и для goal-card (та же карточка счёта, только с иным набором data-*
// не нужна — goal-card несёт ровно те же атрибуты, см. index.html), $accountId берётся
// отдельно, т.к. имя атрибута с id разное (data-id / data-account-id).
function populateEditForm($card, accountId) {
resetForm();
$form.data('mode', 'edit').attr('action', '/accounts/edit/' + accountId);
$form.find('[name=title]').val($card.data('title'));
$form.find('[name=summa]').val($card.data('summa'));
$form.find('[name=currency]').val($card.data('currency'));
$form.find('[name=description]').val($card.data('description'));
$form.find('[name=goal_target_summa]').val($card.data('goal-target'));
$form.find('[name=goal_deadline]').val($card.data('goal-deadline'));
var color = $card.data('color');
if (color) {
hasColorOverride = true;
$colorHidden.val(color);
}
var includeVal = $card.data('include') ? '1' : '0';
$form.find('[name=include_in_total][value="' + includeVal + '"]').prop('checked', true);
setType($card.data('type'), true);
$('#acc-type-tiles').addClass('locked');
$('#acc-type-lock-hint').show();
$('#account-modal-title').text('Редактировать счёт');
accountModal.show();
}
$(document).on('click', '.js-account-edit', function (e) {
e.preventDefault();
var $card = $(this).closest('.room-card');
populateEditForm($card, $card.data('id'));
});
$(document).on('click', '.js-account-delete', function (e) {
e.preventDefault();
var $card = $(this).closest('.room-card');
var title = $card.data('title');
var csrf = $form.find('[name=csrf_token]').val();
if (!window.confirm('Архивировать счёт «' + title + '»?')) {
return;
}
$.post('/accounts/delete/' + $card.data('id'), {csrf_token: csrf})
.done(function () { window.location.reload(); })
.fail(function () { window.alert('Не удалось архивировать счёт'); });
});
// «Редактировать» на карточке копилки — просто .js-account-edit (goal-card несёт класс
// .room-card и тот же набор data-*, см. index.html), отдельного обработчика не нужно.
// «Удалить» на карточке копилки с целью — убирает только цель, счёт остаётся (для копилки
// без цели там же используется обычный .js-account-delete, архивирующий сам счёт).
$(document).on('click', '.js-goal-delete', function (e) {
e.preventDefault();
var $card = $(this).closest('.goal-card');
var csrf = $form.find('[name=csrf_token]').val();
if (!window.confirm('Удалить цель? Сам счёт останется.')) {
return;
}
$.post('/goals/delete/' + $card.data('goal-id'), {csrf_token: csrf})
.done(function () { window.location.reload(); })
.fail(function () { window.alert('Не удалось удалить цель'); });
});
$(document).on('click', '#acc-type-tiles .pm-tile', function () {
if ($form.data('mode') === 'edit') {
return;
}
var type = $(this).data('type');
if (type === 'credit') {
openCreditAfterHide = true;
accountModal.hide();
return;
}
setType(type, $form.data('mode') === 'edit');
});
$(document).on('click', '#acc-color-swatch', function (e) {
e.stopPropagation();
$('#acc-color-picker').toggle();
});
$(document).on('click', '.acc-color-swatch-opt', function () {
var color = $(this).data('color');
hasColorOverride = true;
$colorHidden.val(color);
applyPreview(color, $('#acc-type-tiles .pm-tile.selected').data('icon'));
$('#acc-color-picker').hide();
});
$(document).on('click', function (e) {
if (!$(e.target).closest('#acc-color-swatch, #acc-color-picker').length) {
$('#acc-color-picker').hide();
}
});
// ---------- модалка «Новый кредит» (вёрстка-заглушка, см. App/design_handoff_personal_finance) ----------
$(document).on('click', '#credit-mode-tiles .pm-tile', function () {
$('#credit-mode-tiles .pm-tile').removeClass('selected');
$(this).addClass('selected');
var isExisting = $(this).data('mode') === 'existing';
$('#credit-existing-hint').toggle(isExisting);
$('#credit-existing-fields').css('display', isExisting ? 'grid' : 'none');
});
$(document).on('click', '#credit-type-tiles .pm-tile', function () {
$('#credit-type-tiles .pm-tile').removeClass('selected');
$(this).addClass('selected');
var isCard = $(this).data('credit-type') === 'card';
$('#credit-card-fields').toggle(isCard);
$('#credit-noncard-fields').toggle(!isCard);
$('#credit-min-hint').html(
'Минимум нужно: <strong style="color:var(--ink)">' +
(isCard ? 'название, лимит, ставка' : 'название, сумма, ставка и срок или платёж') +
'</strong>'
);
});
$(document).on('submit', '#account-form', function (e) {
e.preventDefault();
$error.hide().text('');
$.post($form.attr('action'), $form.serialize())
.done(function () { window.location.reload(); })
.fail(function (xhr) {
var errors = xhr.responseJSON && xhr.responseJSON.errors;
var message = errors ? Object.values(errors).join(' ') : 'Не удалось сохранить счёт';
$error.text(message).show();
});
});
$(document).on('click', '.room-menu-btn', function () {
$(this).closest('.room-menu').toggleClass('open');
});
$(document).on('click', function (e) {
if (!$(e.target).closest('.room-menu').length) {
$('.room-menu.open').removeClass('open');
}
});
// ---------- ручная сортировка счетов (drag & drop) ----------
// Одна и та же кнопка включает режим (карточки становятся draggable, ⋮-меню временно
// отключено — см. CSS .reorder-mode .room-menu) и выключает его же, сохраняя итоговый
// порядок одним запросом на /accounts/reorder.
var $accountsGrid = $('.grid-cards').first();
var reorderMode = false;
var $dragging = null;
function saveAccountOrder() {
var ids = $accountsGrid.find('.room-card').map(function () { return $(this).data('id'); }).get();
var csrf = $form.find('[name=csrf_token]').val();
$.post('/accounts/reorder', {csrf_token: csrf, ids: ids});
}
$(document).on('click', '.js-account-reorder-toggle', function () {
reorderMode = !reorderMode;
$accountsGrid.toggleClass('reorder-mode', reorderMode);
$accountsGrid.find('.room-card').attr('draggable', reorderMode);
$(this).toggleClass('active', reorderMode).attr(
'title', reorderMode ? 'Готово' : 'Изменить порядок счетов'
);
if (!reorderMode) {
saveAccountOrder();
}
});
$(document).on('dragstart', '.room-card', function (e) {
if (!reorderMode) {
return;
}
$dragging = $(this).addClass('dragging');
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String($(this).data('id')));
});
$(document).on('dragover', '.room-card', function (e) {
if (!reorderMode || !$dragging) {
return;
}
e.preventDefault();
var $target = $(this);
if ($target.is($dragging)) {
return;
}
if ($dragging.index() < $target.index()) {
$target.after($dragging);
} else {
$target.before($dragging);
}
});
$(document).on('dragend', '.room-card', function () {
$(this).removeClass('dragging');
$dragging = null;
});
});