feat: allow sorting of categories in settings

feat: add filters to statistics page
feat: allow renaming / translation of payment methods
feat: allow deletion of the default payment methods
This commit is contained in:
Miguel Ribeiro 2024-03-05 18:17:15 +01:00 committed by GitHub
parent d7e050b868
commit 83234ab8cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 755 additions and 99 deletions

View File

@ -0,0 +1,34 @@
<?php
require_once '../../includes/connect_endpoint.php';
session_start();
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$categories = $_POST['categoryIds'];
$order = 2;
foreach ($categories as $categoryId) {
$sql = "UPDATE categories SET `order` = :order WHERE id = :categoryId";
$stmt = $db->prepare($sql);
$stmt->bindParam(':order', $order, SQLITE3_INTEGER);
$stmt->bindParam(':categoryId', $categoryId, SQLITE3_INTEGER);
$result = $stmt->execute();
$order++;
}
$response = [
"success" => true,
"message" => translate("sort_order_saved", $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"errorMessage" => translate("session_expired", $i18n)
];
echo json_encode($response);
die();
}
?>

View File

@ -39,7 +39,7 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
<?= $payment['name'] ?>
</span>
<?php
if ($payment['id'] > 31 && !$inUse) {
if (!$inUse) {
?>
<div class="delete-payment-method" title="<?= translate('delete', $i18n) ?>" data-paymentid="<?= $payment['id'] ?>" onclick="deletePaymentMethod(<?= $payment['id'] ?>)">
x

View File

@ -0,0 +1,41 @@
<?php
require_once '../../includes/connect_endpoint.php';
session_start();
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n)
]));
}
if (!isset($_POST['paymentId']) || !isset($_POST['name']) || $_POST['paymentId'] === '' || $_POST['name'] === '') {
die(json_encode([
"success" => false,
"message" => translate('fields_missing', $i18n)
]));
}
$paymentId = $_POST['paymentId'];
$name = $_POST['name'];
$sql = "UPDATE payment_methods SET name = :name WHERE id = :paymentId";
$stmt = $db->prepare($sql);
$stmt->bindParam(':name', $name, SQLITE3_TEXT);
$stmt->bindParam(':paymentId', $paymentId, SQLITE3_INTEGER);
$result = $stmt->execute();
if ($result) {
echo json_encode([
"success" => true,
"message" => translate('payment_renamed', $i18n)
]);
} else {
echo json_encode([
"success" => false,
"message" => translate('payment_not_renamed', $i18n)
]);
}
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

View File

@ -26,7 +26,7 @@
}
$categories = array();
$query = "SELECT * FROM categories";
$query = "SELECT * FROM categories ORDER BY `order` ASC";
$result = $db->query($query);
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$categoryId = $row['id'];

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "Experimentelle Einstellungen funktionieren möglicherweise nicht perfekt.",
"payment_methods" => "Zahlungsmethoden",
"payment_methods_info" => "Zahlungsmethode zum (de-)aktivieren anklicken.",
"rename_payment_methods_info" => "Klicken Sie auf den Namen einer Zahlungsmethode, um sie umzubenennen",
"cant_delete_payment_method_in_use" => "Genutzte Zahlungsmethoden können nicht deaktiviert werden",
"add_custom_payment" => "Eigene Zahlungsmethode hinzufügen",
"payment_method_name" => "Name der Zahlungsmethode",
"payment_method_added_successfuly" => "Zahlungsmethode erfolgreich hinzugefügt",
"disable" => "Deaktivieren",
"enable" => "Aktivieren",
"rename_payment_method" => "Zahlungsmethode umbenennen",
"payment_renamed" => "Zahlungsmethode umbenannt",
"payment_not_renamed" => "Zahlungsmethode konnte nicht umbenannt werden",
"test" => "Test",
"add" => "Hinzufügen",
"save" => "Speichern",
"export_subscriptions" => "Abonnements exportieren",
"export_to_json" => "Nach JSON exportieren",
// Filters menu
"filter" => "Filter",
"clear" => "Leeren",
// Toast
"success" => "Erfolgreich",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "Kategorie konnte nicht gelöscht werden",
"category_saved" => "Kategorie gespeichert",
"category_removed" => "Kategorie gelöscht",
"sort_order_saved" => "Sortierung gespeichert",
// Currency
"currency_saved" => "wurde gespeichert.",
"error_adding_currency" => "Fehler beim hinzufügen der Währung.",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "Οι πειραματικές ρυθμίσεις πιθανότατα δεν θα λειτουργούν τέλεια.",
"payment_methods" => "Τρόποι πληρωμής",
"payment_methods_info" => "Κάνε κλικ σε μια μέθοδο πληρωμής για να την απενεργοποιήσεις/ενεργοποιήσεις.",
"rename_payment_methods_info" => "Κάντε κλικ στο όνομα μιας μεθόδου πληρωμής για να τη μετονομάσετε.",
"cant_delete_payment_method_in_use" => "Δεν είναι εφικτό να απενεργοποιηθεί η χρησιμοποιούμενη μέθοδο πληρωμής",
"add_custom_payment" => "Προσθήκη προσαρμοσμένης μεθόδου πληρωμής",
"payment_method_name" => "Όνομα μεθόδου πληρωμής",
"payment_method_added_successfuly" => "Η μέθοδος πληρωμής προστέθηκε με επιτυχία",
"disable" => "Ανενεργό",
"enable" => "Ενεργό",
"rename_payment_method" => "Μετονομασία μεθόδου πληρωμής",
"payment_renamed" => "Η μέθοδος πληρωμής μετονομάστηκε",
"payment_not_renamed" => "Η μέθοδος πληρωμής δεν μετονομάστηκε",
"test" => "Δοκιμή",
"add" => "Προσθήκη",
"save" => "Αποθήκευση",
"export_subscriptions" => "Εξαγωγή συνδρομών",
"export_to_json" => "Εξαγωγή σε JSON",
// Filters menu
"filter" => "Φίλτρο",
"clear" => "Καθαρισμός",
// Toast
"success" => "Επιτυχία",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "Απέτυχε η διαγραφή κατηγορίας",
"category_saved" => "Αποθήκευση κατηγορίας",
"category_removed" => "Διαγραφή κατηγορίας",
"sort_order_saved" => "Η ταξινόμηση αποθηκεύτηκε",
// Currency
"currency_saved" => "αποθηκεύτηκε.",
"error_adding_currency" => "Error adding currency entry.",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "Experimental settings will probably not work perfectly.",
"payment_methods" => "Payment Methods",
"payment_methods_info" => "Click a payment method to disable / enable it.",
"rename_payment_methods_info" => "Click the name on a payment method to rename it.",
"cant_delete_payment_method_in_use" => "Can't disable used payment method",
"add_custom_payment" => "Add Custom Payment Method",
"payment_method_name" => "Payment Method Name",
"payment_method_added_successfuly" => "Payment method added successfully",
"disable" => "Disable",
"enable" => "Enable",
"rename_payment_method" => "Rename Payment Method",
"payment_renamed" => "Payment method renamed",
"payment_not_renamed" => "Payment method not renamed",
"test" => "Test",
"add" => "Add",
"save" => "Save",
"export_subscriptions" => "Export Subscriptions",
"export_to_json" => "Export to JSON",
// Filters menu
"filter" => "Filter",
"clear" => "Clear",
// Toast
"success" => "Success",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "Failed to remove category",
"category_saved" => "Category saved",
"category_removed" => "Category removed",
"sort_order_saved" => "Sort order saved",
// Currency
"currency_saved" => "was saved.",
"error_adding_currency" => "Error adding currency entry.",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "Las configuraciones experimentales probablemente no funcionarán perfectamente.",
"payment_methods" => "Métodos de Pago",
"payment_methods_info" => "Haz clic en un método de pago para deshabilitarlo/habilitarlo.",
"rename_payment_methods_info" => "Haz clic en el nombre de un método de pago para cambiarle el nombre.",
"cant_delete_payment_method_in_use" => "No se puede desactivar el método de pago utilizado",
"add_custom_payment" => "Añadir método de pago personalizado",
"payment_method_name" => "Nombre del método de pago",
"payment_method_added_successfuly" => "Método de pago añadido con éxito",
"disable" => "Desactivar",
"enable" => "Activar",
"rename_payment_method" => "Renombrar método de pago",
"payment_renamed" => "Método de pago renombrado",
"payment_not_renamed" => "Error al renombrar el método de pago",
"test" => "Probar",
"add" => "Agregar",
"save" => "Guardar",
"export_subscriptions" => "Exportar suscripciones",
"export_to_json" => "Exportar a JSON",
// Filters menu
"filter" => "Filtrar",
"clear" => "Limpiar",
// Toast
"success" => "Éxito",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "Error al eliminar la categoría",
"category_saved" => "Categoría guardada",
"category_removed" => "Categoría eliminada",
"sort_order_saved" => "Orden de clasificación guardado",
// Currency
"currency_saved" => "fue guardada.",
"error_adding_currency" => "Error al añadir la entrada de la moneda.",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "Les paramètres expérimentaux ne fonctionneront probablement pas parfaitement.",
"payment_methods" => "Méthodes de paiement",
"payment_methods_info" => "Cliquez sur une méthode de paiement pour la désactiver / l'activer.",
"rename_payment_methods_info" => "Cliquez sur le nom d'un mode de paiement pour le renommer.",
"cant_delete_payment_method_in_use" => "Impossible de désactiver la méthode de paiement utilisée",
"add_custom_payment" => "Ajouter un paiement personnalisé",
"payment_method_name" => "Nom de la méthode de paiement",
"payment_method_added_successfuly" => "Méthode de paiement ajoutée avec succès",
"disable" => "Désactiver",
"enable" => "Activer",
"rename_payment_method" => "Renommer la méthode de paiement",
"payment_renamed" => "Méthode de paiement renommée",
"payment_not_renamed" => "La méthode de paiement n'a pas été renommée",
"test" => "Test",
"add" => "Ajouter",
"save" => "Enregistrer",
"export_subscriptions" => "Exporter les abonnements",
"export_to_json" => "Exporter en JSON",
// Menu des filtes
"filter" => "Filtre",
"clear" => "Effacer",
// Toast
"success" => "Succès",
// Réponses de l'API
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "Échec de la suppression de la catégorie",
"category_saved" => "Catégorie enregistrée",
"category_removed" => "Catégorie supprimée",
"sort_order_saved" => "L'ordre de tri a été enregistré",
// Devise
"currency_saved" => "a été enregistrée.",
"error_adding_currency" => "Erreur lors de l'ajout de l'entrée de devise.",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "実験的な設定は、おそらく完全には機能しません。",
"payment_methods" => "支払い方法",
"payment_methods_info" => "支払い方法をクリックして無効/有効を切り替えます。",
"rename_payment_methods_info" => "支払い方法の名前をクリックして、名前を変更します。",
"cant_delete_payment_method_in_use" => "支払い方法が使用中のため無効にできません。",
"add_custom_payment" => "カスタム支払い方法を追加",
"payment_method_name" => "支払い方法名",
"payment_method_added_successfuly" => "支払い方法が追加されました",
"disable" => "無効",
"enable" => "有効",
"rename_payment_method" => "支払い方法の名前を変更",
"payment_renamed" => "支払い方法が変更されました",
"payment_not_renamed" => "支払い方法が変更されませんでした",
"test" => "テスト",
"add" => "追加",
"save" => "保存",
"export_subscriptions" => "購読をエクスポート",
"export_to_json" => "JSONにエクスポート",
// Filters menu
"filter" => "フィルタ",
"clear" => "クリア",
// Toast
"success" => "成功",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "カテゴリの削除に失敗",
"category_saved" => "カテゴリの保存",
"category_removed" => "カテゴリの削除",
"sort_order_saved" => "並べ替え順が保存されました",
// Currency
"currency_saved" => "通貨を保存",
"error_adding_currency" => "通貨エントリの追加エラー.",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "Definições experimentais provavelmente não funcionarão correctamente.",
"payment_methods" => "Métodos de Pagamento",
"payment_methods_info" => "Clique num método de pagamento para o activar / desactivar.",
"rename_payment_methods_info" => "Clique no nome do método de pagamento para o renomear.",
"cant_delete_payment_method_in_use" => "Não pode desactivar metodo de pagamento em uso",
"add_custom_payment" => "Adicionar método de pagamento personalizado",
"payment_method_name" => "Nome do método de pagamento",
"payment_method_added_successfuly" => "Método de pagamento adicionado com sucesso",
"disable" => "Desactivar",
"enable" => "Activar",
"rename_payment_method" => "Renomear método de pagamento",
"payment_renamed" => "Método de pagamento renomeado",
"payment_not_renamed" => "Método de pagamento não renomeado",
"test" => "Testar",
"add" => "Adicionar",
"save" => "Guardar",
"export_subscriptions" => "Exportar Subscrições",
"export_to_json" => "Exportar para JSON",
// Filters menu
"filter" => "Filtro",
"clear" => "Limpar",
// Toast
"success" => "Sucesso",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "Erro ao remover categoria",
"category_saved" => "Categoria guardada",
"category_removed" => "Categoria removida",
"sort_order_saved" => "Ordenação guardada",
// Currency
"currency_saved" => "guardada.",
"error_adding_currency" => "Erro ao adicionar moeda.",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "Deneysel ayarlar muhtemelen mükemmel çalışmayacak.",
"payment_methods" => "Ödeme Yöntemleri",
"payment_methods_info" => "Bir ödeme yöntemini devre dışı bırakmak / etkinleştirmek için tıklayın.",
"rename_payment_methods_info" => "Yeniden adlandırmak için bir ödeme yönteminin adına tıklayın.",
"cant_delete_payment_method_in_use" => "Kullanımda olan ödeme yöntemini devre dışı bırakamazsınız",
"add_custom_payment" => "Özel ödeme yöntemi ekle",
"payment_method_name" => "Ödeme Yöntemi Adı",
"payment_method_added_successfuly" => "Ödeme yöntemi başarıyla eklendi",
"disable" => "Devre Dışı Bırak",
"enable" => "Etkinleştir",
"rename_payment_method" => "Ödeme yöntemi adını değiştir",
"payment_renamed" => "Ödeme yöntemi adı değiştirildi",
"payment_not_renamed" => "Ödeme yöntemi adı değiştirilemedi",
"test" => "Test Et",
"add" => "Ekle",
"save" => "Kaydet",
"export_subscriptions" => "Abonelikleri Dışa Aktar",
"export_to_json" => "JSON'a dışa aktar",
// Filters menu
"filter" => "Filtre",
"clear" => "Temizle",
// Toast
"success" => "Başarılı",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "Kategori kaldırılamadı",
"category_saved" => "Kategori kaydedildi",
"category_removed" => "Kategori kaldırıldı",
"sort_order_saved" => "Sıralama düzeni kaydedildi",
// Currency
"currency_saved" => "kaydedildi.",
"error_adding_currency" => "Para birimi girişi eklenirken hata oluştu.",

View File

@ -143,17 +143,25 @@ $i18n = [
"experimental_info" => "实验性设置,可能存在问题。",
"payment_methods" => "支付方式",
"payment_methods_info" => "点击支付方式以禁用/启用。",
"rename_payment_methods_info" => "点击付款方式名称,重新命名该付款方式。"
"cant_delete_payment_method_in_use" => "不能禁用正在使用的支付方式",
"add_custom_payment" => "添加自定义支付方式",
"payment_method_name" => "支付方式名称",
"payment_method_added_successfuly" => "支付方式已成功添加",
"disable" => "禁用",
"enable" => "启用",
"rename_payment_method" => "重命名支付方式",
"payment_renamed" => "支付方式已重命名",
"payment_not_renamed" => "支付方式未重命名",
"test" => "测试",
"add" => "添加",
"save" => "保存",
"export_subscriptions" => "导出订阅",
"export_to_json" => "导出为 JSON",
// Filters menu
"filter" => "筛选",
"clear" => "清除",
// Toast
"success" => "成功",
@ -172,6 +180,7 @@ $i18n = [
"failed_remove_category" => "移除分类失败",
"category_saved" => "分类已保存",
"category_removed" => "分类已移除",
"sort_order_saved" => "排序顺序已保存",
// Currency
"currency_saved" => "货币已保存。",

View File

@ -136,17 +136,24 @@ $i18n = [
"experimental_info" => "實驗性設定,可能存在問題。",
"payment_methods" => "付款方式",
"payment_methods_info" => "點選付款方式以停用/啟用。",
"rename_payment_methods_info" => "點選付款方式的名稱可對其進行重新命名。",
"cant_delete_payment_method_in_use" => "無法停用正在使用的付款方式",
"add_custom_payment" => "新增自訂付款方式",
"payment_method_name" => "付款方式名稱",
"payment_method_added_successfuly" => "付款方式已成功新增",
"disable" => "停用",
"enable" => "啟用",
"rename_payment_method" => "更改付款方式名稱",
"payment_renamed" => "付款方式名稱已更改",
"payment_not_renamed" => "付款方式名稱未更改",
"test" => "測試",
"add" => "新增",
"save" => "儲存",
"export_subscriptions" => "匯出訂閱",
"export_to_json" => "匯出為 JSON 檔案",
// Filters menu
"filter" => "篩選",
"clear" => "清除",
// Toast
"success" => "成功",
// Endpoint responses
@ -162,6 +169,7 @@ $i18n = [
"failed_remove_category" => "移除分類失敗",
"category_saved" => "分類已儲存",
"category_removed" => "分類已移除",
"sort_order_saved" => "排序順序已儲存",
// Currency
"currency_saved" => "已儲存。",
"error_adding_currency" => "新增貨幣時發生錯誤。",

View File

@ -1,3 +1,3 @@
<?php
$version = "v1.11.3";
$version = "v1.12.0";
?>

14
migrations/000010.php Normal file
View File

@ -0,0 +1,14 @@
<?php
// This migration adds a "order" column to the categories table so that they can be sorted and initializes all values to their id.
/** @noinspection PhpUndefinedVariableInspection */
$columnQuery = $db->query("SELECT * FROM pragma_table_info('categories') WHERE name='order'");
$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;
if ($columnRequired) {
$db->exec('ALTER TABLE categories ADD COLUMN `order` INTEGER DEFAULT 0');
$db->exec('UPDATE categories SET `order` = id');
}
?>

2
scripts/libs/sortable.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -165,9 +165,12 @@ function addCategoryButton(categoryId) {
if(responseData.success) {
const newCategoryId = responseData.categoryId;;
let container = document.getElementById("categories");
let div = document.createElement("div");
div.className = "form-group-inline";
div.dataset.categoryid = newCategoryId;
let row = document.createElement("li");
row.className = "form-group-inline";
row.dataset.categoryid = newCategoryId;
let dragIcon = document.createElement("div");
dragIcon.className = "drag-icon";
let input = document.createElement("input");
input.type = "text";
@ -201,11 +204,12 @@ function addCategoryButton(categoryId) {
deleteLink.appendChild(deleteImage);
div.appendChild(input);
div.appendChild(editLink);
div.appendChild(deleteLink);
row.appendChild(dragIcon);
row.appendChild(input);
row.appendChild(editLink);
row.appendChild(deleteLink);
container.appendChild(div);
container.appendChild(row);
} else {
showErrorMessage(responseData.errorMessage);
}
@ -246,6 +250,7 @@ function removeCategory(categoryId) {
function editCategory(categoryId) {
var saveButton = document.querySelector(`div[data-categoryid="${categoryId}"] button[name="save"]`);
var inputElement = document.querySelector(`div[data-categoryid="${categoryId}"] input[name="category"]`);
console.log(saveButton);
saveButton.classList.add("disabled");
saveButton.disabled = true;
if (inputElement) {
@ -424,7 +429,7 @@ function togglePayment(paymentId) {
const element = document.querySelector(`div[data-paymentid="${paymentId}"]`);
if (element.dataset.inUse === 'yes') {
return showErrorMessage(translate(cant_disable_payment_in_use));
return showErrorMessage(translate('cant_disable_payment_in_use'));
}
const newEnabledState = element.dataset.enabled === '1' ? '0' : '1';
@ -449,6 +454,69 @@ function togglePayment(paymentId) {
});
}
document.body.addEventListener('click', function(e) {
let targetElement = e.target;
do {
if (targetElement.classList && targetElement.classList.contains('payments-payment')) {
let targetChild = e.target;
do {
if (targetChild.classList && targetChild.classList.contains('payment-name')) {
return;
}
targetChild = targetChild.parentNode;
} while (targetChild && targetChild !== targetElement);
const paymentId = targetElement.dataset.paymentid;
togglePayment(paymentId);
return;
}
targetElement = targetElement.parentNode;
} while (targetElement);
});
document.body.addEventListener('blur', function(e) {
let targetElement = e.target;
if (targetElement.classList && targetElement.classList.contains('payment-name')) {
const paymentId = targetElement.closest('.payments-payment').dataset.paymentid;
const newName = targetElement.textContent;
renamePayment(paymentId, newName);
}
}, true);
function renamePayment(paymentId, newName) {
const name = newName.trim();
const formData = new FormData();
formData.append('paymentId', paymentId);
formData.append('name', name);
fetch('endpoints/payments/rename.php', {
method: 'POST',
body: formData
}).then(response => {
if (!response.ok) {
throw new Error(translate('network_response_error'));
}
return response.json();
}).then(data => {
if (data.success) {
showSuccessMessage(`${newName} ${data.message}`);
} else {
showErrorMessage(data.message);
}
}).catch(error => {
showErrorMessage(translate('unknown_error'));
});
}
document.body.addEventListener('keypress', function(e) {
let targetElement = e.target;
if (targetElement.classList && targetElement.classList.contains('payment-name')) {
if (e.key === 'Enter') {
e.preventDefault();
targetElement.blur();
}
}
});
function handleFileSelect(event) {
const fileInput = event.target;
const iconPreview = document.querySelector('.icon-preview');
@ -843,3 +911,41 @@ function setRemoveBackground() {
function exportToJson() {
window.location.href = "endpoints/subscriptions/export.php";
}
function saveCategorySorting() {
const categories = document.getElementById('categories');
const categoryIds = Array.from(categories.children).map(category => category.dataset.categoryid);
const formData = new FormData();
categoryIds.forEach(categoryId => {
formData.append('categoryIds[]', categoryId);
});
fetch('endpoints/categories/sort.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message);
} else {
showErrorMessage(data.errorMessage);
}
})
.catch(error => {
showErrorMessage(translate('unknown_error'));
});
}
var el = document.getElementById('categories');
var sortable = Sortable.create(el, {
handle: '.drag-icon',
ghostClass: 'sortable-ghost',
delay: 500,
delayOnTouchOnly: true,
touchStartThreshold: 5,
onEnd: function (evt) {
saveCategorySorting();
},
});

View File

@ -25,3 +25,91 @@ function loadGraph(container, dataPoints, currency, run) {
});
}
}
function closeSubMenus() {
var subMenus = document.querySelectorAll('.filtermenu-submenu-content');
subMenus.forEach(subMenu => {
subMenu.classList.remove('is-open');
});
}
document.addEventListener("DOMContentLoaded", function() {
var filtermenu = document.querySelector('#filtermenu-button');
filtermenu.addEventListener('click', function() {
this.parentElement.querySelector('.filtermenu-content').classList.toggle('is-open');
closeSubMenus();
});
document.addEventListener('click', function(e) {
var filtermenuContent = document.querySelector('.filtermenu-content');
if (filtermenuContent.classList.contains('is-open')) {
var subMenus = document.querySelectorAll('.filtermenu-submenu');
var clickedInsideSubmenu = Array.from(subMenus).some(subMenu => subMenu.contains(e.target) || subMenu === e.target);
if (!filtermenu.contains(e.target) && !clickedInsideSubmenu) {
closeSubMenus();
filtermenuContent.classList.remove('is-open');
}
}
});
});
function toggleSubMenu(subMenu) {
var subMenu = document.getElementById("filter-" + subMenu);
if (subMenu.classList.contains("is-open")) {
closeSubMenus();
} else {
closeSubMenus();
subMenu.classList.add("is-open");
}
}
document.querySelectorAll('.filter-item').forEach(function(item) {
item.addEventListener('click', function(e) {
if (this.hasAttribute('data-categoryid')) {
const categoryId = this.getAttribute('data-categoryid');
const urlParams = new URLSearchParams(window.location.search);
let newUrl = 'stats.php?';
if (urlParams.get('category') === categoryId) {
urlParams.delete('category');
} else {
urlParams.set('category', categoryId);
}
newUrl += urlParams.toString();
window.location.href = newUrl;
} else if (this.hasAttribute('data-memberid')) {
const memberId = this.getAttribute('data-memberid');
const urlParams = new URLSearchParams(window.location.search);
let newUrl = 'stats.php?';
if (urlParams.get('member') === memberId) {
urlParams.delete('member');
} else {
urlParams.set('member', memberId);
}
newUrl += urlParams.toString();
window.location.href = newUrl;
} else if (this.hasAttribute('data-paymentid')) {
const paymentId = this.getAttribute('data-paymentid');
const urlParams = new URLSearchParams(window.location.search);
let newUrl = 'stats.php?';
if (urlParams.get('payment') === paymentId) {
urlParams.delete('payment');
} else {
urlParams.set('payment', paymentId);
}
newUrl += urlParams.toString();
window.location.href = newUrl;
}
});
});
function clearFilters() {
window.location.href = 'stats.php';
}

View File

@ -33,6 +33,7 @@ self.addEventListener('install', function(event) {
'scripts/i18n/zh_tw.js',
'scripts/i18n/getlang.js',
'scripts/libs/chart.js',
'scripts/libs/sortable.min.js',
'images/icon/favicon.ico',
'images/wallossolid.png',
'images/wallossolidwhite.png',

View File

@ -2,6 +2,7 @@
require_once 'includes/header.php';
?>
<script src="scripts/libs/sortable.min.js"></script>
<style>
.logo-preview:after {
content: '<?= translate('upload_logo', $i18n) ?>';
@ -238,7 +239,7 @@
</section>
<?php
$sql = "SELECT * FROM categories";
$sql = "SELECT * FROM categories ORDER BY `order` ASC";
$result = $db->query($sql);
if ($result) {
@ -254,7 +255,7 @@
<h2><?= translate('categories', $i18n) ?></h2>
</header>
<div class="account-categories">
<div id="categories">
<div id="categories" class="sortable-list">
<?php
foreach ($categories as $category) {
if ($category['id'] != 1) {
@ -272,6 +273,7 @@
}
?>
<div class="form-group-inline" data-categoryid="<?= $category['id'] ?>">
<div class="drag-icon"></div>
<input type="text" name="category" value="<?= $category['name'] ?>" placeholder="Category">
<button class="image-button medium" onClick="editCategory(<?= $category['id'] ?>)" name="save">
<img src="images/siteicons/save.png" title="<?= translate('save_category', $i18n) ?>">
@ -497,14 +499,11 @@
data-enabled="<?= $payment['enabled']; ?>"
data-in-use="<?= $inUse ? 'yes' : 'no' ?>"
data-paymentid="<?= $payment['id'] ?>"
title="<?= $inUse ? translate('cant_delete_payment_method_in_use', $i18n) : ($payment['enabled'] ? translate('disable', $i18n) : translate('enable', $i18n)) ?>"
onClick="togglePayment(<?= $payment['id'] ?>)">
title="<?= $inUse ? translate('cant_delete_payment_method_in_use', $i18n) : ($payment['enabled'] ? translate('disable', $i18n) : translate('enable', $i18n)) ?>">
<img src="<?= $paymentIconFolder.$payment['icon'] ?>" alt="Logo" />
<span class="payment-name">
<?= $payment['name'] ?>
</span>
<span class="payment-name" contenteditable="true" title="<?= translate("rename_payment_method", $i18n) ?>"><?= $payment['name'] ?></span>
<?php
if ($payment['id'] > 31 && !$inUse) {
if (!$inUse) {
?>
<div class="delete-payment-method" title="<?= translate('delete', $i18n) ?>" data-paymentid="<?= $payment['id'] ?>">x</div>
<?php
@ -520,6 +519,10 @@
<i class="fa-solid fa-circle-info"></i>
<?= translate('payment_methods_info', $i18n) ?>
</p>
<p>
<i class="fa-solid fa-circle-info"></i>
<?= translate('rename_payment_methods_info', $i18n) ?>
</p>
</div>
<header>
<h2 class="second-header"><?= translate("add_custom_payment", $i18n) ?></h2>

281
stats.php
View File

@ -51,7 +51,7 @@ while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
// Get categories
$categories = array();
$query = "SELECT * FROM categories";
$query = "SELECT * FROM categories ORDER BY 'order' ASC";
$result = $db->query($query);
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$categoryId = $row['id'];
@ -61,7 +61,7 @@ while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
}
// Get payment methods
$categories = array();
$paymentMethodCount = array();
$query = "SELECT * FROM payment_methods WHERE enabled = 1";
$result = $db->query($query);
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
@ -89,8 +89,41 @@ $amountDueThisMonth = 0;
$totalCostPerMonth = 0;
$totalSavingsPerMonth = 0;
$statsSubtitleParts = [];
$query = "SELECT name, price, frequency, cycle, currency_id, next_payment, payer_user_id, category_id, payment_method_id, inactive FROM subscriptions";
$result = $db->query($query);
$conditions = [];
$params = [];
if (isset($_GET['member'])) {
$conditions[] = "payer_user_id = :member";
$params[':member'] = $_GET['member'];
$statsSubtitleParts[] = $members[$_GET['member']]['name'];
}
if (isset($_GET['category'])) {
$conditions[] = "category_id = :category";
$params[':category'] = $_GET['category'];
$statsSubtitleParts[] = $categories[$_GET['category']]['name'];
}
if (isset($_GET['payment'])) {
$conditions[] = "payment_method_id = :payment";
$params[':payment'] = $_GET['payment'];
$statsSubtitleParts[] = $paymentMethodCount[$_GET['payment']]['name'];
}
if (!empty($conditions)) {
$query .= " WHERE " . implode(' AND ', $conditions);
}
$stmt = $db->prepare($query);
$statsSubtitle = !empty($statsSubtitleParts) ? '(' . implode(', ', $statsSubtitleParts) . ')' : "";
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value, SQLITE3_INTEGER);
}
$result = $stmt->execute();
if ($result) {
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$subscriptions[] = $row;
@ -160,7 +193,97 @@ if ($result) {
$numberOfElements = 6;
?>
<section class="contain">
<h2><?= translate('general_statistics', $i18n) ?></h2>
<div class="split-header">
<h2>
<?= translate('general_statistics', $i18n) ?> <span class="header-subtitle"><?= $statsSubtitle ?></span>
</h2>
<div class="filtermenu">
<button class="button" id="filtermenu-button">
<i class="fa-solid fa-filter"></i>
<?= translate("filter", $i18n) ?>
</button>
<div class="filtermenu-content">
<?php
if (count($members) > 1) {
?>
<div class="filtermenu-submenu">
<div class="filter-title" onClick="toggleSubMenu('member')"><?= translate("member", $i18n) ?></div>
<div class="filtermenu-submenu-content" id="filter-member">
<?php
foreach ($members as $member) {
$selectedClass = '';
if (isset($_GET['member']) && $_GET['member'] == $member['id']) {
$selectedClass = 'selected';
}
?>
<div class="filter-item <?= $selectedClass ?>" data-memberid="<?= $member['id'] ?>"><?= $member['name'] ?></div>
<?php
}
?>
</div>
</div>
<?php
}
?>
<?php
if (count($categories) > 1) {
?>
<div class="filtermenu-submenu">
<div class="filter-title" onClick="toggleSubMenu('category')"><?= translate("category", $i18n) ?></div>
<div class="filtermenu-submenu-content" id="filter-category">
<?php
foreach ($categories as $category) {
$selectedClass = '';
if (isset($_GET['category']) && $_GET['category'] == $category['id']) {
$selectedClass = 'selected';
}
?>
<div class="filter-item <?= $selectedClass ?>" data-categoryid="<?= $category['id'] ?>"><?= $category['name'] ?></div>
<?php
}
?>
</div>
</div>
<?php
}
?>
<?php
if (count($paymentMethodCount) > 1) {
?>
<div class="filtermenu-submenu">
<div class="filter-title" onClick="toggleSubMenu('payment')"><?= translate("payment_method", $i18n) ?></div>
<div class="filtermenu-submenu-content" id="filter-payment">
<?php
foreach ($paymentMethodCount as $payment) {
$selectedClass = '';
if (isset($_GET['payment']) && $_GET['payment'] == $payment['id']) {
$selectedClass = 'selected';
}
?>
<div class="filter-item <?= $selectedClass ?>" data-paymentid="<?= $payment['id'] ?>"><?= $payment['name'] ?></div>
<?php
}
?>
</div>
</div>
<?php
}
?>
<?php
if (isset($_GET['member']) || isset($_GET['category']) || isset($_GET['payment'])) {
?>
<div class="filtermenu-submenu">
<div class="filter-title filter-clear" onClick="clearFilters()">
<i class="fa-solid fa-times-circle"></i> <?= translate("clear", $i18n) ?>
</div>
</div>
<?php
}
?>
</div>
</div>
</div>
</div>
<div class="statistics">
<div class="statistic">
<span><?= $activeSubscriptions ?></span>
@ -208,86 +331,92 @@ $numberOfElements = 6;
}
?>
</div>
<h2><?= translate('split_views', $i18n) ?></h2>
<div class="graphs">
<?php
$categoryDataPoints = [];
foreach ($categoryCost as $category) {
if ($category['cost'] != 0) {
$categoryDataPoints[] = [
"label" => $category['name'],
"y" => $category["cost"],
];
}
}
<?php
$categoryDataPoints = [];
foreach ($categoryCost as $category) {
if ($category['cost'] != 0) {
$categoryDataPoints[] = [
"label" => $category['name'],
"y" => $category["cost"],
];
}
}
$showCategoryCostGraph = count($categoryDataPoints) > 1;
$showCategoryCostGraph = count($categoryDataPoints) > 1;
$memberDataPoints = [];
foreach ($memberCost as $member) {
if ($member['cost'] != 0) {
$memberDataPoints[] = [
"label" => $member['name'],
"y" => $member["cost"],
];
}
}
$memberDataPoints = [];
foreach ($memberCost as $member) {
if ($member['cost'] != 0) {
$memberDataPoints[] = [
"label" => $member['name'],
"y" => $member["cost"],
];
}
}
$showMemberCostGraph = count($memberDataPoints) > 1;
$showMemberCostGraph = count($memberDataPoints) > 1;
$paymentMethodDataPoints = [];
foreach ($paymentMethodCount as $paymentMethod) {
if ($paymentMethod['count'] != 0) {
$paymentMethodDataPoints[] = [
"label" => $paymentMethod['name'],
"y" => $paymentMethod["count"],
];
}
}
$showPaymentMethodCountGraph = count($paymentMethodDataPoints) > 1;
if ($showMemberCostGraph) {
?>
<section class="graph">
<header>
<?= translate('household_split', $i18n) ?>
<div class="sub-header">(<?= translate('monthly_cost', $i18n) ?>)</div>
</header>
<canvas id="memberSplitChart"></canvas>
</section>
<?php
}
if ($showCategoryCostGraph) {
?>
<section class="graph">
<header>
<?= translate('category_split', $i18n) ?>
<div class="sub-header">(<?= translate('monthly_cost', $i18n) ?>)</div>
</header>
<canvas id="categorySplitChart" style="height: 370px; width: 100%;"></canvas>
</section>
<?php
}
if ($showPaymentMethodCountGraph) {
?>
<section class="graph">
<header>
<?= translate('payment_method_split', $i18n) ?>
</header>
<canvas id="paymentMethidSplitChart" style="height: 370px; width: 100%;"></canvas>
</section>
<?php
}
$paymentMethodDataPoints = [];
foreach ($paymentMethodCount as $paymentMethod) {
if ($paymentMethod['count'] != 0) {
$paymentMethodDataPoints[] = [
"label" => $paymentMethod['name'],
"y" => $paymentMethod["count"],
];
}
}
$showPaymentMethodCountGraph = count($paymentMethodDataPoints) > 1;
if ($showCategoryCostGraph || $showMemberCostGraph || $showPaymentMethodCountGraph) {
?>
</div>
<h2><?= translate('split_views', $i18n) ?></h2>
<div class="graphs">
<?php
if ($showMemberCostGraph) {
?>
<section class="graph">
<header>
<?= translate('household_split', $i18n) ?>
<div class="sub-header">(<?= translate('monthly_cost', $i18n) ?>)</div>
</header>
<canvas id="memberSplitChart"></canvas>
</section>
<?php
}
if ($showCategoryCostGraph) {
?>
<section class="graph">
<header>
<?= translate('category_split', $i18n) ?>
<div class="sub-header">(<?= translate('monthly_cost', $i18n) ?>)</div>
</header>
<canvas id="categorySplitChart" style="height: 370px; width: 100%;"></canvas>
</section>
<?php
}
if ($showPaymentMethodCountGraph) {
?>
<section class="graph">
<header>
<?= translate('payment_method_split', $i18n) ?>
</header>
<canvas id="paymentMethidSplitChart" style="height: 370px; width: 100%;"></canvas>
</section>
<?php
}
?>
</div>
<?php
}
?>
</section>
<?php
if ($showCategoryCostGraph || $showMemberCostGraph) {
if ($showCategoryCostGraph || $showMemberCostGraph || $showPaymentMethodCountGraph) {
?>
<script src="scripts/libs/chart.js"></script>
<script type="text/javascript">

View File

@ -11,15 +11,21 @@ header .logo .logo-image {
background-image: url("../images/wallossolidwhite.png");
}
.split-header > h2 .header-subtitle {
color: #A9A9A9;
}
.subscription,
.subscription-form,
.account-section,
.avatar-select,
.logo-search,
.icon-search,
.dropdown-content,
.sort-options,
.statistic,
.graph {
.graph,
.filtermenu-content {
background-color: #222;
border: 1px solid #333;
box-shadow: 0 2px 5px rgba(120, 120, 120, 0.1);
@ -37,6 +43,11 @@ header .logo .logo-image {
background-color: #333333;
}
.filtermenu-content .filter-item:hover,
.filtermenu-content .filter-title:hover {
background-color: #333333;
}
.subscription-form h3 {
color: #FFF;
border-bottom: 1px solid #EEE;

View File

@ -21,10 +21,34 @@ h2, h3 {
box-sizing: border-box;
}
.split-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.split-header h2 {
margin-right: 20px;
}
.split-header > h2 .header-subtitle {
font-size: 22px;
font-weight: 400;
color: #666666;
margin-left: 10px;
}
@media (max-width: 768px) {
.contain.settings {
padding: 20px 0px;
}
.split-header > h2 .header-subtitle {
margin-left: 0px;
font-size: 18px;
}
}
body > header {
@ -96,7 +120,7 @@ header .logo .logo-image {
.dropdown-content a {
color: black;
padding: 12px 16px;
padding: 14px 18px;
text-decoration: none;
display: block;
}
@ -593,6 +617,10 @@ main > .contain {
object-fit: contain;
}
.payments-list .payments-payment > .payment-name {
cursor: text;
}
.payments-list .payments-payment .delete-payment-method {
padding: 5px;
font-weight: bold;
@ -817,7 +845,7 @@ input[type="checkbox"] {
}
.icon-search {
width: 100px;
width: 112px;
height: 224px;
top: 50px;
right: 0px;
@ -1047,7 +1075,7 @@ input[type="checkbox"] {
.sort-options > ul > li {
list-style: none;
padding: 10px 35px 10px 10px;
padding: 14px 35px 14px 18px;
border-bottom: 1px solid #DDD;
cursor: pointer;
}
@ -1283,4 +1311,122 @@ input[type="checkbox"] {
flex-basis: 100%;
max-width: 100%;
}
}
/* Settings sort category */
.sortable-list {
margin: 0px;
padding: 0px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.drag-icon {
width: 28px;
height: 50px;
cursor: grab;
background-image: url(../images/siteicons/draggable.png);
background-repeat: no-repeat;
background-position: center;
background-size: 14px auto;
}
.sortable-list .sortable-ghost {
border-radius: 16px;
background-color: #c1d9f7;
border: 1px solid #8FBFFA;
}
/* Fitler dropdown */
.filtermenu {
position: relative;
display: inline-block;
}
.filtermenu-content {
display: none;
position: absolute;
background-color: #f9f9f9;
left: auto;
right: 0;
width: 220px;
background-color: #fff;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 3;
overflow: hidden;
margin-top: 4px;
}
.filtermenu-content.is-open {
display: block;
}
.filtermenu-content .filter-title {
padding: 14px 18px;
text-decoration: none;
display: block;
cursor: pointer;
font-weight: 500;
border-bottom: 1px solid #DDD;
user-select: none;
}
.filtermenu-content .filtermenu-submenu:last-of-type .filter-title {
border-bottom: none;
}
.filtermenu-content .filtermenu-submenu:last-of-type .filter-item:first-of-type {
border-top: 1px solid #DDD;
}
.filtermenu-content .filtermenu-submenu:last-of-type .filter-item:last-of-type {
border-bottom: none;
}
.filtermenu-content .filter-item {
padding: 14px 24px;
text-decoration: none;
display: block;
cursor: pointer;
border-bottom: 1px solid #DDD;
user-select: none;
font-size: 16px;
}
.filtermenu-content .filter-item.selected {
background-image: url(../images/siteicons/check.png);
background-size: 16px;
background-repeat: no-repeat;
background-position: center right 10px;
}
.filtermenu-content .filter-title.filter-clear {
color: #0056b3;
font-weight: normal;
border-bottom: none;
}
.filtermenu-content .filter-title.filter-clear > i {
margin-right: 8px;
}
.filtermenu-content .filter-item:hover,
.filtermenu-content .filter-title:hover {
background-color: #f1f1f1;
}
.filtermenu-submenu-content {
display: none;
}
.filtermenu-submenu-content.is-open {
display: block;
}