From 3a83b4fe58406ef9c5bc5bcbd7a1f0fc7eb8bc53 Mon Sep 17 00:00:00 2001 From: Miguel Ribeiro Date: Sat, 16 Nov 2024 16:18:04 +0100 Subject: [PATCH 1/4] feat: add some leeway for totp codes --- endpoints/user/disable_totp.php | 3 ++- endpoints/user/enable_totp.php | 4 +++- scripts/profile.js | 1 - totp.php | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/endpoints/user/disable_totp.php b/endpoints/user/disable_totp.php index d0936524e..a376bd7c3 100644 --- a/endpoints/user/disable_totp.php +++ b/endpoints/user/disable_totp.php @@ -68,8 +68,9 @@ function trigger_deprecation($package, $version, $message, ...$args) $clock = new OTPHP\InternalClock(); $totp = OTPHP\TOTP::createFromSecret($secret, $clock); + $totp->setPeriod(30); - if ($totp->verify($totp_code)) { + if ($totp->verify($totp_code, null, 15)) { $statement = $db->prepare('UPDATE user SET totp_enabled = 0 WHERE id = :id'); $statement->bindValue(':id', $userId, SQLITE3_INTEGER); $statement->execute(); diff --git a/endpoints/user/enable_totp.php b/endpoints/user/enable_totp.php index 8e0bbdbc2..0e77307bc 100644 --- a/endpoints/user/enable_totp.php +++ b/endpoints/user/enable_totp.php @@ -86,9 +86,11 @@ function base32_encode($hex) } $clock = new OTPHP\InternalClock(); + $totp = OTPHP\TOTP::createFromSecret($secret, $clock); + $totp->setPeriod(30); - if ($totp->verify($totp_code)) { + if ($totp->verify($totp_code, null, 15)) { // Generate 10 backup codes $backupCodes = []; for ($i = 0; $i < 10; $i++) { diff --git a/scripts/profile.js b/scripts/profile.js index 5ca60ba27..04501de6d 100644 --- a/scripts/profile.js +++ b/scripts/profile.js @@ -180,7 +180,6 @@ function submitTotp() { totpBackupCodes.classList.remove('hide'); } else { showErrorMessage(data.message); - console.log(error); } }) .catch(error => { diff --git a/totp.php b/totp.php index 94106c0d7..047b3cd78 100644 --- a/totp.php +++ b/totp.php @@ -71,7 +71,8 @@ $clock = new OTPHP\InternalClock(); $totp = OTPHP\TOTP::createFromSecret($totp_secret, $clock); - $valid = $totp->verify($totp_code); + $totp->setPeriod(30); + $valid = $totp->verify($totp_code, null, 15); // If totp is not valid check backup codes if (!$valid) { From c1d24edead2ed47e20e7ca3286559c049afc2289 Mon Sep 17 00:00:00 2001 From: Miguel Ribeiro Date: Sun, 17 Nov 2024 15:51:52 +0100 Subject: [PATCH 2/4] feat: add start date to subscriptions feat: add option for manual/automatic renewals --- api/subscriptions/get_subscriptions.php | 4 + endpoints/admin/deleteuser.php | 10 +++ endpoints/cronjobs/updatenextpayment.php | 2 +- endpoints/settings/deleteaccount.php | 10 +++ endpoints/subscription/add.php | 12 ++- endpoints/subscription/clone.php | 2 + endpoints/subscription/get.php | 2 + endpoints/subscription/renew.php | 89 ++++++++++++++++++++++ endpoints/subscriptions/export.php | 1 + endpoints/subscriptions/get.php | 1 + images/siteicons/svg/automatic.php | 3 + images/siteicons/svg/manual.php | 3 + images/siteicons/svg/mobile-menu/renew.php | 3 + images/siteicons/svg/renew.php | 5 ++ includes/i18n/de.php | 5 ++ includes/i18n/el.php | 5 ++ includes/i18n/en.php | 5 ++ includes/i18n/es.php | 5 ++ includes/i18n/fr.php | 5 ++ includes/i18n/it.php | 5 ++ includes/i18n/jp.php | 5 ++ includes/i18n/ko.php | 5 ++ includes/i18n/pl.php | 5 ++ includes/i18n/pt.php | 5 ++ includes/i18n/pt_br.php | 5 ++ includes/i18n/ru.php | 5 ++ includes/i18n/sl.php | 5 ++ includes/i18n/sr.php | 5 ++ includes/i18n/sr_lat.php | 5 ++ includes/i18n/tr.php | 5 ++ includes/i18n/vi.php | 5 ++ includes/i18n/zh_cn.php | 5 ++ includes/i18n/zh_tw.php | 5 ++ includes/list_subscriptions.php | 66 ++++++++++++---- index.php | 17 +++++ migrations/000032.php | 35 +++++++++ scripts/dashboard.js | 41 +++++++++- styles/styles.css | 17 +++++ 38 files changed, 401 insertions(+), 17 deletions(-) create mode 100644 endpoints/subscription/renew.php create mode 100644 images/siteicons/svg/automatic.php create mode 100644 images/siteicons/svg/manual.php create mode 100644 images/siteicons/svg/mobile-menu/renew.php create mode 100644 images/siteicons/svg/renew.php create mode 100644 migrations/000032.php diff --git a/api/subscriptions/get_subscriptions.php b/api/subscriptions/get_subscriptions.php index 96fbeb298..fc118669a 100644 --- a/api/subscriptions/get_subscriptions.php +++ b/api/subscriptions/get_subscriptions.php @@ -28,9 +28,11 @@ "logo": "example.png", "price": 10.00, "currency_id": 1, + "start_date": "2024-09-01", "next_payment": "2024-09-01", "cycle": 1, "frequency": 1, + "auto_renew": 1, "notes": "Example note", "payment_method_id": 1, "payer_user_id": 1, @@ -52,9 +54,11 @@ "logo": "another.png", "price": 15.00, "currency_id": 2, + "start_date": "2024-09-02", "next_payment": "2024-09-02", "cycle": 1, "frequency": 1, + "auto_renew": 0, "notes": "", "payment_method_id": 2, "payer_user_id": 2, diff --git a/endpoints/admin/deleteuser.php b/endpoints/admin/deleteuser.php index c00b87691..b137f5044 100644 --- a/endpoints/admin/deleteuser.php +++ b/endpoints/admin/deleteuser.php @@ -115,6 +115,16 @@ $stmt->bindValue(':id', $userId, SQLITE3_INTEGER); $result = $stmt->execute(); + // Delete totp + $stmt = $db->prepare('DELETE FROM totp WHERE user_id = :id'); + $stmt->bindValue(':id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + // Delete total yearly cost + $stmt = $db->prepare('DELETE FROM total_yearly_cost WHERE user_id = :id'); + $stmt->bindValue(':id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + die(json_encode([ "success" => true, "message" => translate('success', $i18n) diff --git a/endpoints/cronjobs/updatenextpayment.php b/endpoints/cronjobs/updatenextpayment.php index 308b86274..6013103ed 100644 --- a/endpoints/cronjobs/updatenextpayment.php +++ b/endpoints/cronjobs/updatenextpayment.php @@ -19,7 +19,7 @@ $cycles[$cycleId] = $row; } -$query = "SELECT id, next_payment, frequency, cycle FROM subscriptions WHERE next_payment < :currentDate"; +$query = "SELECT id, next_payment, frequency, cycle FROM subscriptions WHERE next_payment < :currentDate AND auto_renew = 1"; $stmt = $db->prepare($query); $stmt->bindValue(':currentDate', $currentDate->format('Y-m-d')); $result = $stmt->execute(); diff --git a/endpoints/settings/deleteaccount.php b/endpoints/settings/deleteaccount.php index 9c5c72f00..9b4626860 100644 --- a/endpoints/settings/deleteaccount.php +++ b/endpoints/settings/deleteaccount.php @@ -107,6 +107,16 @@ $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER); $result = $stmt->execute(); + // Delete totp + $stmt = $db->prepare('DELETE FROM totp WHERE user_id = :id'); + $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER); + $result = $stmt->execute(); + + // Delete total yearly cost + $stmt = $db->prepare('DELETE FROM total_yearly_cost WHERE user_id = :id'); + $stmt->bindValue(':id', $userIdToDelete, SQLITE3_INTEGER); + $result = $stmt->execute(); + die(json_encode([ "success" => true, "message" => translate('success', $i18n) diff --git a/endpoints/subscription/add.php b/endpoints/subscription/add.php index 4d7d550b6..66f7c3654 100644 --- a/endpoints/subscription/add.php +++ b/endpoints/subscription/add.php @@ -167,6 +167,8 @@ function resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings) $frequency = $_POST["frequency"]; $cycle = $_POST["cycle"]; $nextPayment = $_POST["next_payment"]; + $autoRenew = isset($_POST['auto_renew']) ? true : false; + $startDate = $_POST["start_date"]; $paymentMethodId = $_POST["payment_method_id"]; $payerUserId = $_POST["payer_user_id"]; $categoryId = $_POST['category_id']; @@ -201,11 +203,13 @@ function resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings) $sql = "INSERT INTO subscriptions ( name, logo, price, currency_id, next_payment, cycle, frequency, notes, payment_method_id, payer_user_id, category_id, notify, inactive, url, - notify_days_before, user_id, cancellation_date, replacement_subscription_id + notify_days_before, user_id, cancellation_date, replacement_subscription_id, + auto_renew, start_date ) VALUES ( :name, :logo, :price, :currencyId, :nextPayment, :cycle, :frequency, :notes, :paymentMethodId, :payerUserId, :categoryId, :notify, :inactive, :url, - :notifyDaysBefore, :userId, :cancellationDate, :replacement_subscription_id + :notifyDaysBefore, :userId, :cancellationDate, :replacement_subscription_id, + :autoRenew, :startDate )"; } else { $id = $_POST['id']; @@ -214,6 +218,8 @@ function resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings) price = :price, currency_id = :currencyId, next_payment = :nextPayment, + auto_renew = :autoRenew, + start_date = :startDate, cycle = :cycle, frequency = :frequency, notes = :notes, @@ -242,6 +248,8 @@ function resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings) $stmt->bindParam(':price', $price, SQLITE3_FLOAT); $stmt->bindParam(':currencyId', $currencyId, SQLITE3_INTEGER); $stmt->bindParam(':nextPayment', $nextPayment, SQLITE3_TEXT); + $stmt->bindParam(':autoRenew', $autoRenew, SQLITE3_INTEGER); + $stmt->bindParam(':startDate', $startDate, SQLITE3_TEXT); $stmt->bindParam(':cycle', $cycle, SQLITE3_INTEGER); $stmt->bindParam(':frequency', $frequency, SQLITE3_INTEGER); $stmt->bindParam(':notes', $notes, SQLITE3_TEXT); diff --git a/endpoints/subscription/clone.php b/endpoints/subscription/clone.php index e143fb6d7..567d9f05b 100644 --- a/endpoints/subscription/clone.php +++ b/endpoints/subscription/clone.php @@ -24,6 +24,8 @@ $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(':auto_renew', $subscriptionToClone['auto_renew'], SQLITE3_INTEGER); + $cloneStmt->bindValue(':start_date', $subscriptionToClone['start_date'], SQLITE3_TEXT); $cloneStmt->bindValue(':cycle', $subscriptionToClone['cycle'], SQLITE3_TEXT); $cloneStmt->bindValue(':frequency', $subscriptionToClone['frequency'], SQLITE3_INTEGER); $cloneStmt->bindValue(':notes', $subscriptionToClone['notes'], SQLITE3_TEXT); diff --git a/endpoints/subscription/get.php b/endpoints/subscription/get.php index 0668139bd..31bd2bf23 100644 --- a/endpoints/subscription/get.php +++ b/endpoints/subscription/get.php @@ -18,6 +18,8 @@ $subscriptionData['logo'] = $row['logo']; $subscriptionData['price'] = $row['price']; $subscriptionData['currency_id'] = $row['currency_id']; + $subscriptionData['auto_renew'] = $row['auto_renew']; + $subscriptionData['start_date'] = $row['start_date']; $subscriptionData['next_payment'] = $row['next_payment']; $subscriptionData['frequency'] = $row['frequency']; $subscriptionData['cycle'] = $row['cycle']; diff --git a/endpoints/subscription/renew.php b/endpoints/subscription/renew.php new file mode 100644 index 000000000..174fd6c08 --- /dev/null +++ b/endpoints/subscription/renew.php @@ -0,0 +1,89 @@ +format('Y-m-d'); + + $cycles = array(); + $query = "SELECT * FROM cycles"; + $result = $db->query($query); + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $cycleId = $row['id']; + $cycles[$cycleId] = $row; + } + + $subscriptionId = $_GET["id"]; + $query = "SELECT * FROM subscriptions WHERE id = :id AND user_id = :user_id AND auto_renew = 0"; + $stmt = $db->prepare($query); + $stmt->bindValue(':id', $subscriptionId, SQLITE3_INTEGER); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $subscriptionToRenew = $result->fetchArray(SQLITE3_ASSOC); + if ($subscriptionToRenew === false) { + die(json_encode([ + "success" => false, + "message" => translate("error", $i18n) + ])); + } + + $nextPaymentDate = new DateTime($subscriptionToRenew['next_payment']); + $frequency = $subscriptionToRenew['frequency']; + $cycle = $cycles[$subscriptionToRenew['cycle']]['name']; + + // Calculate the interval to add based on the cycle + $intervalSpec = "P"; + if ($cycle == 'Daily') { + $intervalSpec .= "{$frequency}D"; + } elseif ($cycle === 'Weekly') { + $intervalSpec .= "{$frequency}W"; + } elseif ($cycle === 'Monthly') { + $intervalSpec .= "{$frequency}M"; + } elseif ($cycle === 'Yearly') { + $intervalSpec .= "{$frequency}Y"; + } + + $interval = new DateInterval($intervalSpec); + + // Add intervals until the next payment date is in the future and after current next payment date + while ($nextPaymentDate < $currentDate || $nextPaymentDate == new DateTime($subscriptionToRenew['next_payment'])) { + $nextPaymentDate->add($interval); + } + + // Update the subscription's next_payment date + $updateQuery = "UPDATE subscriptions SET next_payment = :nextPaymentDate WHERE id = :subscriptionId"; + $updateStmt = $db->prepare($updateQuery); + $updateStmt->bindValue(':nextPaymentDate', $nextPaymentDate->format('Y-m-d')); + $updateStmt->bindValue(':subscriptionId', $subscriptionId); + $updateStmt->execute(); + + if ($updateStmt->execute()) { + $response = [ + "success" => true, + "message" => translate('success', $i18n), + "id" => $subscriptionId + ]; + echo json_encode($response); + } else { + die(json_encode([ + "success" => false, + "message" => translate("error", $i18n) + ])); + } + } else { + $db->close(); + die(json_encode([ + "success" => false, + "message" => translate('invalid_request_method', $i18n) + ])); + } +} else { + $db->close(); + die(json_encode([ + "success" => false, + "message" => translate('session_expired', $i18n) + ])); +} + +?> \ No newline at end of file diff --git a/endpoints/subscriptions/export.php b/endpoints/subscriptions/export.php index b00575629..86f633d95 100644 --- a/endpoints/subscriptions/export.php +++ b/endpoints/subscriptions/export.php @@ -21,6 +21,7 @@ $subscriptionDetails = array( 'Name' => str_replace(',', ' ', $row['name']), 'Next Payment' => $row['next_payment'], + 'Renewal' => $row['auto_renew'] ? 'Automatic' : 'Manual', 'Category' => str_replace(',', ' ', $categories[$row['category_id']]['name']), 'Payment Method' => str_replace(',', ' ', $payment_methods[$row['payment_method_id']]['name']), 'Paid By' => str_replace(',', ' ', $members[$row['payer_user_id']]['name']), diff --git a/endpoints/subscriptions/get.php b/endpoints/subscriptions/get.php index 10c1cc53d..9a9cfbae3 100644 --- a/endpoints/subscriptions/get.php +++ b/endpoints/subscriptions/get.php @@ -156,6 +156,7 @@ $next_payment_timestamp = strtotime($subscription['next_payment']); $formatted_date = $formatter->format($next_payment_timestamp); $print[$id]['next_payment'] = $formatted_date; + $print[$id]['auto_renew'] = $subscription['auto_renew']; $paymentIconFolder = (strpos($payment_methods[$paymentMethodId]['icon'], 'images/uploads/icons/') !== false) ? "" : "images/uploads/logos/"; $print[$id]['payment_method_icon'] = $paymentIconFolder . $payment_methods[$paymentMethodId]['icon']; $print[$id]['payment_method_name'] = $payment_methods[$paymentMethodId]['name']; diff --git a/images/siteicons/svg/automatic.php b/images/siteicons/svg/automatic.php new file mode 100644 index 000000000..67b1f4a62 --- /dev/null +++ b/images/siteicons/svg/automatic.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/images/siteicons/svg/manual.php b/images/siteicons/svg/manual.php new file mode 100644 index 000000000..78db52da8 --- /dev/null +++ b/images/siteicons/svg/manual.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/images/siteicons/svg/mobile-menu/renew.php b/images/siteicons/svg/mobile-menu/renew.php new file mode 100644 index 000000000..3bacc09dc --- /dev/null +++ b/images/siteicons/svg/mobile-menu/renew.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/images/siteicons/svg/renew.php b/images/siteicons/svg/renew.php new file mode 100644 index 000000000..c548c0df2 --- /dev/null +++ b/images/siteicons/svg/renew.php @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/includes/i18n/de.php b/includes/i18n/de.php index 8f243ebc0..1dc631fca 100644 --- a/includes/i18n/de.php +++ b/includes/i18n/de.php @@ -48,6 +48,10 @@ "last_added" => "Zuletzt hinzugefügt", "price" => "Preis", "next_payment" => "Nächste Zahlung", + "auto_renewal" => "Automatische Verlängerung", + "automatically_renews" => "Automatisch verlängert", + "manual_renewal" => "Manuelle Verlängerung", + "start_date" => "Startdatum", "inactive" => "Abonnement deaktivieren", "replaced_with" => "Ersetzt durch", "none" => "Keine", @@ -71,6 +75,7 @@ "clear_filters" => "Filter zurücksetzen", "no_matching_subscriptions" => "Keine passenden Abonnements gefunden", "clone" => "Klonen", + "renew" => "Verlängern", // Subscription form "add_subscription" => "Abonnement hinzufügen", "edit_subscription" => "Abonnement editieren", diff --git a/includes/i18n/el.php b/includes/i18n/el.php index fd4033496..17f75d291 100644 --- a/includes/i18n/el.php +++ b/includes/i18n/el.php @@ -48,6 +48,10 @@ "last_added" => "Τελευταία προσθήκη", "price" => "Τιμή", "next_payment" => "Επόμενη πληρωμή", + "auto_renewal" => "Αυτόματη ανανέωση", + "automatically_renews" => "Ανανεώνεται αυτόματα", + "manual_renewal" => "Χειροκίνητη ανανέωση", + "start_date" => "Ημερομηνία έναρξης", "inactive" => "Απενεργοποίηση συνδρομής", "replaced_with" => "Αντικαταστάθηκε με", "none" => "Κανένα", @@ -71,6 +75,7 @@ "clear_filters" => "Καθαρισμός φίλτρων", "no_matching_subscriptions" => "Δεν υπάρχουν συνδρομές που ταιριάζουν με τα φίλτρα σου", "clone" => "Κλώνος", + "renew" => "Ανανέωση", // Subscription form "add_subscription" => "Προσθήκη συνδρομής", "edit_subscription" => "Επεξεργασία συνδρομής", diff --git a/includes/i18n/en.php b/includes/i18n/en.php index 6cefaffa1..ced661377 100644 --- a/includes/i18n/en.php +++ b/includes/i18n/en.php @@ -48,6 +48,10 @@ "last_added" => "Last Added", "price" => "Price", "next_payment" => "Next Payment", + "auto_renewal" => "Auto Renewal", + "automatically_renews" => "Automatically renews", + "manual_renewal" => "Manual Renewal", + "start_date" => "Start Date", "inactive" => "Disable Subscription", "replaced_with" => "Replaced with", "none" => "None", @@ -71,6 +75,7 @@ "clear_filters" => "Clear Filters", "no_matching_subscriptions" => "No matching subscriptions", "clone" => "Clone", + "renew" => "Renew", // Subscription form "add_subscription" => "Add subscription", "edit_subscription" => "Edit subscription", diff --git a/includes/i18n/es.php b/includes/i18n/es.php index ede2b4079..7be4e440f 100644 --- a/includes/i18n/es.php +++ b/includes/i18n/es.php @@ -48,6 +48,10 @@ "last_added" => "Última Añadida", "price" => "Precio", "next_payment" => "Próximo Pago", + "auto_renewal" => "Renovación Automática", + "automatically_renews" => "Renovación Automática", + "manual_renewal" => "Renovación Manual", + "start_date" => "Fecha de Inicio", "inactive" => "Desactivar Suscripción", "replaced_with" => "Reemplazada con", "none" => "Ninguna", @@ -71,6 +75,7 @@ "clear_filters" => "Limpiar Filtros", "no_matching_subscriptions" => "No hay suscripciones que coincidan con los filtros", "clone" => "Clonar", + "renew" => "Renovar", // Subscription form "add_subscription" => "Añadir suscripción", "edit_subscription" => "Editar suscripción", diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php index 5da586e88..52eb81d09 100644 --- a/includes/i18n/fr.php +++ b/includes/i18n/fr.php @@ -48,6 +48,10 @@ "last_added" => "Dernier ajouté", "price" => "Prix", "next_payment" => "Prochain paiement", + "auto_renewal" => "Renouvellement automatique", + "automatically_renews" => "Renouvellement automatique", + "manual_renewal" => "Renouvellement manuel", + "start_date" => "Date de début", "inactive" => "Désactiver l'abonnement", "replaced_with" => "Remplacé par", "none" => "Aucun", @@ -71,6 +75,7 @@ "clear_filters" => "Effacer les filtres", "no_matching_subscriptions" => "Aucun abonnement ne correspond à vos critères de recherche", "clone" => "Cloner", + "renew" => "Renouveler", // Formulaire d'abonnement "add_subscription" => "Ajouter un abonnement", "edit_subscription" => "Modifier l'abonnement", diff --git a/includes/i18n/it.php b/includes/i18n/it.php index a35731a2c..f483ea183 100644 --- a/includes/i18n/it.php +++ b/includes/i18n/it.php @@ -52,6 +52,10 @@ "last_added" => 'Ultimo aggiunto', "price" => 'Prezzo', "next_payment" => 'Prossimo pagamento', + "auto_renewal" => 'Rinnovo automatico', + "automatically_renews" => 'Si rinnova automaticamente', + "manual_renewal" => "Rinnovo manuale", + "start_date" => 'Data di inizio', "inactive" => 'Disattiva abbonamento', "replaced_with" => 'Sostituito con', "none" => 'Nessuno', @@ -75,6 +79,7 @@ "clear_filters" => 'Pulisci filtri', "no_matching_subscriptions" => 'Nessun abbonamento corrispondente', "clone" => "Clona", + "renew" => "Rinnova", // Add/Edit Subscription "add_subscription" => 'Aggiungi abbonamento', diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php index 003bfcc53..775bb2100 100644 --- a/includes/i18n/jp.php +++ b/includes/i18n/jp.php @@ -48,6 +48,10 @@ "last_added" => "最終追加日", "price" => "金額", "next_payment" => "次回支払い", + "auto_renewal" => "自動更新", + "automatically_renews" => "自動更新", + "manual_renewal" => "手動更新", + "start_date" => "開始日", "inactive" => "サブスクリプションを無効にする", "replaced_with" => "置き換えられた", "none" => "なし", @@ -71,6 +75,7 @@ "clear_filters" => "フィルタをクリア", "no_matching_subscriptions" => "一致する定期購入がありません", "clone" => "複製", + "renew" => "更新", // Subscription form "add_subscription" => "定期購入の追加", "edit_subscription" => "定期購入の編集", diff --git a/includes/i18n/ko.php b/includes/i18n/ko.php index 29cebd37c..092d7f23d 100644 --- a/includes/i18n/ko.php +++ b/includes/i18n/ko.php @@ -48,6 +48,10 @@ "last_added" => "최근 등록", "price" => "가격", "next_payment" => "다음 결제일", + "auto_renewal" => "자동 갱신", + "automatically_renews" => "자동 갱신", + "manual_renewal" => "수동 갱신", + "start_date" => "시작일", "inactive" => "구독 비활성화", "replaced_with" => "다음 구독으로 대체됨", "none" => "없음", @@ -71,6 +75,7 @@ "clear_filters" => "필터 제거", "no_matching_subscriptions" => "해당하는 구독이 없습니다.", "clone" => "복제", + "renew" => "갱신", // Subscription form "add_subscription" => "구독 추가", "edit_subscription" => "구독 편집", diff --git a/includes/i18n/pl.php b/includes/i18n/pl.php index 26bd40bc4..4541d76bd 100644 --- a/includes/i18n/pl.php +++ b/includes/i18n/pl.php @@ -48,6 +48,10 @@ "last_added" => "Ostatnio dodane", "price" => "Cena", "next_payment" => "Następna płatność", + "auto_renewal" => "Automatyczne odnawianie", + "automatically_renews" => "Automatycznie odnawia się", + "manual_renewal" => "Ręczne odnawianie", + "start_date" => "Data rozpoczęcia", "inactive" => "Wyłącz subskrypcję", "replaced_with" => "Zastąpione przez", "none" => "Brak", @@ -71,6 +75,7 @@ "clear_filters" => "Wyczyść filtry", "no_matching_subscriptions" => "Brak pasujących subskrypcji", "clone" => "Klonuj", + "renew" => "Odnów", // Subscription form "add_subscription" => "Dodaj subskrypcję", "edit_subscription" => "Edytuj subskrypcję", diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php index 2d6b6589f..dd252e059 100644 --- a/includes/i18n/pt.php +++ b/includes/i18n/pt.php @@ -48,6 +48,10 @@ "last_added" => "Última Adicionada", "price" => "Preço", "next_payment" => "Próximo Pagamento", + "auto_renewal" => "Renovação Automática", + "automatically_renews" => "Renova automaticamente", + "manual_renewal" => "Renovação Manual", + "start_date" => "Data de Início", "inactive" => "Desactivar Subscrição", "replaced_with" => "Substituída por", "none" => "Nenhuma", @@ -71,6 +75,7 @@ "clear_filters" => "Limpar Filtros", "no_matching_subscriptions" => "Sem subscrições correspondentes", "clone" => "Clonar", + "renew" => "Renovar", // Subscription form "add_subscription" => "Adicionar subscrição", "edit_subscription" => "Modificar subscrição", diff --git a/includes/i18n/pt_br.php b/includes/i18n/pt_br.php index 0bac79125..5bb6c7117 100644 --- a/includes/i18n/pt_br.php +++ b/includes/i18n/pt_br.php @@ -48,6 +48,10 @@ "last_added" => "Última adicionada", "price" => "Preço", "next_payment" => "Próximo pagamento", + "auto_renewal" => "Renovação automática", + "automatically_renews" => "Renova automaticamente", + "manual_renewal" => "Renovação manual", + "start_date" => "Data de início", "inactive" => "Assinatura inativa", "replaced_with" => "Substituída por", "none" => "Nenhuma", @@ -71,6 +75,7 @@ "clear_filters" => "Limpar filtros", "no_matching_subscriptions" => "Nenhuma assinatura encontrada", "clone" => "Clonar", + "renew" => "Renovar", // Subscription form "add_subscription" => "Adicionar assinatura", "edit_subscription" => "Editar assinatura", diff --git a/includes/i18n/ru.php b/includes/i18n/ru.php index 3c792b1f5..f2a0a57aa 100644 --- a/includes/i18n/ru.php +++ b/includes/i18n/ru.php @@ -48,6 +48,10 @@ "last_added" => "Дата создания", "price" => "Стоимость", "next_payment" => "Следующий платеж", + "auto_renewal" => "Автоматическое продление", + "automatically_renews" => "Автоматическое продление", + "manual_renewal" => "Ручное продление", + "start_date" => "Дата начала", "inactive" => "Отключить подписку", "replaced_with" => "Заменена на", "none" => "Нет", @@ -71,6 +75,7 @@ "clear_filters" => "Очистить фильтры", "no_matching_subscriptions" => "Нет подходящих подписок", "clone" => "Клонировать", + "renew" => "Продлить", // Subscription form "add_subscription" => "Добавить подписку", "edit_subscription" => "Изменить подписку", diff --git a/includes/i18n/sl.php b/includes/i18n/sl.php index eaf4cd3ee..ee889309f 100644 --- a/includes/i18n/sl.php +++ b/includes/i18n/sl.php @@ -48,6 +48,10 @@ "last_added" => "Zadnje dodano", "price" => "Cena", "next_payment" => "Naslednje plačilo", + "auto_renewal" => "Samodejno obnavljanje", + "automatically_renews" => "Se samodejno obnavlja", + "manual_renewal" => "Ročno obnavljanje", + "start_date" => "Datum začetka", "inactive" => "Onemogoči naročnino", "replaced_with" => "Zamenjano z", "none" => "brez", @@ -71,6 +75,7 @@ "clear_filters" => "Počisti filter", "no_matching_subscriptions" => "Ni ustreznih naročnin", "clone" => "Klon", + "renew" => "Obnovi", // Subscription form "add_subscription" => "Dodaj naročnino", "edit_subscription" => "Uredi naročnino", diff --git a/includes/i18n/sr.php b/includes/i18n/sr.php index d03c5bcee..4fe9f7cbe 100644 --- a/includes/i18n/sr.php +++ b/includes/i18n/sr.php @@ -48,6 +48,10 @@ "last_added" => "Последње додато", "price" => "Цена", "next_payment" => "Следећа уплата", + "auto_renewal" => "Аутоматско обновљење", + "automatically_renews" => "Аутоматски обновља", + "manual_renewal" => "Ручно обновљење", + "start_date" => "Датум почетка", "inactive" => "Онемогући претплату", "replaced_with" => "Замењено са", "none" => "Ништа", @@ -71,6 +75,7 @@ "clear_filters" => "Очисти филтере", "no_matching_subscriptions" => "Нема подударајућих претплата", "clone" => "Клонирај", + "renew" => "Обнови", // Форма за претплату "add_subscription" => "Додај претплату", "edit_subscription" => "Уреди претплату", diff --git a/includes/i18n/sr_lat.php b/includes/i18n/sr_lat.php index 66bc650b3..6ca48c574 100644 --- a/includes/i18n/sr_lat.php +++ b/includes/i18n/sr_lat.php @@ -48,6 +48,10 @@ "last_added" => "Poslednje dodato", "price" => "Cena", "next_payment" => "Sledeća uplata", + "auto_renewal" => "Automatsko obnavljanje", + "automatically_renews" => "Automatsko obnavljanje", + "manual_renewal" => "Ručno obnavljanje", + "start_date" => "Datum početka", "inactive" => "Onemogući pretplatu", "replaced_with" => "Zamenjeno sa", "none" => "Nijedna", @@ -71,6 +75,7 @@ "clear_filters" => "Očisti filtere", "no_matching_subscriptions" => "Nema podudarajućih pretplata", "clone" => "Kloniraj", + "renew" => "Obnovi", // Forma za pretplatu "add_subscription" => "Dodaj pretplatu", "edit_subscription" => "Uredi pretplatu", diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php index f8dc9bc77..5f50e3e91 100644 --- a/includes/i18n/tr.php +++ b/includes/i18n/tr.php @@ -48,6 +48,10 @@ "last_added" => "Son Eklenen", "price" => "Fiyat", "next_payment" => "Sonraki Ödeme", + "auto_renewal" => "Otomatik Yenileme", + "automatically_renews" => "Otomatik Yenileme", + "manual_renewal" => "Manuel Yenileme", + "start_date" => "Başlangıç Tarihi", "inactive" => "Aboneliği Devre Dışı Bırak", "replaced_with" => "Şununla değiştirildi", "none" => "Yok", @@ -71,6 +75,7 @@ "clear_filters" => "Filtreleri Temizle", "no_matching_subscriptions" => "Eşleşen abonelik bulunamadı", "clone" => "Kopyala", + "renew" => "Yenile", // Subscription form "add_subscription" => "Abonelik ekle", "edit_subscription" => "Aboneliği düzenle", diff --git a/includes/i18n/vi.php b/includes/i18n/vi.php index 8e7c983b7..439245c3d 100644 --- a/includes/i18n/vi.php +++ b/includes/i18n/vi.php @@ -48,6 +48,10 @@ "last_added" => "Thêm gần đây", "price" => "Giá", "next_payment" => "Thanh toán tiếp theo", + "auto_renewal" => "Tự động gia hạn", + "automatically_renews" => "Tự động gia hạn", + "manual_renewal" => "Gia hạn thủ công", + "start_date" => "Ngày bắt đầu", "inactive" => "Vô hiệu hóa đăng ký", "replaced_with" => "Thay thế bằng", "none" => "Không", @@ -71,6 +75,7 @@ "clear_filters" => "Xóa bộ lọc", "no_matching_subscriptions" => "Không có đăng ký phù hợp", "clone" => "Nhân bản", + "renew" => "Gia hạn", // Subscription form "add_subscription" => "Thêm đăng ký", "edit_subscription" => "Chỉnh sửa đăng ký", diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php index d3dd63020..20411848a 100644 --- a/includes/i18n/zh_cn.php +++ b/includes/i18n/zh_cn.php @@ -52,6 +52,10 @@ "last_added" => "创建时间", "price" => "价格", "next_payment" => "下次支付时间", + "auto_renewal" => "自动续订", + "automatically_renews" => "自动续订", + "manual_renewal" => "手动续订", + "start_date" => "开始日期", "inactive" => "停用订阅", "replaced_with" => "替换为", "none" => "无", @@ -75,6 +79,7 @@ "clear_filters" => "清除筛选", "no_matching_subscriptions" => "没有匹配的订阅", "clone" => "克隆", + "renew" => "续订", // 订阅表单 "add_subscription" => "添加订阅", diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php index 8ab6eaeab..c58fc39ba 100644 --- a/includes/i18n/zh_tw.php +++ b/includes/i18n/zh_tw.php @@ -48,6 +48,10 @@ "last_added" => "建立時間", "price" => "價格", "next_payment" => "下次付款時間", + "auto_renewal" => "自動續訂", + "automatically_renews" => "自動續訂", + "manual_renewal" => "手動續訂", + "start_date" => "開始日期", "inactive" => "停用訂閱", "replaced_with" => "替換為", "none" => "無", @@ -71,6 +75,7 @@ "clear_filters" => "清除篩選", "no_matching_subscriptions" => "沒有符合的訂閱", "clone" => "複製", + "renew" => "續訂", // 訂閱表單 "add_subscription" => "新增訂閱", "edit_subscription" => "編輯訂閱", diff --git a/includes/list_subscriptions.php b/includes/list_subscriptions.php index a9b74f394..23a271b1a 100644 --- a/includes/list_subscriptions.php +++ b/includes/list_subscriptions.php @@ -104,25 +104,43 @@ function printSubscriptions($subscriptions, $sort, $categories, $members, $i18n, if ($mobileNavigation === 'true') { ?>
- - + - + +
-
@@ -138,7 +156,17 @@ function printSubscriptions($subscriptions, $sort, $categories, $members, $i18n, ?> - + "> + + + @@ -158,10 +186,11 @@ class="original_price">(
diff --git a/index.php b/index.php index cf71f21ea..37e3cb2b0 100644 --- a/index.php +++ b/index.php @@ -356,6 +356,7 @@ $paymentMethodId = $subscription['payment_method_id']; $print[$id]['currency_code'] = $currencies[$subscription['currency_id']]['code']; $currencyId = $subscription['currency_id']; + $print[$id]['auto_renew'] = $subscription['auto_renew']; $next_payment_timestamp = strtotime($subscription['next_payment']); $formatted_date = $formatter->format($next_payment_timestamp); $print[$id]['next_payment'] = $formatted_date; @@ -490,6 +491,22 @@
+ +
+ + +
+
+ + + +
+
+
+ + +
+
diff --git a/migrations/000032.php b/migrations/000032.php new file mode 100644 index 000000000..e747d5167 --- /dev/null +++ b/migrations/000032.php @@ -0,0 +1,35 @@ +query("SELECT name FROM sqlite_master WHERE type='table' AND name='total_yearly_cost'"); +$tableRequired = $tableQuery->fetchArray(SQLITE3_ASSOC) === false; + +if ($tableRequired) { + $db->exec('CREATE TABLE total_yearly_cost ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + date INTEGER NOT NULL, + cost REAL NOT NULL, + currency TEXT NOT NULL + )'); +} + +/** @noinspection PhpUndefinedVariableInspection */ +$columnQuery = $db->query("PRAGMA table_info(subscriptions)"); +$columns = []; +while ($column = $columnQuery->fetchArray(SQLITE3_ASSOC)) { + $columns[] = $column['name']; +} + +if (!in_array('start_date', $columns)) { + $db->exec('ALTER TABLE subscriptions ADD COLUMN start_date INTEGER DEFAULT NULL'); +} + +if (!in_array('auto_renew', $columns)) { + $db->exec('ALTER TABLE subscriptions ADD COLUMN auto_renew INTEGER DEFAULT 1'); +} + +?> \ No newline at end of file diff --git a/scripts/dashboard.js b/scripts/dashboard.js index 8fb2ae122..58235e9ef 100644 --- a/scripts/dashboard.js +++ b/scripts/dashboard.js @@ -33,6 +33,10 @@ function resetForm() { logoSearchButton.classList.add("disabled"); const submitButton = document.querySelector("#save-button"); submitButton.disabled = false; + const autoRenew = document.querySelector("#auto_renew"); + autoRenew.checked = true; + const startDate = document.querySelector("#start_date"); + startDate.value = new Date().toISOString().split('T')[0]; const notifyDaysBefore = document.querySelector("#notify_days_before"); notifyDaysBefore.disabled = true; const replacementSubscriptionIdSelect = document.querySelector("#replacement_subscription_id"); @@ -78,6 +82,8 @@ function fillEditFormFields(subscription) { const payerSelect = document.querySelector("#payer_user"); payerSelect.value = subscription.payer_user_id; + const startDate = document.querySelector("#start_date"); + startDate.value = subscription.start_date; const nextPament = document.querySelector("#next_payment"); nextPament.value = subscription.next_payment; const cancellationDate = document.querySelector("#cancellation_date"); @@ -90,6 +96,11 @@ function fillEditFormFields(subscription) { const url = document.querySelector("#url"); url.value = subscription.url; + const autoRenew = document.querySelector("#auto_renew"); + if (autoRenew) { + autoRenew.checked = subscription.auto_renew; + } + const notifications = document.querySelector("#notifications"); if (notifications) { notifications.checked = subscription.notify; @@ -142,6 +153,7 @@ function openEditSubscription(event, id) { } }) .catch((error) => { + console.log(error); showErrorMessage(translate('failed_to_load_subscription')); }); } @@ -233,6 +245,33 @@ function cloneSubscription(event, id) { }); } +function renewSubscription(event, id) { + event.stopPropagation(); + event.preventDefault(); + + const url = `endpoints/subscription/renew.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) { + const id = data.id; + fetchSubscriptions(id, event, "renew"); + showSuccessMessage(decodeURI(data.message)); + } else { + showErrorMessage(data.message || translate('error')); + } + }) + .catch(error => { + showErrorMessage(error.message || translate('error')); + }); +} + function setSearchButtonStatus() { const nameInput = document.querySelector("#name"); @@ -511,7 +550,7 @@ function setSwipeElements() { let currentX = 0; let currentY = 0; let translateX = 0; - const maxTranslateX = -180; + const maxTranslateX = element.classList.contains('manual') ? -240 : -180; element.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; diff --git a/styles/styles.css b/styles/styles.css index 8277d2f71..867b7e4f3 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -384,6 +384,10 @@ button.mobile-action-clone { background-color: #2da7f3 } +button.mobile-action-renew { + background-color: #188823; +} + .subscription-container>.mobile-actions>button>svg { width: 25px; height: 25px; @@ -553,6 +557,15 @@ button.mobile-action-clone { .subscription .cycle { flex-basis: 16%; flex-grow: 1; + flex-direction: row; + align-items: center; +} + +.subscription .cycle > svg { + width: 15px; + height: 15px; + margin-right: 3px; + margin-top: 2px; } .subscription .next { @@ -1175,6 +1188,10 @@ header #avatar { display: none; } +.height50 { + height: 50px; +} + .inline-row { display: flex; flex-direction: row; From 5160071797ac78a99ad42616c1632623b20644c0 Mon Sep 17 00:00:00 2001 From: Miguel Ribeiro Date: Sun, 17 Nov 2024 16:03:47 +0100 Subject: [PATCH 3/4] fix: layout issue with subscriptions list during search --- scripts/dashboard.js | 4 ++-- styles/styles.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/dashboard.js b/scripts/dashboard.js index 58235e9ef..ea0bbf11e 100644 --- a/scripts/dashboard.js +++ b/scripts/dashboard.js @@ -518,9 +518,9 @@ function searchSubscriptions() { subscriptions.forEach(subscription => { const name = subscription.getAttribute('data-name').toLowerCase(); if (!name.includes(searchTerm)) { - subscription.classList.add("hide"); + subscription.parentElement.classList.add("hide"); } else { - subscription.classList.remove("hide"); + subscription.parentElement.classList.remove("hide"); } }); } diff --git a/styles/styles.css b/styles/styles.css index 867b7e4f3..6d1fe8c86 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -409,7 +409,7 @@ button.mobile-action-renew { transition: transform 0.2s; } -.subscription.hide { +.subscription-container.hide { display: none; } From 762c9c61b13f4538214046d1942dafc2507cdec6 Mon Sep 17 00:00:00 2001 From: Miguel Ribeiro Date: Sun, 17 Nov 2024 16:04:17 +0100 Subject: [PATCH 4/4] bump version --- includes/version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/version.php b/includes/version.php index 887077ecf..8f8f1dc36 100644 --- a/includes/version.php +++ b/includes/version.php @@ -1,3 +1,3 @@ \ No newline at end of file