diff --git a/README.md b/README.md index f7e1730a4..4b6684a89 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ See instructions to run Wallos below. ```bash 0 1 * * * php /var/www/html/endpoints/cronjobs/updatenextpayment.php >> /var/log/cron/updatenextpayment.log 2>&1 0 2 * * * php /var/www/html/endpoints/cronjobs/updateexchange.php >> /var/log/cron/updateexchange.log 2>&1 +0 8 * * * php /var/www/html/endpoints/cronjobs/sendcancellationnotifications.php >> /var/log/cron/sendcancellationnotifications.log 2>&1 0 9 * * * php /var/www/html/endpoints/cronjobs/sendnotifications.php >> /var/log/cron/sendnotifications.log 2>&1 */2 * * * * php /var/www/html/endpoints/cronjobs/sendverificationemails.php >> /var/log/cron/sendverificationemail.log 2>&1 */2 * * * * php /var/www/html/endpoints/cronjobs/sendresetpasswordemails.php >> /var/log/cron/sendresetpasswordemails.log 2>&1 @@ -138,6 +139,8 @@ If you want to trigger an Update of the exchange rates, change your main currenc ![Screenshot](screenshots/wallos-stats.png) +![Screenshot](screenshots/wallos-calendar.png) + ![Screenshot](screenshots/wallos-form.png) ![Screenshot](screenshots/wallos-dashboard-mobile-light.png) ![Screenshot](screenshots/wallos-dashboard-mobile-dark.png) diff --git a/cronjobs b/cronjobs index 500cc7b6f..fe9372b13 100644 --- a/cronjobs +++ b/cronjobs @@ -1,6 +1,7 @@ # Run the scripts every day 0 1 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/updatenextpayment.php >> /var/log/cron/updatenextpayment.log 2>&1 0 2 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/updateexchange.php >> /var/log/cron/updateexchange.log 2>&1 +0 8 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendcancellationnotifications.php >> /var/log/cron/sendcancellationnotifications.log 2>&1 0 9 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendnotifications.php >> /var/log/cron/sendnotifications.log 2>&1 */2 * * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendverificationemails.php >> /var/log/cron/sendverificationemails.log 2>&1 */2 * * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/sendresetpasswordemails.php >> /var/log/cron/sendresetpasswordemails.log 2>&1 diff --git a/endpoints/cronjobs/sendcancellationnotifications.php b/endpoints/cronjobs/sendcancellationnotifications.php new file mode 100644 index 000000000..c24350871 --- /dev/null +++ b/endpoints/cronjobs/sendcancellationnotifications.php @@ -0,0 +1,470 @@ +prepare($query); +$usersToNotify = $stmt->execute(); + +while ($userToNotify = $usersToNotify->fetchArray(SQLITE3_ASSOC)) { + $userId = $userToNotify['id']; + echo "For user: " . $userToNotify['username'] . "
"; + + $emailNotificationsEnabled = false; + $gotifyNotificationsEnabled = false; + $telegramNotificationsEnabled = false; + $pushoverNotificationsEnabled = false; + $discordNotificationsEnabled = false; + $ntfyNotificationsEnabled = false; + + // Check if email notifications are enabled and get the settings + $query = "SELECT * FROM email_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $emailNotificationsEnabled = $row['enabled']; + $email['smtpAddress'] = $row["smtp_address"]; + $email['smtpPort'] = $row["smtp_port"]; + $email['encryption'] = $row["encryption"]; + $email['smtpUsername'] = $row["smtp_username"]; + $email['smtpPassword'] = $row["smtp_password"]; + $email['fromEmail'] = $row["from_email"] ? $row["from_email"] : "wallos@wallosapp.com"; + } + + // Check if Discord notifications are enabled and get the settings + $query = "SELECT * FROM discord_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $discordNotificationsEnabled = $row['enabled']; + $discord['webhook_url'] = $row["webhook_url"]; + $discord['bot_username'] = $row["bot_username"]; + $discord['bot_avatar_url'] = $row["bot_avatar_url"]; + } + + // Check if Gotify notifications are enabled and get the settings + $query = "SELECT * FROM gotify_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + $gotify = []; + + if ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $gotifyNotificationsEnabled = $row['enabled']; + $gotify['serverUrl'] = $row["url"]; + $gotify['appToken'] = $row["token"]; + } + + // Check if Telegram notifications are enabled and get the settings + $query = "SELECT * FROM telegram_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $telegramNotificationsEnabled = $row['enabled']; + $telegram['botToken'] = $row["bot_token"]; + $telegram['chatId'] = $row["chat_id"]; + } + + // Check if Pushover notifications are enabled and get the settings + $query = "SELECT * FROM pushover_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $pushoverNotificationsEnabled = $row['enabled']; + $pushover['user_key'] = $row["user_key"]; + $pushover['token'] = $row["token"]; + } + + // Check if Ntfy notifications are enabled and get the settings + $query = "SELECT * FROM ntfy_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $ntfyNotificationsEnabled = $row['enabled']; + $ntfy['host'] = $row["host"]; + $ntfy['topic'] = $row["topic"]; + $ntfy['headers'] = $row["headers"]; + } + + $notificationsEnabled = $emailNotificationsEnabled || $gotifyNotificationsEnabled || $telegramNotificationsEnabled || + $pushoverNotificationsEnabled || $discordNotificationsEnabled ||$ntfyNotificationsEnabled; + + // If no notifications are enabled, no need to run + if (!$notificationsEnabled) { + echo "Notifications are disabled. No need to run.
"; + continue; + } else { + // Get all currencies + $currencies = array(); + $query = "SELECT * FROM currencies WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $currencies[$row['id']] = $row; + } + + // Get all household members + $query = "SELECT * FROM household WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $resultHousehold = $stmt->execute(); + + $household = []; + while ($rowHousehold = $resultHousehold->fetchArray(SQLITE3_ASSOC)) { + $household[$rowHousehold['id']] = $rowHousehold; + } + + // Get all categories + $query = "SELECT * FROM categories WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $resultCategories = $stmt->execute(); + + $categories = []; + while ($rowCategory = $resultCategories->fetchArray(SQLITE3_ASSOC)) { + $categories[$rowCategory['id']] = $rowCategory; + } + + // Get current date to check which subscriptions are set to notify for cancellation + $currentDate = new DateTime('now'); + $currentDate = $currentDate->format('Y-m-d'); + + $query = "SELECT * FROM subscriptions WHERE user_id = :user_id AND inactive = :inactive AND cancellation_date = :cancellationDate ORDER BY payer_user_id ASC"; + $stmt = $db->prepare($query); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $stmt->bindValue(':inactive', 0, SQLITE3_INTEGER); + $stmt->bindValue(':cancellationDate', $currentDate, SQLITE3_TEXT); + $resultSubscriptions = $stmt->execute(); + + $notify = []; + $i = 0; + $currentDate = new DateTime('now'); + while ($rowSubscription = $resultSubscriptions->fetchArray(SQLITE3_ASSOC)) { + $notify[$rowSubscription['payer_user_id']][$i]['name'] = $rowSubscription['name']; + $notify[$rowSubscription['payer_user_id']][$i]['price'] = $rowSubscription['price'] . $currencies[$rowSubscription['currency_id']]['symbol']; + $notify[$rowSubscription['payer_user_id']][$i]['currency'] = $currencies[$rowSubscription['currency_id']]['name']; + $notify[$rowSubscription['payer_user_id']][$i]['category'] = $categories[$rowSubscription['category_id']]['name']; + $notify[$rowSubscription['payer_user_id']][$i]['payer'] = $household[$rowSubscription['payer_user_id']]['name']; + $notify[$rowSubscription['payer_user_id']][$i]['date'] = $rowSubscription['next_payment']; + $i++; + } + + if (!empty($notify)) { + + // Email notifications if enabled + if ($emailNotificationsEnabled) { + + $stmt = $db->prepare('SELECT * FROM user WHERE id = :user_id'); + $stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $defaultUser = $result->fetchArray(SQLITE3_ASSOC); + $defaultEmail = $defaultUser['email']; + $defaultName = $defaultUser['username']; + + foreach ($notify as $userId => $perUser) { + $message = "The following subscriptions are up for cancellation:\n"; + + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] ."\n"; + } + + $mail = new PHPMailer(true); + $mail->CharSet = "UTF-8"; + $mail->isSMTP(); + + $mail->Host = $email['smtpAddress']; + $mail->SMTPAuth = true; + $mail->Username = $email['smtpUsername']; + $mail->Password = $email['smtpPassword']; + $mail->SMTPSecure = $email['encryption']; + $mail->Port = $email['smtpPort']; + + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + $emailaddress = !empty($user['email']) ? $user['email'] : $defaultEmail; + $name = !empty($user['name']) ? $user['name'] : $defaultName; + + $mail->setFrom($email['fromEmail'], 'Wallos App'); + $mail->addAddress($emailaddress, $name); + + $mail->Subject = 'Wallos Cancellation Notification'; + $mail->Body = $message; + + if ($mail->send()) { + echo "Email Notifications sent
"; + } else { + echo "Error sending notifications: " . $mail->ErrorInfo . "
"; + } + } + } + + // Discord notifications if enabled + if ($discordNotificationsEnabled) { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + $title = translate('wallos_notification', $i18n); + + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } + + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } + + $postfields = [ + 'content' => $message + ]; + + if (!empty($discord['bot_username'])) { + $postfields['username'] = $discord['bot_username']; + } + + if (!empty($discord['bot_avatar_url'])) { + $postfields['avatar_url'] = $discord['bot_avatar_url']; + } + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $discord['webhook_url']); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postfields)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json' + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + curl_close($ch); + + if ($result === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Discord Notifications sent
"; + } + } + } + + // Gotify notifications if enabled + if ($gotifyNotificationsEnabled) { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } + + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } + + $data = array( + 'message' => $message, + 'priority' => 5 + ); + + $data_string = json_encode($data); + + $ch = curl_init($gotify['serverUrl'] . '/message?token=' . $gotify['appToken']); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data_string) + ) + ); + + $result = curl_exec($ch); + if ($result === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Gotify Notifications sent
"; + } + } + } + + // Telegram notifications if enabled + if ($telegramNotificationsEnabled) { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } + + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } + + $data = array( + 'chat_id' => $telegram['chatId'], + 'text' => $message + ); + + $data_string = json_encode($data); + + $ch = curl_init('https://api.telegram.org/bot' . $telegram['botToken'] . '/sendMessage'); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data_string) + ) + ); + + $result = curl_exec($ch); + if ($result === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Telegram Notifications sent
"; + } + } + } + + // Pushover notifications if enabled + if ($pushoverNotificationsEnabled) { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } + + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "https://api.pushover.net/1/messages.json"); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'token' => $pushover['token'], + 'user' => $pushover['user_key'], + 'message' => $message, + ])); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $result = curl_exec($ch); + + curl_close($ch); + + if ($result === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Pushover Notifications sent
"; + } + } + } + + // Ntfy notifications if enabled + if ($ntfyNotificationsEnabled) { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for cancellation:\n"; + } else { + $message = "The following subscriptions are up for cancellation:\n"; + } + + foreach ($perUser as $subscription) { + $message .= $subscription['name'] . " for " . $subscription['price'] . "\n"; + } + + $headers = json_decode($ntfy["headers"], true); + $customheaders = array_map(function ($key, $value) { + return "$key: $value"; + }, array_keys($headers), $headers); + + $ch = curl_init(); + + $ntfyHost = rtrim($ntfy["host"], '/'); + $ntfyTopic = $ntfy['topic']; + + curl_setopt($ch, CURLOPT_URL, $ntfyHost . '/' . $ntfyTopic); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + curl_close($ch); + + if ($response === false) { + echo "Error sending notifications: " . curl_error($ch) . "
"; + } else { + echo "Ntfy Notifications sent
"; + } + } + } + + } else { + echo "Nothing to notify.
"; + } + + } + +} + +?> \ No newline at end of file diff --git a/endpoints/cronjobs/sendnotifications.php b/endpoints/cronjobs/sendnotifications.php index 3471c8b8e..8b9153ff0 100644 --- a/endpoints/cronjobs/sendnotifications.php +++ b/endpoints/cronjobs/sendnotifications.php @@ -105,7 +105,7 @@ $pushover['token'] = $row["token"]; } - // Check if Nrfy notifications are enabled and get the settings + // Check if Ntfy notifications are enabled and get the settings $query = "SELECT * FROM ntfy_notifications WHERE user_id = :userId"; $stmt = $db->prepare($query); $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); diff --git a/endpoints/subscription/add.php b/endpoints/subscription/add.php index 222e80103..0715e5076 100644 --- a/endpoints/subscription/add.php +++ b/endpoints/subscription/add.php @@ -172,6 +172,7 @@ function resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings) $notify = isset($_POST['notifications']) ? true : false; $notifyDaysBefore = $_POST['notify_days_before']; $inactive = isset($_POST['inactive']) ? true : false; + $cancellationDate = $_POST['cancellation_date'] ?? null; if ($logoUrl !== "") { $logo = getLogoFromUrl($logoUrl, '../../images/uploads/logos/', $name, $settings, $i18n); @@ -188,21 +189,21 @@ function resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings) if (!$isEdit) { $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) + payment_method_id, payer_user_id, category_id, notify, inactive, url, notify_days_before, user_id, cancellation_date) VALUES (:name, :logo, :price, :currencyId, :nextPayment, :cycle, :frequency, :notes, - :paymentMethodId, :payerUserId, :categoryId, :notify, :inactive, :url, :notifyDaysBefore, :userId)"; + :paymentMethodId, :payerUserId, :categoryId, :notify, :inactive, :url, :notifyDaysBefore, :userId, :cancellationDate)"; } else { $id = $_POST['id']; if ($logo != "") { $sql = "UPDATE subscriptions SET name = :name, logo = :logo, price = :price, currency_id = :currencyId, next_payment = :nextPayment, cycle = :cycle, frequency = :frequency, notes = :notes, payment_method_id = :paymentMethodId, payer_user_id = :payerUserId, category_id = :categoryId, notify = :notify, inactive = :inactive, - url = :url, notify_days_before = :notifyDaysBefore WHERE id = :id AND user_id = :userId"; + url = :url, notify_days_before = :notifyDaysBefore, camcelation_date = :cancellationDate WHERE id = :id AND user_id = :userId"; } else { $sql = "UPDATE subscriptions SET name = :name, price = :price, currency_id = :currencyId, next_payment = :nextPayment, cycle = :cycle, frequency = :frequency, notes = :notes, payment_method_id = :paymentMethodId, payer_user_id = :payerUserId, - category_id = :categoryId, notify = :notify, inactive = :inactive, url = :url,notify_days_before = :notifyDaysBefore - WHERE id = :id AND user_id = :userId"; + category_id = :categoryId, notify = :notify, inactive = :inactive, url = :url, notify_days_before = :notifyDaysBefore, + cancellation_date = :cancellationDate WHERE id = :id AND user_id = :userId"; } } @@ -228,6 +229,7 @@ function resizeAndUploadLogo($uploadedFile, $uploadDir, $name, $settings) $stmt->bindParam(':url', $url, SQLITE3_TEXT); $stmt->bindParam(':notifyDaysBefore', $notifyDaysBefore, SQLITE3_INTEGER); $stmt->bindParam(':userId', $userId, SQLITE3_INTEGER); + $stmt->bindParam(':cancellationDate', $cancellationDate, SQLITE3_TEXT); if ($stmt->execute()) { $success['status'] = "Success"; diff --git a/endpoints/subscription/clone.php b/endpoints/subscription/clone.php index bb9682281..35282bb14 100644 --- a/endpoints/subscription/clone.php +++ b/endpoints/subscription/clone.php @@ -17,7 +17,7 @@ ])); } - $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)"; + $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, cancellation_date) 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, :cancellation_date)"; $cloneStmt = $db->prepare($query); $cloneStmt->bindValue(':name', $subscriptionToClone['name'], SQLITE3_TEXT); $cloneStmt->bindValue(':logo', $subscriptionToClone['logo'], SQLITE3_TEXT); @@ -35,6 +35,7 @@ $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); + $cloneStmt->bindValue(':cancellation_date', $subscriptionToClone['cancellation_date'], SQLITE3_TEXT); if ($cloneStmt->execute()) { $response = [ diff --git a/endpoints/subscription/get.php b/endpoints/subscription/get.php index f9e2134ba..1ab31d722 100644 --- a/endpoints/subscription/get.php +++ b/endpoints/subscription/get.php @@ -29,6 +29,7 @@ $subscriptionData['inactive'] = $row['inactive']; $subscriptionData['url'] = htmlspecialchars_decode($row['url'] ?? ""); $subscriptionData['notify_days_before'] = $row['notify_days_before']; + $subscriptionData['cancellation_date'] = $row['cancellation_date']; $subscriptionJson = json_encode($subscriptionData); header('Content-Type: application/json'); diff --git a/includes/i18n/de.php b/includes/i18n/de.php index 811f989a0..aeb1e0567 100644 --- a/includes/i18n/de.php +++ b/includes/i18n/de.php @@ -83,6 +83,7 @@ "notes" => "Notizen", "enable_notifications" => "Benachrichtigungen für dieses Abonnement aktivieren", "default_value_from_settings" => "Standardwert aus den Einstellungen", + "cancellation_notification" => "Benachrichtigung bei Kündigung", "delete" => "Löschen", "cancel" => "Abbrechen", "upload_logo" => "Logo hochladen", diff --git a/includes/i18n/el.php b/includes/i18n/el.php index 969298490..8a123abaa 100644 --- a/includes/i18n/el.php +++ b/includes/i18n/el.php @@ -83,6 +83,7 @@ "notes" => "Σημειώσεις", "enable_notifications" => "Ενεργοποίηση ειδοποιήσεων για αυτή τη συνδρομή", "default_value_from_settings" => "Προεπιλεγμένη τιμή από τις ρυθμίσεις", + "cancellation_notification" => "Ειδοποίηση ακύρωσης", "delete" => "Διαγραφή", "cancel" => "Ακύρωση", "upload_logo" => "Φόρτωση λογότυπου", diff --git a/includes/i18n/en.php b/includes/i18n/en.php index c38d4b671..8e53989a6 100644 --- a/includes/i18n/en.php +++ b/includes/i18n/en.php @@ -83,6 +83,7 @@ "notes" => "Notes", "enable_notifications" => "Enable Notifications for this subscription", "default_value_from_settings" => "Default value from settings", + "cancellation_notification" => "cancellation Notification", "delete" => "Delete", "cancel" => "Cancel", "upload_logo" => "Upload Logo", diff --git a/includes/i18n/es.php b/includes/i18n/es.php index aba72ebfd..d0267c9bb 100644 --- a/includes/i18n/es.php +++ b/includes/i18n/es.php @@ -83,6 +83,7 @@ "notes" => "Notas", "enable_notifications" => "Habilitar notificaciones para esta suscripción", "default_value_from_settings" => "Valor predeterminado de la configuración", + "cancellation_notification" => "Notificación de cancelación", "delete" => "Eliminar", "cancel" => "Cancelar", "upload_logo" => "Cargar Logotipo", diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php index 4295ac06e..307e50bde 100644 --- a/includes/i18n/fr.php +++ b/includes/i18n/fr.php @@ -83,6 +83,7 @@ "notes" => "Notes", "enable_notifications" => "Activer les notifications pour cet abonnement", "default_value_from_settings" => "Valeur par défaut des paramètres", + "cancellation_notification" => "Notification d'annulation", "delete" => "Supprimer", "cancel" => "Annuler", "upload_logo" => "Télécharger le logo", diff --git a/includes/i18n/it.php b/includes/i18n/it.php index 362208c7b..5b76206a3 100644 --- a/includes/i18n/it.php +++ b/includes/i18n/it.php @@ -88,6 +88,7 @@ 'notes' => 'Note', 'enable_notifications' => 'Abilita notifiche per questo abbonamento', 'default_value_from_settings' => 'Valore predefinito dalle impostazioni', + "cancellation_notification" => "Notifica di cancellazione", 'delete' => 'Cancella', 'cancel' => 'Annulla', 'upload_logo' => 'Carica logo', diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php index 5a313972a..138adad51 100644 --- a/includes/i18n/jp.php +++ b/includes/i18n/jp.php @@ -83,6 +83,7 @@ "notes" => "注釈", "enable_notifications" => "この定期購入の通知を有効にする", "default_value_from_settings" => "設定からデフォルト値を使用", + "cancellation_notification" => "キャンセル通知", "delete" => "削除", "cancel" => "キャンセル", "upload_logo" => "ロゴのアップロード", diff --git a/includes/i18n/ko.php b/includes/i18n/ko.php index ee3365ff5..cbed40456 100644 --- a/includes/i18n/ko.php +++ b/includes/i18n/ko.php @@ -83,6 +83,7 @@ "notes" => "메모", "enable_notifications" => "이 구독에 대한 알림을 활성화합니다.", "default_value_from_settings" => "설정에서 기본값 사용", + "cancellation_notification" => "구독 취소 알림", "delete" => "삭제", "cancel" => "취소", "upload_logo" => "로고 업로드", diff --git a/includes/i18n/pl.php b/includes/i18n/pl.php index 5873c2d5d..d34c5614a 100644 --- a/includes/i18n/pl.php +++ b/includes/i18n/pl.php @@ -83,6 +83,7 @@ "notes" => "Notatki", "enable_notifications" => "Włącz powiadomienia dla tej subskrypcji", "default_value_from_settings" => "Wartość domyślna z ustawień", + "cancellation_notification" => "Powiadomienie o anulowaniu", "delete" => "Usuń", "cancel" => "Anuluj", "upload_logo" => "Prześlij logo", diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php index f1e265490..09b1fd022 100644 --- a/includes/i18n/pt.php +++ b/includes/i18n/pt.php @@ -83,6 +83,7 @@ "notes" => "Notas", "enable_notifications" => "Activar notificações para esta subscrição", "default_value_from_settings" => "Valor por defeito das definições", + "cancellation_notification" => "Notificação de cancelamento", "delete" => "Remover", "cancel" => "Cancelar", "upload_logo" => "Enviar Logo", diff --git a/includes/i18n/pt_br.php b/includes/i18n/pt_br.php index fe57578c8..e1a3f5d92 100644 --- a/includes/i18n/pt_br.php +++ b/includes/i18n/pt_br.php @@ -83,6 +83,7 @@ "notes" => "Anotações", "enable_notifications" => "Ativar notificações para essa assinatura", "default_value_from_settings" => "Valor padrão das configurações", + "cancellation_notification" => "Notificação de cancelamento", "delete" => "Excluir", "cancel" => "Cancelar", "upload_logo" => "Enviar Logo", diff --git a/includes/i18n/ru.php b/includes/i18n/ru.php index 02bbdec24..d84bff6d0 100644 --- a/includes/i18n/ru.php +++ b/includes/i18n/ru.php @@ -83,6 +83,7 @@ "notes" => "Примечания", "enable_notifications" => "Включить уведомления для этой подписки", "default_value_from_settings" => "Значение по умолчанию из настроек", + "cancellation_notification" => "Уведомление об отмене", "delete" => "Удалить", "cancel" => "Отмена", "upload_logo" => "Загрузить логотип", diff --git a/includes/i18n/sl.php b/includes/i18n/sl.php index 322bc7474..e11e2888d 100644 --- a/includes/i18n/sl.php +++ b/includes/i18n/sl.php @@ -83,6 +83,7 @@ "notes" => "Opombe", "enable_notifications" => "Omogoči obvestila za to naročnino", "default_value_from_settings" => "Privzeta vrednost iz nastavitev", + "cancellation_notification" => "Obvestilo o preklicu", "delete" => "Izbriši", "cancel" => "Prekliči", "upload_logo" => "Naloži logotip", diff --git a/includes/i18n/sr.php b/includes/i18n/sr.php index 4149c8501..0c0a0626a 100644 --- a/includes/i18n/sr.php +++ b/includes/i18n/sr.php @@ -82,6 +82,8 @@ "url" => "URL", "notes" => "Напомене", "enable_notifications" => "Омогући обавештења за ову претплату", + "default_value_from_settings" => "Подразумевана вредност из подешавања", + "cancellation_notification" => "Обавештење о отказивању", "delete" => "Обриши", "cancel" => "Откажи", "upload_logo" => "Постави логотип", diff --git a/includes/i18n/sr_lat.php b/includes/i18n/sr_lat.php index 79456b0bc..78b2ab0da 100644 --- a/includes/i18n/sr_lat.php +++ b/includes/i18n/sr_lat.php @@ -83,6 +83,7 @@ "notes" => "Beleške", "enable_notifications" => "Omogući obaveštenja za ovu pretplatu", "default_value_from_settings" => "Podrazumevana vrednost iz podešavanja", + "cancellation_notification" => "Obaveštenje o otkazivanju", "delete" => "Izbriši", "cancel" => "Otkaži", "upload_logo" => "Učitaj logo", diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php index 91243a86c..433ec439f 100644 --- a/includes/i18n/tr.php +++ b/includes/i18n/tr.php @@ -83,6 +83,7 @@ "notes" => "Notlar", "enable_notifications" => "Bu abonelik için bildirimleri etkinleştir", "default_value_from_settings" => "Ayarlar'dan varsayılan değeri al", + "cancellation_notification" => "İptal Bildirimi", "delete" => "Sil", "cancel" => "İptal", "upload_logo" => "Logo Yükle", diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php index 1111a8899..86c4a0df6 100644 --- a/includes/i18n/zh_cn.php +++ b/includes/i18n/zh_cn.php @@ -88,6 +88,7 @@ "notes" => "备注", "enable_notifications" => "为此订阅启用通知", "default_value_from_settings" => "默认值从设置中获取", + "cancellation_notification" => "取消通知", "delete" => "删除", "cancel" => "取消", "upload_logo" => "上传 Logo", diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php index 2ad410e71..7a7a63cc8 100644 --- a/includes/i18n/zh_tw.php +++ b/includes/i18n/zh_tw.php @@ -83,6 +83,7 @@ "notes" => "備註", "enable_notifications" => "為該訂閱開啟通知", "default_value_from_settings" => "從設定中取得預設值", + "cancellation_notification" => "取消通知", "delete" => "刪除", "cancel" => "取消", "upload_logo" => "上傳圖示", diff --git a/includes/version.php b/includes/version.php index d35e05d96..e360ce324 100644 --- a/includes/version.php +++ b/includes/version.php @@ -1,3 +1,3 @@ \ No newline at end of file diff --git a/index.php b/index.php index 6b9770d29..36004c81d 100644 --- a/index.php +++ b/index.php @@ -93,7 +93,8 @@ } ?>
-
+ + @@ -116,7 +117,8 @@ } ?>
-
+ + @@ -141,11 +143,14 @@
-
- - -
- -
- - -
-
- - +
+
+ + +
+
+ + +
+
@@ -347,18 +349,32 @@
+
+ + +
- - +
+
+ + +
+
+ + +
+
diff --git a/migrations/000024.php b/migrations/000024.php new file mode 100644 index 000000000..72bd31408 --- /dev/null +++ b/migrations/000024.php @@ -0,0 +1,10 @@ +query("SELECT * FROM pragma_table_info('subscriptions') where name='cancellation_date'"); +$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false; + +if ($columnRequired) { + $db->exec('ALTER TABLE subscriptions ADD COLUMN cancellation_date DATE;'); +} \ No newline at end of file diff --git a/screenshots/wallos-calendar.png b/screenshots/wallos-calendar.png new file mode 100644 index 000000000..8b837abc4 Binary files /dev/null and b/screenshots/wallos-calendar.png differ diff --git a/scripts/dashboard.js b/scripts/dashboard.js index c69ba4c3b..4409301c2 100644 --- a/scripts/dashboard.js +++ b/scripts/dashboard.js @@ -74,6 +74,9 @@ function fillEditFormFields(subscription) { const nextPament = document.querySelector("#next_payment"); nextPament.value = subscription.next_payment; + const cancellationDate = document.querySelector("#cancellation_date"); + cancellationDate.value = subscription.cancellation_date; + const notes = document.querySelector("#notes"); notes.value = subscription.notes; const inactive = document.querySelector("#inactive"); diff --git a/styles/dark-theme.css b/styles/dark-theme.css index dd6e53568..044fa5fa9 100644 --- a/styles/dark-theme.css +++ b/styles/dark-theme.css @@ -228,4 +228,8 @@ input[type="radio"]:disabled + label::before { background-color: #333; border-color: #222; cursor: not-allowed; +} + +input { + color-scheme: dark; } \ No newline at end of file diff --git a/styles/styles.css b/styles/styles.css index 3117eacd6..eb40b6376 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -1006,6 +1006,20 @@ header #avatar { flex-basis: 66.66%; } +.form-group .inline .split50 { + flex-basis: 50%; +} + +@media (max-width: 768px) { + .form-group .inline .mobile-split-50 { + flex-basis: 50%; + } + + select#frequency { + width: 100px; + padding: 0px 10px; + } +} label { display: block;