feat: edit and delete options now available directly on the subscription list

fix: typo on webhook payload
refactor: split currency endpoint
feat: add option to clone subscription
This commit is contained in:
Miguel Ribeiro
2024-06-21 02:00:27 +02:00
committed by GitHub
parent 1775f4ba91
commit 8304ed7b54
33 changed files with 398 additions and 13 deletions
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/inputvalidation.php';
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$currencyName = "Currency";
$currencySymbol = "$";
$currencyCode = "CODE";
$currencyRate = 1;
$sqlInsert = "INSERT INTO currencies (name, symbol, code, rate, user_id) VALUES (:name, :symbol, :code, :rate, :userId)";
$stmtInsert = $db->prepare($sqlInsert);
$stmtInsert->bindParam(':name', $currencyName, SQLITE3_TEXT);
$stmtInsert->bindParam(':symbol', $currencySymbol, SQLITE3_TEXT);
$stmtInsert->bindParam(':code', $currencyCode, SQLITE3_TEXT);
$stmtInsert->bindParam(':rate', $currencyRate, SQLITE3_TEXT);
$stmtInsert->bindParam(':userId', $userId, SQLITE3_INTEGER);
$resultInsert = $stmtInsert->execute();
if ($resultInsert) {
$currencyId = $db->lastInsertRowID();
echo $currencyId;
} else {
echo translate('error_adding_currency', $i18n);
}
} else {
$response = [
"success" => false,
"message" => translate('session_expired', $i18n)
];
echo json_encode($response);
}
?>
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/inputvalidation.php';
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if (isset($_GET['currencyId']) && $_GET['currencyId'] != "" && isset($_GET['name']) && $_GET['name'] != "" && isset($_GET['symbol']) && $_GET['symbol'] != "") {
$currencyId = $_GET['currencyId'];
$name = validate($_GET['name']);
$symbol = validate($_GET['symbol']);
$code = validate($_GET['code']);
$sql = "UPDATE currencies SET name = :name, symbol = :symbol, code = :code WHERE id = :currencyId AND user_id = :userId";
$stmt = $db->prepare($sql);
$stmt->bindParam(':name', $name, SQLITE3_TEXT);
$stmt->bindParam(':symbol', $symbol, SQLITE3_TEXT);
$stmt->bindParam(':code', $code, SQLITE3_TEXT);
$stmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);
$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
if ($result) {
$response = [
"success" => true,
"message" => $name . " " . translate('currency_saved', $i18n)
];
echo json_encode($response);
} else {
$response = [
"success" => false,
"message" => translate('failed_to_store_currency', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"message" => translate('fields_missing', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"message" => translate('session_expired', $i18n)
];
echo json_encode($response);
}
?>
+70
View File
@@ -0,0 +1,70 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/inputvalidation.php';
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if (isset($_GET['currencyId']) && $_GET['currencyId'] != "") {
$query = "SELECT main_currency FROM user WHERE id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$mainCurrencyId = $row['main_currency'];
$currencyId = $_GET['currencyId'];
$checkQuery = "SELECT COUNT(*) FROM subscriptions WHERE currency_id = :currencyId AND user_id = :userId";
$checkStmt = $db->prepare($checkQuery);
$checkStmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);
$checkStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$checkResult = $checkStmt->execute();
$row = $checkResult->fetchArray();
$count = $row[0];
if ($count > 0) {
$response = [
"success" => false,
"message" => translate('currency_in_use', $i18n)
];
echo json_encode($response);
exit;
} else {
if ($currencyId == $mainCurrencyId) {
$response = [
"success" => false,
"message" => translate('currency_is_main', $i18n)
];
echo json_encode($response);
exit;
} else {
$sql = "DELETE FROM currencies WHERE id = :currencyId AND user_id = :userId";
$stmt = $db->prepare($sql);
$stmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER);
$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
if ($result) {
echo json_encode(["success" => true, "message" => translate('currency_removed', $i18n)]);
} else {
$response = [
"success" => false,
"message" => translate('failed_to_remove_currency', $i18n)
];
echo json_encode($response);
}
}
}
} else {
$response = [
"success" => false,
"message" => translate('fields_missing', $i18n)
];
echo json_encode($response);
}
} else {
$response = [
"success" => false,
"message" => translate('session_expired', $i18n)
];
echo json_encode($response);
}
?>
+59
View File
@@ -0,0 +1,59 @@
<?php
require_once '../../includes/connect_endpoint.php';
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
if ($_SERVER["REQUEST_METHOD"] === "GET") {
$subscriptionId = $_GET["id"];
$query = "SELECT * FROM subscriptions WHERE id = :id AND user_id = :user_id";
$stmt = $db->prepare($query);
$stmt->bindValue(':id', $subscriptionId, SQLITE3_INTEGER);
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$subscriptionToClone = $result->fetchArray(SQLITE3_ASSOC);
if ($subscriptionToClone === false) {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
$query = "INSERT INTO subscriptions (name, logo, price, currency_id, next_payment, cycle, frequency, notes, payment_method_id, payer_user_id, category_id, notify, url, inactive, notify_days_before, user_id) VALUES (:name, :logo, :price, :currency_id, :next_payment, :cycle, :frequency, :notes, :payment_method_id, :payer_user_id, :category_id, :notify, :url, :inactive, :notify_days_before, :user_id)";
$cloneStmt = $db->prepare($query);
$cloneStmt->bindValue(':name', $subscriptionToClone['name'], SQLITE3_TEXT);
$cloneStmt->bindValue(':logo', $subscriptionToClone['logo'], SQLITE3_TEXT);
$cloneStmt->bindValue(':price', $subscriptionToClone['price'], SQLITE3_TEXT);
$cloneStmt->bindValue(':currency_id', $subscriptionToClone['currency_id'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':next_payment', $subscriptionToClone['next_payment'], SQLITE3_TEXT);
$cloneStmt->bindValue(':cycle', $subscriptionToClone['cycle'], SQLITE3_TEXT);
$cloneStmt->bindValue(':frequency', $subscriptionToClone['frequency'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':notes', $subscriptionToClone['notes'], SQLITE3_TEXT);
$cloneStmt->bindValue(':payment_method_id', $subscriptionToClone['payment_method_id'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':payer_user_id', $subscriptionToClone['payer_user_id'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':category_id', $subscriptionToClone['category_id'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':notify', $subscriptionToClone['notify'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':url', $subscriptionToClone['url'], SQLITE3_TEXT);
$cloneStmt->bindValue(':inactive', $subscriptionToClone['inactive'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':notify_days_before', $subscriptionToClone['notify_days_before'], SQLITE3_INTEGER);
$cloneStmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
if ($cloneStmt->execute()) {
$response = [
"success" => true,
"message" => translate('success', $i18n)
];
echo json_encode($response);
} else {
die(json_encode([
"success" => false,
"message" => translate("error", $i18n)
]));
}
} else {
die(json_encode([
"success" => false,
"message" => translate('invalid_request_method', $i18n)
]));
}
}
$db->close();
?>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Leere Seite",
"clear_filters" => "Filter zurücksetzen",
"no_matching_subscriptions" => "Keine passenden Abonnements gefunden",
"clone" => "Klonen",
// Subscription form
"add_subscription" => "Abonnement hinzufügen",
"edit_subscription" => "Abonnement editieren",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Κενή σελίδα",
"clear_filters" => "Καθαρισμός φίλτρων",
"no_matching_subscriptions" => "Δεν υπάρχουν συνδρομές που ταιριάζουν με τα φίλτρα σου",
"clone" => "Κλώνος",
// Subscription form
"add_subscription" => "Προσθήκη συνδρομής",
"edit_subscription" => "Επεξεργασία συνδρομής",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Empty Page",
"clear_filters" => "Clear Filters",
"no_matching_subscriptions" => "No matching subscriptions",
"clone" => "Clone",
// Subscription form
"add_subscription" => "Add subscription",
"edit_subscription" => "Edit subscription",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Página Vacía",
"clear_filters" => "Limpiar Filtros",
"no_matching_subscriptions" => "No hay suscripciones que coincidan con los filtros",
"clone" => "Clonar",
// Subscription form
"add_subscription" => "Añadir suscripción",
"edit_subscription" => "Editar suscripción",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Page vide",
"clear_filters" => "Effacer les filtres",
"no_matching_subscriptions" => "Aucun abonnement ne correspond à vos critères de recherche",
"clone" => "Cloner",
// Formulaire d'abonnement
"add_subscription" => "Ajouter un abonnement",
"edit_subscription" => "Modifier l'abonnement",
+1
View File
@@ -69,6 +69,7 @@ $i18n = [
'empty_page' => 'Pagina vuota',
'clear_filters' => 'Pulisci filtri',
'no_matching_subscriptions' => 'Nessun abbonamento corrispondente',
"clone" => "Clona",
// Add/Edit Subscription
'add_subscription' => 'Aggiungi abbonamento',
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "空のページ",
"clear_filters" => "フィルタをクリア",
"no_matching_subscriptions" => "一致する定期購入がありません",
"clone" => "複製",
// Subscription form
"add_subscription" => "定期購入の追加",
"edit_subscription" => "定期購入の編集",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "빈 페이지",
"clear_filters" => "필터 제거",
"no_matching_subscriptions" => "해당하는 구독이 없습니다.",
"clone" => "복제",
// Subscription form
"add_subscription" => "구독 추가",
"edit_subscription" => "구독 편집",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Pusta strona",
"clear_filters" => "Wyczyść filtry",
"no_matching_subscriptions" => "Brak pasujących subskrypcji",
"clone" => "Klonuj",
// Subscription form
"add_subscription" => "Dodaj subskrypcję",
"edit_subscription" => "Edytuj subskrypcję",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Página Vazia",
"clear_filters" => "Limpar Filtros",
"no_matching_subscriptions" => "Sem subscrições correspondentes",
"clone" => "Clonar",
// Subscription form
"add_subscription" => "Adicionar subscrição",
"edit_subscription" => "Modificar subscrição",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Página vazia",
"clear_filters" => "Limpar filtros",
"no_matching_subscriptions" => "Nenhuma assinatura encontrada",
"clone" => "Clonar",
// Subscription form
"add_subscription" => "Adicionar assinatura",
"edit_subscription" => "Editar assinatura",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Пустая страница",
"clear_filters" => "Очистить фильтры",
"no_matching_subscriptions" => "Нет подходящих подписок",
"clone" => "Клонировать",
// Subscription form
"add_subscription" => "Добавить подписку",
"edit_subscription" => "Изменить подписку",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Prazna stran",
"clear_filters" => "Počisti filter",
"no_matching_subscriptions" => "Ni ustreznih naročnin",
"clone" => "Klon",
// Subscription form
"add_subscription" => "Dodaj naročnino",
"edit_subscription" => "Uredi naročnino",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Празна страница",
"clear_filters" => "Очисти филтере",
"no_matching_subscriptions" => "Нема подударајућих претплата",
"clone" => "Клонирај",
// Форма за претплату
"add_subscription" => "Додај претплату",
"edit_subscription" => "Уреди претплату",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Prazna stranica",
"clear_filters" => "Očisti filtere",
"no_matching_subscriptions" => "Nema podudarajućih pretplata",
"clone" => "Kloniraj",
// Forma za pretplatu
"add_subscription" => "Dodaj pretplatu",
"edit_subscription" => "Uredi pretplatu",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "Boş Sayfa",
"clear_filters" => "Filtreleri Temizle",
"no_matching_subscriptions" => "Eşleşen abonelik bulunamadı",
"clone" => "Kopyala",
// Subscription form
"add_subscription" => "Abonelik ekle",
"edit_subscription" => "Aboneliği düzenle",
+1
View File
@@ -69,6 +69,7 @@ $i18n = [
"empty_page" => "空白页面",
"clear_filters" => "清除筛选",
"no_matching_subscriptions" => "没有匹配的订阅",
"clone" => "克隆",
// 订阅表单
"add_subscription" => "添加订阅",
+1
View File
@@ -65,6 +65,7 @@ $i18n = [
"empty_page" => "空白頁面",
"clear_filters" => "清除篩選",
"no_matching_subscriptions" => "沒有符合的訂閱",
"clone" => "複製",
// 訂閱表單
"add_subscription" => "新增訂閱",
"edit_subscription" => "編輯訂閱",
+16 -4
View File
@@ -108,11 +108,23 @@
<img src="<?= $subscription['payment_method_icon'] ?>" title="<?= translate('payment_method', $i18n) ?>: <?= $subscription['payment_method_name'] ?>"/>
<?= CurrencyFormatter::format($subscription['price'], $subscription['currency_code']) ?>
</span>
<span class="actions">
<button class="image-button medium" onClick="openEditSubscription(event, <?= $subscription['id'] ?>)" name="edit">
<img src="images/siteicons/<?= $colorTheme ?>/edit.png" title="<?= translate('edit_subscription', $i18n) ?>">
<button type="button" class="actions-expand" onClick="expandActions(event, <?= $subscription['id'] ?>)">
<i class="fas fa-ellipsis-v"></i>
</button>
</span>
<ul class="actions">
<li class="edit" title="<?= translate('edit_subscription', $i18n) ?>" onClick="openEditSubscription(event, <?= $subscription['id'] ?>)">
<img src="images/siteicons/<?= $colorTheme ?>/edit.png" title="<?= translate('edit_subscription', $i18n) ?>">
<?= translate('edit_subscription', $i18n) ?>
</li>
<li class="delete" title="<?= translate('delete', $i18n) ?>" onClick="deleteSubscription(event, <?= $subscription['id'] ?>)">
<img src="images/siteicons/<?= $colorTheme ?>/delete.png" title="<?= translate('edit_subscription', $i18n) ?>">
<?= translate('delete', $i18n) ?>
</li>
<li class="clone" title="<?= translate('clone', $i18n) ?>" onClick="cloneSubscription(event, <?= $subscription['id'] ?>)">
<img src="images/siteicons/<?= $colorTheme ?>/clone.png" title="<?= translate('edit_subscription', $i18n) ?>">
<?= translate('clone', $i18n) ?>
</li>
</ul>
</div>
<div class="subscription-secondary">
<span class="name"><img src="images/siteicons/<?= $colorTheme ?>/subscription.png" alt="<?= translate('subscription', $i18n) ?>" /><?= $subscription['name'] ?></span>
+1 -1
View File
@@ -1,3 +1,3 @@
<?php
$version = "v2.4.2";
$version = "v2.5.0";
?>
+66 -2
View File
@@ -160,7 +160,9 @@ function handleFileSelect(event) {
}
}
function deleteSubscription(id) {
function deleteSubscription(event, id) {
event.stopPropagation();
event.preventDefault();
if (confirm(translate('confirm_delete_subscription'))) {
fetch(`endpoints/subscription/delete.php?id=${id}`, {
method: 'DELETE',
@@ -171,7 +173,7 @@ function deleteSubscription(id) {
fetchSubscriptions();
closeAddSubscription();
} else {
alert(translate('error_deleting_subscription'));
showErrorMessage(translate('error_deleting_subscription'));
}
})
.catch(error => {
@@ -180,6 +182,32 @@ function deleteSubscription(id) {
}
}
function cloneSubscription(event, id) {
event.stopPropagation();
event.preventDefault();
const url = `endpoints/subscription/clone.php?id=${id}`;
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(translate('network_response_error'));
}
return response.json();
})
.then(data => {
if (data.success) {
fetchSubscriptions();
showSuccessMessage(decodeURI(data.message));
} else {
showErrorMessage(data.message || translate('error'));
}
})
.catch(error => {
showErrorMessage(error.message || translate('error'));
});
}
function setSearchButtonStatus() {
const nameInput = document.querySelector("#name");
@@ -468,4 +496,40 @@ function clearFilters() {
});
document.querySelector('#clear-filters').classList.add('hide');
fetchSubscriptions();
}
let currentActions = null;
document.addEventListener('click', function(event) {
// Check if click was outside currentActions
if (currentActions && !currentActions.contains(event.target)) {
// Click was outside currentActions, close currentActions
currentActions.classList.remove('is-open');
currentActions = null;
}
});
function expandActions(event, subscriptionId) {
event.stopPropagation();
event.preventDefault();
const subscriptionDiv = document.querySelector(`.subscription[data-id="${subscriptionId}"]`);
const actions = subscriptionDiv.querySelector('.actions');
// Close all other open actions
const allActions = document.querySelectorAll('.actions.is-open');
allActions.forEach((openAction) => {
if (openAction !== actions) {
openAction.classList.remove('is-open');
}
});
// Toggle the clicked actions
actions.classList.toggle('is-open');
// Update currentActions
if (actions.classList.contains('is-open')) {
currentActions = actions;
} else {
currentActions = null;
}
}
+3 -3
View File
@@ -360,7 +360,7 @@ function editCategory(categoryId) {
function addCurrencyButton(currencyId) {
document.getElementById("addCurrency").disabled = true;
const url = 'endpoints/currency/currency.php?action=add';
const url = 'endpoints/currency/add.php';
fetch(url)
.then(response => {
if (!response.ok) {
@@ -442,7 +442,7 @@ function addCurrencyButton(currencyId) {
}
function removeCurrency(currencyId) {
let url = `endpoints/currency/currency.php?action=delete&currencyId=${currencyId}`;
let url = `endpoints/currency/remove.php?currencyId=${currencyId}`;
fetch(url)
.then(response => {
if (!response.ok) {
@@ -477,7 +477,7 @@ function editCurrency(currencyId) {
var currencyName = encodeURIComponent(inputNameElement.value);
var currencySymbol = encodeURIComponent(inputSymbolElement.value);
var currencyCode = encodeURIComponent(inputCodeElement.value);
var url = `endpoints/currency/currency.php?action=edit&currencyId=${currencyId}&name=${currencyName}&symbol=${currencySymbol}&code=${currencyCode}`;
var url = `endpoints/currency/edit.php?currencyId=${currencyId}&name=${currencyName}&symbol=${currencySymbol}&code=${currencyCode}`;
fetch(url)
.then(response => {
+1 -1
View File
@@ -364,7 +364,7 @@
"category": "{{subscription_category}}",
"date": "{{subscription_date}}",
"payer": "{{subscription_payer}}"
"dyas": "{{subscription_days_until_payment}}"
"days": "{{subscription_days_until_payment}}"
}
]
+25 -1
View File
@@ -25,7 +25,8 @@ header .logo .logo-image {
.sort-options,
.statistic,
.graph,
.filtermenu-content {
.filtermenu-content,
.subscription-main .actions {
background-color: #222;
border: 1px solid #333;
box-shadow: 0 2px 5px rgba(120, 120, 120, 0.1);
@@ -53,6 +54,29 @@ header .logo .logo-image {
border-bottom: 1px solid #EEE;
}
.subscription.inactive {
background-color: rgba(24,24,24,0.3);
color: rgba(200,200,200,0.6);
box-shadow: 0 2px 5px rgba(50, 50, 50, 0.1);
}
.subscription-main .actions {
color: #E0E0E0
}
.subscription-main .actions > li {
border-bottom: 1px solid #555;
border-color: #666;
}
.subscription-main .actions > li:hover {
background-color: #333;
}
.subscription-main .actions > li:last-of-type {
border: none;
}
.close-form {
color: #EEE;
}
+58 -1
View File
@@ -262,7 +262,9 @@ main > .contain {
}
.subscription.inactive {
opacity: 0.6;
background-color: rgba(255,255,255,0.6);
color: rgba(100,100,100,0.6);
box-shadow: 0 2px 5px rgba(100, 100, 100, 0.1);
}
.subscription.inactive span.price {
@@ -274,8 +276,63 @@ main > .contain {
flex-direction: row;
align-items: center;
gap: 12px;
position: relative;
}
.subscription-main .actions-expand {
font-size: 21px;
padding: 8px 16px;
color: var(--main-color);
background-color: transparent;
border: none;
cursor: pointer;
}
.subscription-main .actions-expand:hover {
color: var(--hover-color);
}
.subscription-main .actions {
display: none;
position: absolute;
right: -16px;
top: 60px;
z-index: 2;
flex-direction: column;
color: #202020;
background-color: #FFFFFF;
border: 1px solid #eee;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
border-radius: 16px;
padding: 0px;
margin: 0px;
}
.subscription-main .actions.is-open {
display: flex;
}
.subscription-main .actions > li {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 14px 35px 14px 18px;
gap: 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.subscription-main .actions > li:hover {
background-color: #f9f9f9;
}
.subscription-main .actions > li > i {
color: var(--main-color);
}
.subscription-main .actions > li:hover > i {
color: var(--hover-color);
}
.subscription-secondary {
display: none;