feat: add payment cycle to csv/json export

feat: run db migration after restoring database
feat: run db migration after importing db
feat: store weekly the total yearly cost of subscriptions
fix: double encoding in statistics labels
This commit is contained in:
Miguel Ribeiro
2024-12-12 00:09:51 +01:00
committed by GitHub
parent 66ad4d8ecd
commit 5e6bc903bc
10 changed files with 224 additions and 118 deletions

View File

@@ -1,4 +1,4 @@
# Use the php:8.0.5-fpm-alpine base image
# Use the php:8.2-fpm-alpine base image
FROM php:8.2-fpm-alpine
# Set working directory to /var/www/html

View File

@@ -98,6 +98,7 @@ See instructions to run Wallos below.
*/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
0 */6 * * * php /var/www/html/endpoints/cronjobs/checkforupdates.php >> /var/log/cron/checkforupdates.log 2>&1
30 1 * * 1 php /var/www/html/endpoints/cronjobs/storetotalyearlycost.php >> /var/log/cron/storetotalyearlycost.log 2>&1
```
5. If your web root is not `/var/www/html/` adjust the cronjobs above accordingly.

View File

@@ -362,6 +362,7 @@ $loginDisabledAllowed = $userCount == 1 && $settings['registrations_open'] == 0;
<input type="button" value="Send Verification Emails" class="button tiny mobile-grow" onclick="executeCronJob('sendverificationemails')">
<input type="button" value="Update Exchange Rates" class="button tiny mobile-grow" onclick="executeCronJob('updateexchange')">
<input type="button" value="Update Next Payments" class="button tiny mobile-grow" onclick="executeCronJob('updatenextpayment')">
<input type="button" value="Store Total Yearly Cost" class="button tiny mobile-grow" onclick="executeCronJob('storetotalyearlycost')">
</div>
<div class="inline-row">
<textarea id="cronjobResult" class="thin" readonly></textarea>

View File

@@ -6,3 +6,4 @@
*/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
0 */6 * * * /usr/local/bin/php /var/www/html/endpoints/cronjobs/checkforupdates.php >> /var/log/cron/checkforupdates.log 2>&1
30 1 * * 1 /usr/local/bin/php /var/www/html/endpoints/cronjobs/storetotalyearlycost.php >> /var/log/cron/storetotalyearlycost.log 2>&1

View File

@@ -0,0 +1,72 @@
<?php
require_once __DIR__ . '/../../includes/connect_endpoint_crontabs.php';
if (php_sapi_name() == 'cli') {
$date = new DateTime('now');
echo "\n" . $date->format('Y-m-d') . " " . $date->format('H:i:s') . "<br />\n";
}
$currentDate = new DateTime();
$currentDateString = $currentDate->format('Y-m-d');
function getPriceConverted($price, $currency, $database, $userId)
{
$query = "SELECT rate FROM currencies WHERE id = :currency AND user_id = :userId";
$stmt = $database->prepare($query);
$stmt->bindParam(':currency', $currency, SQLITE3_INTEGER);
$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$exchangeRate = $result->fetchArray(SQLITE3_ASSOC);
if ($exchangeRate === false) {
return $price;
} else {
$fromRate = $exchangeRate['rate'];
return $price / $fromRate;
}
}
// Get all users
$query = "SELECT id, main_currency FROM user";
$stmt = $db->prepare($query);
$result = $stmt->execute();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$userId = $row['id'];
$userCurrencyId = $row['main_currency'];
$totalYearlyCost = 0;
$query = "SELECT * FROM subscriptions WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$resultSubscriptions = $stmt->execute();
while ($rowSubscriptions = $resultSubscriptions->fetchArray(SQLITE3_ASSOC)) {
$price = getPriceConverted($rowSubscriptions['price'], $rowSubscriptions['currency_id'], $db, $userId);
$totalYearlyCost += $price;
}
$query = "INSERT INTO total_yearly_cost (user_id, date, cost, currency) VALUES (:userId, :date, :cost, :currency)";
$stmt = $db->prepare($query);
$stmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$stmt->bindParam(':date', $currentDateString, SQLITE3_TEXT);
$stmt->bindParam(':cost', $totalYearlyCost, SQLITE3_FLOAT);
$stmt->bindParam(':currency', $userCurrencyId, SQLITE3_INTEGER);
if ($stmt->execute()) {
echo "Inserted total yearly cost for user " . $userId . " with cost " . $totalYearlyCost . "<br />\n";
} else {
echo "Error inserting total yearly cost for user " . $userId . "<br />\n";
}
}
?>

View File

@@ -18,8 +18,25 @@ $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$cycle = $cycles[$row['cycle']]['name'];
$frequency =$row['frequency'];
$cyclesMap = array(
'Daily' => 'Days',
'Weekly' => 'Weeks',
'Monthly' => 'Months',
'Yearly' => 'Years'
);
if ($frequency == 1) {
$cyclePrint = $cycle;
} else {
$cyclePrint = "Every " . $frequency . " " . $cyclesMap[$cycle];
}
$subscriptionDetails = array(
'Name' => str_replace(',', ' ', $row['name']),
'Payment Cycle' => $cyclePrint,
'Next Payment' => $row['next_payment'],
'Renewal' => $row['auto_renew'] ? 'Automatic' : 'Manual',
'Category' => str_replace(',', ' ', $categories[$row['category_id']]['name']),

View File

@@ -1,3 +1,3 @@
<?php
$version = "v2.40.0";
$version = "v2.41.0";
?>

View File

@@ -148,8 +148,15 @@ function restoreDB() {
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message)
window.location.href = 'logout.php';
showSuccessMessage(data.message);
fetch('endpoints/db/migrate.php')
.then(response => response.text())
.then(() => {
window.location.href = 'logout.php';
})
.catch(error => {
window.location.href = 'logout.php';
});
} else {
showErrorMessage(data.message);
}

View File

@@ -1,155 +1,162 @@
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + value + expires + "; SameSite=Strict";
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + value + expires + "; SameSite=Strict";
}
function storeFormFieldValue(fieldId) {
var fieldElement = document.getElementById(fieldId);
if (fieldElement) {
localStorage.setItem(fieldId, fieldElement.value);
}
var fieldElement = document.getElementById(fieldId);
if (fieldElement) {
localStorage.setItem(fieldId, fieldElement.value);
}
}
function storeFormFields() {
storeFormFieldValue('username');
storeFormFieldValue('email');
storeFormFieldValue('password');
storeFormFieldValue('confirm_password');
storeFormFieldValue('currency');
storeFormFieldValue('username');
storeFormFieldValue('email');
storeFormFieldValue('password');
storeFormFieldValue('confirm_password');
storeFormFieldValue('currency');
}
function restoreFormFieldValue(fieldId) {
var fieldElement = document.getElementById(fieldId);
if (localStorage.getItem(fieldId)) {
fieldElement.value = localStorage.getItem(fieldId) || '';
}
var fieldElement = document.getElementById(fieldId);
if (localStorage.getItem(fieldId)) {
fieldElement.value = localStorage.getItem(fieldId) || '';
}
}
function restoreFormFields() {
restoreFormFieldValue('username');
restoreFormFieldValue('email');
restoreFormFieldValue('password');
restoreFormFieldValue('confirm_password');
restoreFormFieldValue('currency');
restoreFormFieldValue('username');
restoreFormFieldValue('email');
restoreFormFieldValue('password');
restoreFormFieldValue('confirm_password');
restoreFormFieldValue('currency');
}
function removeFromStorage() {
localStorage.removeItem('username');
localStorage.removeItem('email');
localStorage.removeItem('password');
localStorage.removeItem('confirm_password');
localStorage.removeItem('currency');
localStorage.removeItem('username');
localStorage.removeItem('email');
localStorage.removeItem('password');
localStorage.removeItem('confirm_password');
localStorage.removeItem('currency');
}
function changeLanguage(selectedLanguage) {
storeFormFields();
setCookie("language", selectedLanguage, 365);
location.reload();
storeFormFields();
setCookie("language", selectedLanguage, 365);
location.reload();
}
function runDatabaseMigration() {
let url = "endpoints/db/migrate.php";
fetch(url)
let url = "endpoints/db/migrate.php";
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(translate('network_response_error'));
}
if (!response.ok) {
throw new Error(translate('network_response_error'));
}
});
}
function showErrorMessage(message) {
const toast = document.querySelector(".toast#errorToast");
(closeIcon = document.querySelector(".close-error")),
const toast = document.querySelector(".toast#errorToast");
(closeIcon = document.querySelector(".close-error")),
(errorMessage = document.querySelector(".errorMessage")),
(progress = document.querySelector(".progress.error"));
let timer1, timer2;
errorMessage.textContent = message;
toast.classList.add("active");
progress.classList.add("active");
timer1 = setTimeout(() => {
toast.classList.remove("active");
closeIcon.removeEventListener("click", () => {});
}, 5000);
timer2 = setTimeout(() => {
let timer1, timer2;
errorMessage.textContent = message;
toast.classList.add("active");
progress.classList.add("active");
timer1 = setTimeout(() => {
toast.classList.remove("active");
closeIcon.removeEventListener("click", () => { });
}, 5000);
timer2 = setTimeout(() => {
progress.classList.remove("active");
}, 5300);
closeIcon.addEventListener("click", () => {
toast.classList.remove("active");
setTimeout(() => {
progress.classList.remove("active");
}, 5300);
closeIcon.addEventListener("click", () => {
toast.classList.remove("active");
setTimeout(() => {
progress.classList.remove("active");
}, 300);
clearTimeout(timer1);
clearTimeout(timer2);
closeIcon.removeEventListener("click", () => {});
});
}, 300);
clearTimeout(timer1);
clearTimeout(timer2);
closeIcon.removeEventListener("click", () => { });
});
}
function showSuccessMessage(message) {
const toast = document.querySelector(".toast#successToast");
(closeIcon = document.querySelector(".close-success")),
const toast = document.querySelector(".toast#successToast");
(closeIcon = document.querySelector(".close-success")),
(successMessage = document.querySelector(".successMessage")),
(progress = document.querySelector(".progress.success"));
let timer1, timer2;
successMessage.textContent = message;
toast.classList.add("active");
progress.classList.add("active");
timer1 = setTimeout(() => {
toast.classList.remove("active");
closeIcon.removeEventListener("click", () => {});
}, 5000);
timer2 = setTimeout(() => {
let timer1, timer2;
successMessage.textContent = message;
toast.classList.add("active");
progress.classList.add("active");
timer1 = setTimeout(() => {
toast.classList.remove("active");
closeIcon.removeEventListener("click", () => { });
}, 5000);
timer2 = setTimeout(() => {
progress.classList.remove("active");
}, 5300);
closeIcon.addEventListener("click", () => {
toast.classList.remove("active");
setTimeout(() => {
progress.classList.remove("active");
}, 5300);
closeIcon.addEventListener("click", () => {
toast.classList.remove("active");
setTimeout(() => {
progress.classList.remove("active");
}, 300);
clearTimeout(timer1);
clearTimeout(timer2);
closeIcon.removeEventListener("click", () => {});
});
}, 300);
clearTimeout(timer1);
clearTimeout(timer2);
closeIcon.removeEventListener("click", () => { });
});
}
function openRestoreDBFileSelect() {
document.getElementById('restoreDBFile').click();
document.getElementById('restoreDBFile').click();
};
function restoreDB() {
const input = document.getElementById('restoreDBFile');
const file = input.files[0];
if (!file) {
console.error('No file selected');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch('endpoints/db/import.php', {
method: 'POST',
body: formData
})
const input = document.getElementById('restoreDBFile');
const file = input.files[0];
if (!file) {
console.error('No file selected');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch('endpoints/db/import.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccessMessage(data.message)
window.location.href = 'logout.php';
showSuccessMessage(data.message);
fetch('endpoints/db/migrate.php')
.then(response => response.text())
.then(() => {
window.location.href = 'logout.php';
})
.catch(error => {
window.location.href = 'logout.php';
});
} else {
showErrorMessage(data.message);
}
@@ -170,8 +177,8 @@ function checkThemeNeedsUpdate() {
}
window.onload = function () {
restoreFormFields();
removeFromStorage();
runDatabaseMigration();
checkThemeNeedsUpdate();
restoreFormFields();
removeFromStorage();
runDatabaseMigration();
checkThemeNeedsUpdate();
};

View File

@@ -485,7 +485,7 @@ if ($usesMultipleCurrencies) {
foreach ($categoryCost as $category) {
if ($category['cost'] != 0) {
$categoryDataPoints[] = [
"label" => $category['name'],
"label" => html_entity_decode($category['name']),
"y" => $category["cost"],
];
}
@@ -499,7 +499,7 @@ if ($usesMultipleCurrencies) {
foreach ($memberCost as $member) {
if ($member['cost'] != 0) {
$memberDataPoints[] = [
"label" => $member['name'],
"label" => html_entity_decode($member['name']),
"y" => $member["cost"],
];
@@ -513,7 +513,7 @@ if ($usesMultipleCurrencies) {
foreach ($paymentMethodsCount as $paymentMethod) {
if ($paymentMethod['count'] != 0) {
$paymentMethodDataPoints[] = [
"label" => $paymentMethod['name'],
"label" => html_entity_decode($paymentMethod['name']),
"y" => $paymentMethod["count"],
];
}