diff --git a/endpoints/categories/sort.php b/endpoints/categories/sort.php
new file mode 100644
index 000000000..955222635
--- /dev/null
+++ b/endpoints/categories/sort.php
@@ -0,0 +1,34 @@
+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();
+}
+
+?>
\ No newline at end of file
diff --git a/endpoints/payments/get.php b/endpoints/payments/get.php
index fdd5f4e06..902c64559 100644
--- a/endpoints/payments/get.php
+++ b/endpoints/payments/get.php
@@ -39,7 +39,7 @@
= $payment['name'] ?>
31 && !$inUse) {
+ if (!$inUse) {
?>
x
diff --git a/endpoints/payments/rename.php b/endpoints/payments/rename.php
new file mode 100644
index 000000000..f7ba2507b
--- /dev/null
+++ b/endpoints/payments/rename.php
@@ -0,0 +1,41 @@
+ 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)
+ ]);
+}
+
+?>
\ No newline at end of file
diff --git a/images/siteicons/draggable.png b/images/siteicons/draggable.png
new file mode 100644
index 000000000..55d8da3e8
Binary files /dev/null and b/images/siteicons/draggable.png differ
diff --git a/includes/getdbkeys.php b/includes/getdbkeys.php
index 0f0a8749e..6caaff19c 100644
--- a/includes/getdbkeys.php
+++ b/includes/getdbkeys.php
@@ -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'];
diff --git a/includes/i18n/de.php b/includes/i18n/de.php
index 632ed1a45..82edb037b 100644
--- a/includes/i18n/de.php
+++ b/includes/i18n/de.php
@@ -136,17 +136,24 @@
"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 @@
"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.",
diff --git a/includes/i18n/el.php b/includes/i18n/el.php
index f73753fdd..c4234c55f 100644
--- a/includes/i18n/el.php
+++ b/includes/i18n/el.php
@@ -136,17 +136,24 @@
"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 @@
"failed_remove_category" => "Απέτυχε η διαγραφή κατηγορίας",
"category_saved" => "Αποθήκευση κατηγορίας",
"category_removed" => "Διαγραφή κατηγορίας",
+ "sort_order_saved" => "Η ταξινόμηση αποθηκεύτηκε",
// Currency
"currency_saved" => "αποθηκεύτηκε.",
"error_adding_currency" => "Error adding currency entry.",
diff --git a/includes/i18n/en.php b/includes/i18n/en.php
index b6d06924f..cfcc650b8 100644
--- a/includes/i18n/en.php
+++ b/includes/i18n/en.php
@@ -136,17 +136,24 @@
"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 @@
"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.",
diff --git a/includes/i18n/es.php b/includes/i18n/es.php
index c0f60f843..44ee4e23c 100644
--- a/includes/i18n/es.php
+++ b/includes/i18n/es.php
@@ -136,17 +136,24 @@
"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 @@
"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.",
diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php
index ba844e315..e53ba7f3f 100644
--- a/includes/i18n/fr.php
+++ b/includes/i18n/fr.php
@@ -136,17 +136,24 @@
"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 @@
"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.",
diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php
index d0d0b4aaa..ec3afd0d5 100644
--- a/includes/i18n/jp.php
+++ b/includes/i18n/jp.php
@@ -136,17 +136,24 @@
"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 @@
"failed_remove_category" => "カテゴリの削除に失敗",
"category_saved" => "カテゴリの保存",
"category_removed" => "カテゴリの削除",
+ "sort_order_saved" => "並べ替え順が保存されました",
// Currency
"currency_saved" => "通貨を保存",
"error_adding_currency" => "通貨エントリの追加エラー.",
diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php
index 3f8249030..3f5f5840a 100644
--- a/includes/i18n/pt.php
+++ b/includes/i18n/pt.php
@@ -136,17 +136,24 @@
"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 @@
"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.",
diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php
index 15a7c9019..8d05ede92 100644
--- a/includes/i18n/tr.php
+++ b/includes/i18n/tr.php
@@ -136,17 +136,24 @@
"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 @@
"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.",
diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php
index 859d7cbc0..405125cb5 100644
--- a/includes/i18n/zh_cn.php
+++ b/includes/i18n/zh_cn.php
@@ -143,17 +143,25 @@
"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 @@
"failed_remove_category" => "移除分类失败",
"category_saved" => "分类已保存",
"category_removed" => "分类已移除",
+ "sort_order_saved" => "排序顺序已保存",
// Currency
"currency_saved" => "货币已保存。",
diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php
index 5cb233208..6838f4b93 100644
--- a/includes/i18n/zh_tw.php
+++ b/includes/i18n/zh_tw.php
@@ -136,17 +136,24 @@
"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 @@
"failed_remove_category" => "移除分類失敗",
"category_saved" => "分類已儲存",
"category_removed" => "分類已移除",
+ "sort_order_saved" => "排序順序已儲存",
// Currency
"currency_saved" => "已儲存。",
"error_adding_currency" => "新增貨幣時發生錯誤。",
diff --git a/includes/version.php b/includes/version.php
index ee4e48b5b..4a16bfe4a 100644
--- a/includes/version.php
+++ b/includes/version.php
@@ -1,3 +1,3 @@
\ No newline at end of file
diff --git a/migrations/000010.php b/migrations/000010.php
new file mode 100644
index 000000000..5c65e1976
--- /dev/null
+++ b/migrations/000010.php
@@ -0,0 +1,14 @@
+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');
+ }
+
+
+?>
\ No newline at end of file
diff --git a/scripts/libs/sortable.min.js b/scripts/libs/sortable.min.js
new file mode 100644
index 000000000..bb9953355
--- /dev/null
+++ b/scripts/libs/sortable.min.js
@@ -0,0 +1,2 @@
+/*! Sortable 1.15.2 - MIT | git://github.com/SortableJS/Sortable.git */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t
t.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&p(t,e)||o&&t===n)return t}while(t!==n&&(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode))}var i;return null}var g,m=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(m," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(m," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function b(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Bt(t){V&&V.parentNode[K]._isOutsideThisEl(t.target)}function Ft(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Pt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Ft.supportPointer&&"PointerEvent"in window&&!u,emptyInsertThreshold:5};for(n in W.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in kt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Nt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),Dt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,x())}function jt(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Ht(t){t.draggable=!1}function Lt(){Tt=!1}function Kt(t){return setTimeout(t,0)}function Wt(t){return clearTimeout(t)}Ft.prototype={constructor:Ft,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(mt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,V):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){xt.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&xt.push(o)}}(o),!V&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||tt===l)){if(ot=j(l),rt=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return q({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),G("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return q({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),G("filter",n,{evt:e}),!0}))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!V&&n.parentNode===r&&(o=X(n),Q=r,Z=(V=n).parentNode,J=V.nextSibling,tt=n,lt=a.group,ct={target:Ft.dragged=V,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ct.clientX-o.left,pt=ct.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,V.style["will-change"]="all",o=function(){G("delayEnded",i,{evt:t}),Ft.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(V.draggable=!0),i._triggerDragStart(t,e),q({sortable:i,name:"choose",originalEvent:t}),k(V,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){b(V,t.trim(),Ht)}),h(l,"dragover",Yt),h(l,"mousemove",Yt),h(l,"touchmove",Yt),h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,V.draggable=!0),G("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Ft.eventCanceled?this._onDrop():(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){V&&Ht(V),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;f(t,"mouseup",this._disableDelayedDrag),f(t,"touchend",this._disableDelayedDrag),f(t,"touchcancel",this._disableDelayedDrag),f(t,"mousemove",this._delayedDragTouchMoveHandler),f(t,"touchmove",this._delayedDragTouchMoveHandler),f(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(V,"dragend",this),h(Q,"dragstart",this._onDragStart));try{document.selection?Kt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;wt=!1,Q&&V?(G("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Bt),n=this.options,t||k(V,n.dragClass,!1),k(V,n.ghostClass,!0),Ft.active=this,t&&this._appendGhost(),q({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ut){this._lastX=ut.clientX,this._lastY=ut.clientY,Rt();for(var t=document.elementFromPoint(ut.clientX,ut.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ut.clientX,ut.clientY))!==e;)e=t;if(V.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:ut.clientX,clientY:ut.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=(t=e).parentNode);Xt()}},_onTouchMove:function(t){if(ct){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=$&&v($,!0),a=$&&r&&r.a,l=$&&r&&r.d,e=Mt&&yt&&E(yt),a=(i.clientX-ct.clientX+o.x)/(a||1)+(e?e[0]-Ct[0]:0)/(a||1),l=(i.clientY-ct.clientY+o.y)/(l||1)+(e?e[1]-Ct[1]:0)/(l||1);if(!Ft.active&&!wt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))D.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>D.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,$),e?t.clientX<_.left-10||t.clientY {
+ 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();
+ },
+});
\ No newline at end of file
diff --git a/scripts/stats.js b/scripts/stats.js
index d67db3097..cc8160e5e 100644
--- a/scripts/stats.js
+++ b/scripts/stats.js
@@ -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';
+}
\ No newline at end of file
diff --git a/service-worker.js b/service-worker.js
index 8fb587b95..ec834cd57 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -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',
diff --git a/settings.php b/settings.php
index 20f7a704c..c5a67fa1d 100644
--- a/settings.php
+++ b/settings.php
@@ -2,6 +2,7 @@
require_once 'includes/header.php';
?>
+