diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml deleted file mode 100644 index da1f80ada..000000000 --- a/.github/workflows/build-images.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Build images - -on: -# schedule: -# - cron: '0 0 * * *' # Midnight every day - workflow_dispatch: - inputs: - build_type: - description: Build Type - required: true - default: edge - type: choice - options: - - edge - - dev - - stable - -jobs: - build: - name: Build image - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: amd64,arm64,arm - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - install: true - - - name: Create the tag - id: image_tag - run: | - choice="${{ inputs.build_type }}" - out="" - - # if the workflow is running on a branch, let the tag be the branch name - if [[ $GITHUB_REF == "refs/heads/"* ]] ; then - echo "TAG=${GITHUB_REF#'refs/heads/'}" >> $GITHUB_OUTPUT - exit 0 - fi - - tag="${GITHUB_REF#'refs/tags/'}" - case $choice in - edge) - out="TAG=$tag-edge" - ;; - dev) - out="TAG=$tag-dev" - ;; - stable) - out="TAG=$tag-stable,${{ vars.DOCKERHUB_TAG }}:latest" - ;; - esac - echo $out >> $GITHUB_OUTPUT - - - name: Build and publish image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ vars.DOCKERHUB_TAG }}:${{ steps.image_tag.outputs.TAG }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml new file mode 100644 index 000000000..86af6d0fc --- /dev/null +++ b/.github/workflows/build-release.yaml @@ -0,0 +1,78 @@ +name: Build & Release +on: + push: + branches: + - "*" + pull_request: + branches: + - main +permissions: + contents: write + pull-requests: write + packages: write +env: + # login to docker hub with provided secrets + REGISTRY: docker.io + REGISTRY_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} + IMAGE_NAME: ${{ vars.DOCKERHUB_TAG }} + # For release-please, see available types at https://github.com/google-github-actions/release-please-action/tree/v4/?tab=readme-ov-file#release-types-supported + PROJECT_TYPE: simple +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: rp + if: github.event_name != 'pull_request' && github.ref_name == 'main' + uses: google-github-actions/release-please-action@v4 + with: + release-type: ${{ env.PROJECT_TYPE }} + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.REGISTRY_USERNAME }} + password: ${{ env.REGISTRY_PASSWORD }} + - name: Prepare tags for Docker meta + id: tags + env: + # When release please is skipped, these values will be empty + is_release: ${{ steps.rp.outputs.release_created }} + version: v${{ steps.rp.outputs.major }}.${{ steps.rp.outputs.minor }}.${{ steps.rp.outputs.patch }} + run: | + tags="" + if [[ "$is_release" = 'true' ]]; then + tags="type=semver,pattern={{version}},value=$version + type=ref,event=branch,value=main" + else + tags="type=ref,event=branch + type=ref,event=pr" + fi + { + echo 'tags<> "$GITHUB_OUTPUT" + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: ${{ steps.tags.outputs.tags }} + # necessary for multi-platform images + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + # necessary for multi-platform images + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64,linux/arm/v7 + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 7506f36fd..9143a9f13 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ !/images/uploads/logos/wallos.png .DS_Store .idea/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ed05a10cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog + +## [1.4.1](https://github.com/ellite/Wallos/compare/v1.4.0...v1.4.1) (2024-02-22) + + +### Bug Fixes + +* bug on saving fixer api key ([#142](https://github.com/ellite/Wallos/issues/142)) ([866eb28](https://github.com/ellite/Wallos/commit/866eb28e88495e851336b5e224274a823ff4173d)) + +## [1.4.0](https://github.com/ellite/Wallos/compare/v1.3.1...v1.4.0) (2024-02-21) + + +### Features + +* persist display and experimental settings on the db ([f0a6f1a](https://github.com/ellite/Wallos/commit/f0a6f1a2f18b329c9f784a9f1953cd0e7616e1c6)) +* small styles changed ([f0a6f1a](https://github.com/ellite/Wallos/commit/f0a6f1a2f18b329c9f784a9f1953cd0e7616e1c6)) + +## [1.3.1](https://github.com/ellite/Wallos/compare/v1.3.0...v1.3.1) (2024-02-20) + + +### Bug Fixes + +* missing authentication check ([#133](https://github.com/ellite/Wallos/issues/133)) ([b887d3a](https://github.com/ellite/Wallos/commit/b887d3a0503585dadde4b1b59b023c981b0f7f66)) + +## [1.3.0](https://github.com/ellite/Wallos/compare/v1.2.0...v1.3.0) (2024-02-19) + + +### Features + +* add apilayer as provider for fixer api ([0f19dd6](https://github.com/ellite/Wallos/commit/0f19dd688fe3a2156e7d26d1bf1e1f8b30ce79ad)) +* add apilayer as provider for fixer api ([#127](https://github.com/ellite/Wallos/issues/127)) ([0f19dd6](https://github.com/ellite/Wallos/commit/0f19dd688fe3a2156e7d26d1bf1e1f8b30ce79ad)) +* update exchange rate when saving api key ([0f19dd6](https://github.com/ellite/Wallos/commit/0f19dd688fe3a2156e7d26d1bf1e1f8b30ce79ad)) + +## [1.2.0](https://github.com/ellite/Wallos/compare/v1.1.0...v1.2.0) (2024-02-19) + + +### Features + +* enable deployment in subdirectory ([e2af9af](https://github.com/ellite/Wallos/commit/e2af9afc32bfc248f594336c50d44ad6f36f197e)) + +## [1.1.0](https://github.com/ellite/Wallos/compare/v1.0.1...v1.1.0) (2024-02-18) + + +### Features + +* new statistics per payment method ([#124](https://github.com/ellite/Wallos/issues/124)) ([6200fa5](https://github.com/ellite/Wallos/commit/6200fa5e87d3f60853c3d8b95f5d676e39b378f4)) + +## [1.0.1](https://github.com/ellite/Wallos/compare/v1.0.0...v1.0.1) (2024-02-18) + + +### Bug Fixes + +* show translated no category when sorting by category ([#122](https://github.com/ellite/Wallos/issues/122)) ([330c061](https://github.com/ellite/Wallos/commit/330c061b74ad1580173f3d3bc7b14048492e22d2)) + +## 1.0.0 (2024-02-15) + + +### Features + +* add workflow for building and publishing docker images ([970c96a](https://github.com/ellite/Wallos/commit/970c96a8c904809544c944071986be2a684daf50)) +* specify image stability type when triggering build ([5b22cfd](https://github.com/ellite/Wallos/commit/5b22cfd87a94a865f53b282964961862bbea1861)) + + +### Bug Fixes + +* Currency not preselected on registration ([fc56cf6](https://github.com/ellite/Wallos/commit/fc56cf69ef22a07978022265b2e8344dc293eb14)) +* Language sort order ([884a8e5](https://github.com/ellite/Wallos/commit/884a8e569339ddbcb89af4634c0c845b053affbb)) diff --git a/README.md b/README.md index bf06168f5..50a25c4ab 100644 --- a/README.md +++ b/README.md @@ -93,16 +93,6 @@ docker run -d --name wallos -v /path/to/config/wallos/db:/var/www/html/db \ bellamy/wallos:latest ``` -For ARM processors you need to use the tag main - -```bash -docker run -d --name wallos -v /path/to/config/wallos/db:/var/www/html/db \ --v /path/to/config/wallos/logos:/var/www/html/images/uploads/logos \ --e TZ=Europe/Berlin -p 8282:80 --restart unless-stopped \ -bellamy/wallos:main -``` - - ### Docker Compose ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..27150a83a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover any security vulnerabilities in this project, please report them to the developer by emailing [wallos@henrique.pt](mailto:wallos@henrique.pt). I appreciate your help in keeping the project secure. + +## Supported Versions + +This project is currently supported with security updates for the following versions: + +| Version | Supported | +| ------- | ------------------ | +| latest | :white_check_mark: | +| main | :white_check_mark: | +| 1.x.x | :x: | + +## Security Measures + +I take security seriously and am working on ways to implement security measures to protect the project. + +## Reporting a Security Concern + +If you have any security concerns or questions regarding the security of this project, please contact the developer at [wallos@henrique.pt](mailto:wallos@henrique.pt). + +## Responsible Disclosure + +I kindly request that you follow responsible disclosure practices and give me reasonable time to address any reported vulnerabilities before making them public. + diff --git a/endpoints/currency/fixer_api_key.php b/endpoints/currency/fixer_api_key.php index bac1b3920..02b7fdde6 100644 --- a/endpoints/currency/fixer_api_key.php +++ b/endpoints/currency/fixer_api_key.php @@ -5,16 +5,32 @@ if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) { if ($_SERVER["REQUEST_METHOD"] === "POST") { $newApiKey = isset($_POST["api_key"]) ? $_POST["api_key"] : ""; + $provider = isset($_POST["provider"]) ? $_POST["provider"] : 0; + $removeOldKey = "DELETE FROM fixer"; $db->exec($removeOldKey); - $testKeyUrl = "http://data.fixer.io/api/latest?access_key=$newApiKey"; - $response = file_get_contents($testKeyUrl); + + if ($provider == 1) { + $testKeyUrl = "https://api.apilayer.com/fixer/latest?base=USD&symbols=EUR"; + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => 'apikey: ' . $newApiKey, + ] + ]); + $response = file_get_contents($testKeyUrl, false, $context); + } else { + $testKeyUrl = "http://data.fixer.io/api/latest?access_key=$newApiKey"; + $response = file_get_contents($testKeyUrl); + } + $apiData = json_decode($response, true); if ($apiData['success'] && $apiData['success'] == 1) { if (!empty($newApiKey)) { - $insertNewKey = "INSERT INTO fixer (api_key) VALUES (:api_key)"; + $insertNewKey = "INSERT INTO fixer (api_key, provider) VALUES (:api_key, :provider)"; $stmt = $db->prepare($insertNewKey); $stmt->bindParam(":api_key", $newApiKey, SQLITE3_TEXT); + $stmt->bindParam(":provider", $provider, SQLITE3_INTEGER); $result = $stmt->execute(); if ($result) { echo json_encode(["success" => true, "message" => translate('api_key_saved', $i18n)]); diff --git a/endpoints/currency/update_exchange.php b/endpoints/currency/update_exchange.php index fb5a8c0ef..db3ae192c 100644 --- a/endpoints/currency/update_exchange.php +++ b/endpoints/currency/update_exchange.php @@ -2,23 +2,28 @@ require_once '../../includes/connect_endpoint.php'; $shouldUpdate = true; -$query = "SELECT date FROM last_exchange_update"; -$result = $db->querySingle($query); -if ($result) { - $lastUpdateDate = new DateTime($result); - $currentDate = new DateTime(); - $lastUpdateDateString = $lastUpdateDate->format('Y-m-d'); - $currentDateString = $currentDate->format('Y-m-d'); - $shouldUpdate = $lastUpdateDateString < $currentDateString; -} +if (isset($_GET['force']) && $_GET['force'] === "true") { + $shouldUpdate = true; +} else { + $query = "SELECT date FROM last_exchange_update"; + $result = $db->querySingle($query); -if (!$shouldUpdate) { - echo "Rates are current, no need to update."; - exit; + if ($result) { + $lastUpdateDate = new DateTime($result); + $currentDate = new DateTime(); + $lastUpdateDateString = $lastUpdateDate->format('Y-m-d'); + $currentDateString = $currentDate->format('Y-m-d'); + $shouldUpdate = $lastUpdateDateString < $currentDateString; + } + + if (!$shouldUpdate) { + echo "Rates are current, no need to update."; + exit; + } } -$query = "SELECT api_key FROM fixer"; +$query = "SELECT api_key, provider FROM fixer"; $result = $db->query($query); if ($result) { @@ -26,6 +31,7 @@ if ($row) { $apiKey = $row['api_key']; + $provider = $row['provider']; $codes = ""; $query = "SELECT id, name, symbol, code FROM currencies"; @@ -41,8 +47,20 @@ $mainCurrencyCode = $row['code']; $mainCurrencyId = $row['main_currency']; - $api_url = "http://data.fixer.io/api/latest?access_key=". $apiKey . "&base=EUR&symbols=" . $codes; - $response = file_get_contents($api_url); + if ($provider === 1) { + $api_url = "https://api.apilayer.com/fixer/latest?base=EUR&symbols=" . $codes; + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => 'apikey: ' . $apiKey, + ] + ]); + $response = file_get_contents($api_url, false, $context); + } else { + $api_url = "http://data.fixer.io/api/latest?access_key=". $apiKey . "&base=EUR&symbols=" . $codes; + $response = file_get_contents($api_url); + } + $apiData = json_decode($response, true); $mainCurrencyToEUR = $apiData['rates'][$mainCurrencyCode]; diff --git a/endpoints/payments/payment.php b/endpoints/payments/payment.php index 7242ad64d..03ae0fbc7 100644 --- a/endpoints/payments/payment.php +++ b/endpoints/payments/payment.php @@ -17,7 +17,12 @@ $paymentId = $_GET['paymentId']; -$inUse = $db->querySingle('SELECT COUNT(*) as count FROM subscriptions WHERE payment_method_id=' . $paymentId) === 1; +$stmt = $db->prepare('SELECT COUNT(*) as count FROM subscriptions WHERE payment_method_id=:paymentId'); +$stmt->bindValue(':paymentId', $paymentId, SQLITE3_INTEGER); +$result = $stmt->execute(); +$row = $result->fetchArray(); +$inUse = $row['count'] === 1; + if ($inUse) { die(json_encode([ "success" => false, diff --git a/endpoints/settings/convert_currency.php b/endpoints/settings/convert_currency.php new file mode 100644 index 000000000..86c1b3897 --- /dev/null +++ b/endpoints/settings/convert_currency.php @@ -0,0 +1,33 @@ + false, + "message" => translate('session_expired', $i18n) + ])); +} + +if ($_SERVER["REQUEST_METHOD"] === "POST") { + $postData = file_get_contents("php://input"); + $data = json_decode($postData, true); + + $convert_currency = $data['value']; + + $stmt = $db->prepare('UPDATE settings SET convert_currency = :convert_currency'); + $stmt->bindParam(':convert_currency', $convert_currency, SQLITE3_INTEGER); + + if ($stmt->execute()) { + die(json_encode([ + "success" => true, + "message" => translate("success", $i18n) + ])); + } else { + die(json_encode([ + "success" => false, + "message" => translate("error", $i18n) + ])); + } +} + +?> \ No newline at end of file diff --git a/endpoints/settings/monthly_price.php b/endpoints/settings/monthly_price.php new file mode 100644 index 000000000..f6dc17285 --- /dev/null +++ b/endpoints/settings/monthly_price.php @@ -0,0 +1,33 @@ + false, + "message" => translate('session_expired', $i18n) + ])); +} + +if ($_SERVER["REQUEST_METHOD"] === "POST") { + $postData = file_get_contents("php://input"); + $data = json_decode($postData, true); + + $monthly_price = $data['value']; + + $stmt = $db->prepare('UPDATE settings SET monthly_price = :monthly_price'); + $stmt->bindParam(':monthly_price', $monthly_price, SQLITE3_INTEGER); + + if ($stmt->execute()) { + die(json_encode([ + "success" => true, + "message" => translate("success", $i18n) + ])); + } else { + die(json_encode([ + "success" => false, + "message" => translate("error", $i18n) + ])); + } +} + +?> \ No newline at end of file diff --git a/endpoints/settings/remove_background.php b/endpoints/settings/remove_background.php new file mode 100644 index 000000000..6eecbcffc --- /dev/null +++ b/endpoints/settings/remove_background.php @@ -0,0 +1,33 @@ + false, + "message" => translate('session_expired', $i18n) + ])); +} + +if ($_SERVER["REQUEST_METHOD"] === "POST") { + $postData = file_get_contents("php://input"); + $data = json_decode($postData, true); + + $remove_background = $data['value']; + + $stmt = $db->prepare('UPDATE settings SET remove_background = :remove_background'); + $stmt->bindParam(':remove_background', $remove_background, SQLITE3_INTEGER); + + if ($stmt->execute()) { + die(json_encode([ + "success" => true, + "message" => translate("success", $i18n) + ])); + } else { + die(json_encode([ + "success" => false, + "message" => translate("error", $i18n) + ])); + } +} + +?> \ No newline at end of file diff --git a/endpoints/settings/theme.php b/endpoints/settings/theme.php new file mode 100644 index 000000000..48a867c2b --- /dev/null +++ b/endpoints/settings/theme.php @@ -0,0 +1,33 @@ + false, + "message" => translate('session_expired', $i18n) + ])); +} + +if ($_SERVER["REQUEST_METHOD"] === "POST") { + $postData = file_get_contents("php://input"); + $data = json_decode($postData, true); + + $theme = $data['theme']; + + $stmt = $db->prepare('UPDATE settings SET dark_theme = :theme'); + $stmt->bindParam(':theme', $theme, SQLITE3_INTEGER); + + if ($stmt->execute()) { + die(json_encode([ + "success" => true, + "message" => translate("success", $i18n) + ])); + } else { + die(json_encode([ + "success" => false, + "message" => translate("error", $i18n) + ])); + } +} + +?> \ No newline at end of file diff --git a/endpoints/subscription/add.php b/endpoints/subscription/add.php index 5a3a27427..30df7d371 100644 --- a/endpoints/subscription/add.php +++ b/endpoints/subscription/add.php @@ -2,6 +2,7 @@ error_reporting(E_ERROR | E_PARSE); require_once '../../includes/connect_endpoint.php'; require_once '../../includes/inputvalidation.php'; + require_once '../../includes/getsettings.php'; session_start(); @@ -40,7 +41,7 @@ function getLogoFromUrl($url, $uploadDir, $name) { function saveLogo($imageData, $uploadFile, $name) { $image = imagecreatefromstring($imageData); - $removeBackground = isset($_COOKIE['removeBackground']) && $_COOKIE['removeBackground'] === 'true'; + $removeBackground = isset($settings['removeBackground']) && $settings['removeBackground'] === 'true'; if ($image !== false) { $tempFile = tempnam(sys_get_temp_dir(), 'logo'); imagepng($image, $tempFile); diff --git a/endpoints/subscriptions/export.php b/endpoints/subscriptions/export.php new file mode 100644 index 000000000..2b8df91d0 --- /dev/null +++ b/endpoints/subscriptions/export.php @@ -0,0 +1,48 @@ + false, + "message" => translate('session_expired', $i18n) + ])); +} + +require_once '../../includes/getdbkeys.php'; + +$query = "SELECT * FROM subscriptions"; + +$result = $db->query($query); +if ($result) { + $subscriptions = array(); + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + // Map foreign keys to their corresponding values + $row['currency'] = $currencies[$row['currency_id']]; + $row['payment_method'] = $payment_methods[$row['payment_method_id']]; + $row['payer_user'] = $members[$row['payer_user_id']]; + $row['category'] = $categories[$row['category_id']]; + $row['cycle'] = $cycles[$row['cycle']]; + $row['frequency'] = $frequencies[$row['frequency']]; + + $subscriptions[] = $row; + } + + // Output JSON + $json = json_encode($subscriptions, JSON_PRETTY_PRINT); + + // Set headers for file download + header('Content-Type: application/json'); + header('Content-Disposition: attachment; filename="subscriptions.json"'); + header('Pragma: no-cache'); + header('Expires: 0'); + + // Output JSON for download + echo $json; +} else { + echo json_encode(array('error' => 'Failed to fetch subscriptions.')); +} + +?> \ No newline at end of file diff --git a/endpoints/subscriptions/get.php b/endpoints/subscriptions/get.php index 718c18122..cb5850508 100644 --- a/endpoints/subscriptions/get.php +++ b/endpoints/subscriptions/get.php @@ -7,9 +7,11 @@ include_once '../../includes/list_subscriptions.php'; + require_once '../../includes/getsettings.php'; + $theme = "light"; - if (isset($_COOKIE['theme'])) { - $theme = $_COOKIE['theme']; + if (isset($settings['theme'])) { + $theme = $settings['theme']; } if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) { @@ -56,12 +58,13 @@ $print[$id]['price'] = floatval($subscription['price']); $print[$id]['inactive'] = $subscription['inactive']; $print[$id]['url'] = $subscription['url']; + $print[$id]['notes'] = $subscription['notes']; - if (isset($_COOKIE['convertCurrency']) && $_COOKIE['convertCurrency'] === 'true' && $currencyId != $mainCurrencyId) { + if (isset($settings['convertCurrency']) && $settings['convertCurrency'] === 'true' && $currencyId != $mainCurrencyId) { $print[$id]['price'] = getPriceConverted($print[$id]['price'], $currencyId, $db); $print[$id]['currency_code'] = $currencies[$mainCurrencyId]['code']; } - if (isset($_COOKIE['showMonthlyPrice']) && $_COOKIE['showMonthlyPrice'] === 'true') { + if (isset($settings['showMonthlyPrice']) && $settings['showMonthlyPrice'] === 'true') { $print[$id]['price'] = getPricePerMonth($cycle, $frequency, $print[$id]['price']); } } diff --git a/endpoints/user/save_user.php b/endpoints/user/save_user.php index 07c63f1fc..38bdfceb1 100644 --- a/endpoints/user/save_user.php +++ b/endpoints/user/save_user.php @@ -5,7 +5,7 @@ session_start(); function update_exchange_rate($db) { - $query = "SELECT api_key FROM fixer"; + $query = "SELECT api_key, provider FROM fixer"; $result = $db->query($query); if ($result) { @@ -13,6 +13,7 @@ function update_exchange_rate($db) { if ($row) { $apiKey = $row['api_key']; + $provider = $row['provider']; $codes = ""; $query = "SELECT id, name, symbol, code FROM currencies"; @@ -29,8 +30,20 @@ function update_exchange_rate($db) { $mainCurrencyCode = $row['code']; $mainCurrencyId = $row['main_currency']; - $api_url = "http://data.fixer.io/api/latest?access_key=". $apiKey . "&base=EUR&symbols=" . $codes; - $response = file_get_contents($api_url); + if ($provider === 1) { + $api_url = "https://api.apilayer.com/fixer/latest?base=EUR&symbols=" . $codes; + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => 'apikey: ' . $apiKey, + ] + ]); + $response = file_get_contents($api_url, false, $context); + } else { + $api_url = "http://data.fixer.io/api/latest?access_key=". $apiKey . "&base=EUR&symbols=" . $codes; + $response = file_get_contents($api_url); + } + $apiData = json_decode($response, true); $mainCurrencyToEUR = $apiData['rates'][$mainCurrencyCode]; @@ -125,7 +138,9 @@ function update_exchange_rate($db) { if ($result) { $cookieExpire = time() + (30 * 24 * 60 * 60); $oldLanguage = isset($_COOKIE['language']) ? $_COOKIE['language'] : "en"; - setcookie('language', $language, $cookieExpire, '/'); + $root = str_replace('/endpoints/user', '', dirname($_SERVER['PHP_SELF'])); + $root = $root == '' ? '/' : $root; + setcookie('language', $language, $cookieExpire, $root); if ($username != $oldUsername) { $_SESSION['username'] = $username; if (isset($_COOKIE['wallos_login'])) { @@ -166,4 +181,4 @@ function update_exchange_rate($db) { echo json_encode($response); exit(); } -?> \ No newline at end of file +?> diff --git a/images/icon/site.webmanifest b/images/icon/site.webmanifest index 3087a360a..0b5a830c4 100644 --- a/images/icon/site.webmanifest +++ b/images/icon/site.webmanifest @@ -1 +1 @@ -{"name":"","short_name":"","icons":[{"src":"/images/icon/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/images/icon/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{"name":"","short_name":"","icons":[{"src":"android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} diff --git a/images/siteicons/notes.png b/images/siteicons/notes.png new file mode 100644 index 000000000..3bb5cd8bc Binary files /dev/null and b/images/siteicons/notes.png differ diff --git a/includes/getsettings.php b/includes/getsettings.php new file mode 100644 index 000000000..421df17ed --- /dev/null +++ b/includes/getsettings.php @@ -0,0 +1,15 @@ +query($query); +$settings = $result->fetchArray(SQLITE3_ASSOC); +if ($settings) { + $cookieExpire = time() + (30 * 24 * 60 * 60); + setcookie('theme', $settings['dark_theme'] ? 'dark': 'light', $cookieExpire); + $settings['theme'] = $settings['dark_theme'] ? 'dark': 'light'; + $settings['showMonthlyPrice'] = $settings['monthly_price'] ? 'true': 'false'; + $settings['convertCurrency'] = $settings['convert_currency'] ? 'true': 'false'; + $settings['removeBackground'] = $settings['remove_background'] ? 'true': 'false'; +} + +?> \ No newline at end of file diff --git a/includes/header.php b/includes/header.php index 4218b42a0..35f33592e 100644 --- a/includes/header.php +++ b/includes/header.php @@ -8,6 +8,8 @@ require_once 'i18n/getlang.php'; require_once 'i18n/' . $lang . '.php'; + require_once 'getsettings.php'; + require_once 'version.php'; if ($userCount == 0) { @@ -17,8 +19,8 @@ } $theme = "light"; - if (isset($_COOKIE['theme'])) { - $theme = $_COOKIE['theme']; + if (isset($settings['theme'])) { + $theme = $settings['theme']; } ?> @@ -47,7 +49,7 @@
@@ -58,7 +60,7 @@ + +
+ + <?= translate('notes', $i18n) ?> + + +
+
\ No newline at end of file diff --git a/index.php b/index.php index 42dedd045..54a04e7e1 100644 --- a/index.php +++ b/index.php @@ -89,12 +89,13 @@ $print[$id]['price'] = floatval($subscription['price']); $print[$id]['inactive'] = $subscription['inactive']; $print[$id]['url'] = $subscription['url']; + $print[$id]['notes'] = $subscription['notes']; - if (isset($_COOKIE['convertCurrency']) && $_COOKIE['convertCurrency'] === 'true' && $currencyId != $mainCurrencyId) { + if (isset($settings['convertCurrency']) && $settings['convertCurrency'] === 'true' && $currencyId != $mainCurrencyId) { $print[$id]['price'] = getPriceConverted($print[$id]['price'], $currencyId, $db); $print[$id]['currency_code'] = $currencies[$mainCurrencyId]['code']; } - if (isset($_COOKIE['showMonthlyPrice']) && $_COOKIE['showMonthlyPrice'] === 'true') { + if (isset($settings['showMonthlyPrice']) && $settings['showMonthlyPrice'] === 'true') { $print[$id]['price'] = getPricePerMonth($cycle, $frequency, $print[$id]['price']); } } diff --git a/login.php b/login.php index 6cf2a92ef..e77d57145 100644 --- a/login.php +++ b/login.php @@ -16,7 +16,7 @@ session_start(); if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) { $db->close(); - header("Location: /"); + header("Location: ."); exit(); } @@ -47,7 +47,7 @@ $_SESSION['loggedin'] = true; $_SESSION['main_currency'] = $main_currency; $cookieExpire = time() + (30 * 24 * 60 * 60); - setcookie('language', $language, $cookieExpire, '/'); + setcookie('language', $language, $cookieExpire); if ($rememberMe) { $token = bin2hex(random_bytes(32)); $addLoginTokens = "INSERT INTO login_tokens (user_id, token) VALUES (?, ?)"; @@ -57,10 +57,10 @@ $addLoginTokensStmt->execute(); $_SESSION['token'] = $token; $cookieValue = $username . "|" . $token . "|" . $main_currency; - setcookie('wallos_login', $cookieValue, $cookieExpire, '/'); + setcookie('wallos_login', $cookieValue, $cookieExpire); } $db->close(); - header("Location: /"); + header("Location: ."); exit(); } else { $loginFailed = true; diff --git a/logout.php b/logout.php index 4e2fc5f82..700b08786 100644 --- a/logout.php +++ b/logout.php @@ -12,8 +12,8 @@ $_SESSION = array(); session_destroy(); $cookieExpire = time() - 3600; - setcookie('wallos_login', '', $cookieExpire, '/'); + setcookie('wallos_login', '', $cookieExpire); $db->close(); - header("Location: /"); + header("Location: ."); exit(); ?> \ No newline at end of file diff --git a/migrations/000006.php b/migrations/000006.php new file mode 100644 index 000000000..e52c45ebd --- /dev/null +++ b/migrations/000006.php @@ -0,0 +1,12 @@ +query("SELECT * FROM pragma_table_info('fixer') where name='provider'"); +$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false; + +if ($columnRequired) { + $db->exec('ALTER TABLE fixer ADD COLUMN provider INT DEFAULT 0'); + $db->exec('UPDATE fixer SET provider = 0'); +} \ No newline at end of file diff --git a/migrations/000007.php b/migrations/000007.php new file mode 100644 index 000000000..f6e072332 --- /dev/null +++ b/migrations/000007.php @@ -0,0 +1,15 @@ +exec('CREATE TABLE IF NOT EXISTS settings ( + dark_theme BOOLEAN DEFAULT 0, + monthly_price BOOLEAN DEFAULT 0, + convert_currency BOOLEAN DEFAULT 0, + remove_background BOOLEAN DEFAULT 0 +)'); + + +$db->exec('INSERT INTO settings (dark_theme, monthly_price, convert_currency, remove_background) VALUES (0, 0, 0, 0)'); + diff --git a/scripts/dashboard.js b/scripts/dashboard.js index 10bab631e..d2b2596c6 100644 --- a/scripts/dashboard.js +++ b/scripts/dashboard.js @@ -147,7 +147,7 @@ function handleFileSelect(event) { } function deleteSubscription(id) { - if (confirm("Are you sure you want to delete this subscription?")) { + if (confirm(translate('confirm_delete_subscription'))) { fetch(`endpoints/subscription/delete.php?id=${id}`, { method: 'DELETE', }) @@ -268,7 +268,7 @@ function setSortOption(sortOption) { const daysToExpire = 30; const expirationDate = new Date(); expirationDate.setDate(expirationDate.getDate() + daysToExpire); - const cookieValue = encodeURIComponent(sortOption) + '; expires=' + expirationDate.toUTCString() + '; path=/'; + const cookieValue = encodeURIComponent(sortOption) + '; expires=' + expirationDate.toUTCString(); document.cookie = 'sortOrder=' + cookieValue; fetchSubscriptions(); toggleSortOptions(); @@ -316,4 +316,4 @@ document.addEventListener('DOMContentLoaded', function() { document.querySelector('#sort-options').addEventListener('focus', function() { isSortOptionsOpen = true; }); -}); \ No newline at end of file +}); diff --git a/scripts/i18n/de.js b/scripts/i18n/de.js index c696605d7..e9616f715 100644 --- a/scripts/i18n/de.js +++ b/scripts/i18n/de.js @@ -7,6 +7,7 @@ let i18n = { failed_to_load_subscription: "Fehler beim Laden des Abonnements", edit_subscription: "Abonnement bearbeiten", add_subscription: "Abonnement hinzufügen", + confirm_delete_subscription: "Sind Sie sicher, dass Sie dieses Abonnement löschen möchten?", // Settings network_response_error: "Netzwerkfehler", failed_add_member: "Hinzufügen von Mitglied fehlgeschlagen", diff --git a/scripts/i18n/el.js b/scripts/i18n/el.js index 1a7461d9b..b6e119652 100644 --- a/scripts/i18n/el.js +++ b/scripts/i18n/el.js @@ -7,6 +7,7 @@ let i18n = { failed_to_load_subscription: "Απέτυχε η φόρτωση της συνδρομής", edit_subscription: "Επεξεργασία συνδρομής", add_subscription: "Προσθήκη συνδρομής", + confirm_delete_subscription: "Είστε σίγουρος ότι θέλετε να διαγράψετε αυτή τη συνδρομή;", // Settings network_response_error: "Η ανταπόκριση του δικτύου δεν ήταν εντάξει", failed_add_member: "Αποτυχία προσθήκης μέλους", diff --git a/scripts/i18n/en.js b/scripts/i18n/en.js index aac61c9b3..d84bdfb58 100644 --- a/scripts/i18n/en.js +++ b/scripts/i18n/en.js @@ -7,6 +7,7 @@ let i18n = { failed_to_load_subscription: "Failed to load subscription", edit_subscription: "Edit subscription", add_subscription: "Add subscription", + confirm_delete_subscription: "Are you sure you want to delete this subscription?", // Settings network_response_error: "Network response was not ok", failed_add_member: "Failed to add member", diff --git a/scripts/i18n/es.js b/scripts/i18n/es.js index a2eb63926..d1dc3e398 100644 --- a/scripts/i18n/es.js +++ b/scripts/i18n/es.js @@ -7,6 +7,7 @@ let i18n = { failed_to_load_subscription: "Error al cargar la suscripción", edit_subscription: "Editar suscripción", add_subscription: "Añadir suscripción", + confirm_delete_subscription: "¿Estás seguro de que quieres eliminar esta suscripción?", // Settings network_response_error: "Error en la respuesta de la red", failed_add_member: "Error al añadir miembro", diff --git a/scripts/i18n/fr.js b/scripts/i18n/fr.js index cadc6ce6c..fb1406c58 100644 --- a/scripts/i18n/fr.js +++ b/scripts/i18n/fr.js @@ -7,6 +7,7 @@ let i18n = { failed_to_load_subscription: "Impossible de charger l'abonnement", edit_subscription: "Modifier l'abonnement", add_subscription: "Ajouter un abonnement", + confirm_delete_subscription: "Êtes-vous sûr de vouloir supprimer cet abonnement ?", // Paramètres network_response_error: "La réponse du réseau n'était pas correcte", failed_add_member: "Échec de l'ajout du membre", diff --git a/scripts/i18n/jp.js b/scripts/i18n/jp.js index 47fae7269..bb0d14d07 100644 --- a/scripts/i18n/jp.js +++ b/scripts/i18n/jp.js @@ -7,6 +7,7 @@ let i18n = { failed_to_load_subscription: "定期購入の読み込みに失敗しました", edit_subscription: "定期購入の編集", add_subscription: "定期購入の追加", + confirm_delete_subscription: "この定期購入を削除してもよろしいですか?", // Settings network_response_error: "ネットワークの応答異常", failed_add_member: "世帯員の追加に失敗", diff --git a/scripts/i18n/pt.js b/scripts/i18n/pt.js index 4e4f337a9..ca2ae51cd 100644 --- a/scripts/i18n/pt.js +++ b/scripts/i18n/pt.js @@ -7,6 +7,7 @@ let i18n = { 'failed_to_load_subscription': 'Falha ao carregar a subscrição', 'edit_subscription': 'Editar subscrição', 'add_subscription': 'Adicionar subscrição', + 'confirm_delete_subscription': 'Tem a certeza de que deseja eliminar esta subscrição?', // Settings 'network_response_error': 'Erro de resposta de rede', 'failed_add_member': 'Falha ao adicionar membro', diff --git a/scripts/i18n/tr.js b/scripts/i18n/tr.js new file mode 100644 index 000000000..98140c204 --- /dev/null +++ b/scripts/i18n/tr.js @@ -0,0 +1,38 @@ +let i18n = { + // Dashboard + error_reloading_subscription: "Abonelik yeniden yüklenirken hata oluştu:", + error_fetching_image_results: "Görüntü sonuçları alınırken hata oluştu:", + subscription_deleted: "Abonelik silindi", + error_deleting_subscription: "Abonelik silinirken hata oluştu", + failed_to_load_subscription: "Abonelik yüklenemedi", + edit_subscription: "Aboneliği Düzenle", + add_subscription: "Abonelik Ekle", + confirm_delete_subscription: "Bu aboneliği silmek istediğinizden emin misiniz?", + // Ayarlar + network_response_error: "Ağ yanıtı kabul edilmedi", + failed_add_member: "Üye eklenemedi", + member: "Üye", + save_member: "Üyeyi Kaydet", + delete_member: "Üyeyi Sil", + failed_remove_member: "Üye silinmedi", + failed_save_member: "Üye kaydedilemedi", + failed_add_category: "Kategori eklenemedi", + category: "Kategori", + save_category: "Kategoriyi Kaydet", + delete_category: "Kategoriyi Sil", + failed_remove_category: "Kategori silinmedi", + currency: "Para Birimi", + currency_code: "Para Birimi Kodu", + save_currency: "Para Birimini Kaydet", + delete_currency: "Para Birimini Sil", + failed_remove_currency: "Para birimi kaldırılamadı", + failed_save_currency: "Para birimi kaydedilemedi", + cant_disable_payment_in_use: "Kullanımdaki ödemeyi devre dışı bırakamazsınız", + failed_save_payment_method: "Ödeme yöntemi kaydedilemedi", + unknown_error: "Bilinmeyen hata, lütfen tekrar deneyin.", + error_saving_notification_data: "Bildirim verisi kaydedilirken hata oluştu", + error_sending_notification: "Bildirim gönderilirken hata oluştu" +} + + + diff --git a/scripts/i18n/zh_cn.js b/scripts/i18n/zh_cn.js index 9658d5c38..b0bdcea51 100644 --- a/scripts/i18n/zh_cn.js +++ b/scripts/i18n/zh_cn.js @@ -7,6 +7,7 @@ let i18n = { 'failed_to_load_subscription': "加载订阅失败", 'edit_subscription': "编辑订阅", 'add_subscription': "添加订阅", + 'confirm_delete_subscription': "您确定要删除此订阅吗?", // Settings 'network_response_error': "网络响应不正常", 'failed_add_member': '添加成员失败', diff --git a/scripts/i18n/zh_tw.js b/scripts/i18n/zh_tw.js new file mode 100644 index 000000000..aae672427 --- /dev/null +++ b/scripts/i18n/zh_tw.js @@ -0,0 +1,35 @@ +let i18n = { + // Dashboard + 'error_reloading_subscription': '重新讀取訂閱時發生錯誤:', + 'error_fetching_image_results': '抓取圖片時發生錯誤:', + 'subscription_deleted': '訂閱已刪除', + 'error_deleting_subscription': "刪除訂閱時發生錯誤", + 'failed_to_load_subscription': "讀取訂閱失敗", + 'edit_subscription': "編輯訂閱", + 'add_subscription': "新增訂閱", + 'confirm_delete_subscription': "您確定要刪除此訂閱嗎?", + // Settings + 'network_response_error': "網路無回應", + 'failed_add_member': '新增成員失敗', + 'member': '成員', + 'save_member': '保存成員', + 'delete_member': '刪除成員', + 'failed_remove_member': '移除成員失敗', + 'failed_save_member': '保存成員失敗', + 'failed_add_category': '新增類別失敗', + 'category': '類別', + 'save_category': '保存類別', + 'delete_category': '刪除類別', + 'failed_remove_category': '移除類別失敗', + 'currency': '貨幣', + 'currency_code': '貨幣代碼', + 'save_currency': '保存貨幣', + 'delete_currency': '刪除貨幣', + 'failed_remove_currency': '移除貨幣失敗', + 'failed_save_currency': '保存貨幣失敗', + 'cant_disable_payment_in_use': '無法停用正在使用中的支付方式', + 'failed_save_payment_method': '保存支付方式失敗', + 'unknown_error': '發生未知的錯誤,請再試一次。', + 'error_saving_notification_data': '保存通知資料時發生錯誤', + 'error_sending_notification': '發送通知時發生錯誤', +}; diff --git a/scripts/registration.js b/scripts/registration.js index 645560e86..90fc8e6fa 100644 --- a/scripts/registration.js +++ b/scripts/registration.js @@ -5,7 +5,7 @@ function setCookie(name, value, days) { date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = "; expires=" + date.toUTCString(); } - document.cookie = name + "=" + value + expires + "; path=/"; + document.cookie = name + "=" + value + expires; } function storeFormFieldValue(fieldId) { @@ -25,7 +25,7 @@ function storeFormFields() { function restoreFormFieldValue(fieldId) { var fieldElement = document.getElementById(fieldId); - if (fieldElement) { + if (localStorage.getItem(fieldId)) { fieldElement.value = localStorage.getItem(fieldId) || ''; } } @@ -52,7 +52,18 @@ function changeLanguage(selectedLanguage) { location.reload(); } +function runDatabaseMigration() { + let url = "endpoints/db/migrate.php"; + fetch(url) + .then(response => { + if (!response.ok) { + throw new Error(translate('network_response_error')); + } + }); +} + window.onload = function () { restoreFormFields(); removeFromStorage(); -}; \ No newline at end of file + runDatabaseMigration(); +}; diff --git a/scripts/settings.js b/scripts/settings.js index 3e92fbd75..a0974e797 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -477,18 +477,21 @@ function addFixerKeyButton() { document.getElementById("addFixerKey").disabled = true; const apiKeyInput = document.querySelector("#fixerKey"); apiKey = apiKeyInput.value.trim(); + const provider = document.querySelector("#fixerProvider").value; fetch("endpoints/currency/fixer_api_key.php", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, - body: `api_key=${encodeURIComponent(apiKey)}`, + body: `api_key=${encodeURIComponent(apiKey)}&provider=${encodeURIComponent(provider)}`, }) .then(response => response.json()) .then(data => { if (data.success) { showSuccessMessage(data.message); document.getElementById("addFixerKey").disabled = false; + // update currency exchange rates + fetch("endpoints/currency/update_exchange.php?force=true"); } else { showErrorMessage(data.message); document.getElementById("addFixerKey").disabled = false; @@ -522,7 +525,7 @@ function saveNotificationsButton() { fromemail: fromEmail }; - fetch('/endpoints/notifications/save.php', { + fetch('endpoints/notifications/save.php', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -562,7 +565,7 @@ function testNotificationButton() { fromemail: fromEmail }; - fetch('/endpoints/notifications/sendtestmail.php', { + fetch('endpoints/notifications/sendtestmail.php', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -589,23 +592,70 @@ function switchTheme() { darkThemeCss.disabled = !darkThemeCss.disabled; const themeChoice = darkThemeCss.disabled ? 'light' : 'dark'; - document.cookie = `theme=${themeChoice}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; + document.cookie = `theme=${themeChoice}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; + + const button = document.getElementById("switchTheme"); + button.disabled = true; + + fetch('endpoints/settings/theme.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({theme: themeChoice === 'dark'}) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showSuccessMessage(data.message); + } else { + showErrorMessage(data.errorMessage); + } + button.disabled = false; + }).catch(error => { + button.disabled = false; + }); } -function setShowMonthlyPriceCookie() { +function storeSettingsOnDB(endpoint, value) { + fetch('endpoints/settings/' + endpoint + '.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({"value": value}) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showSuccessMessage(data.message); + } else { + showErrorMessage(data.errorMessage); + } + }); +} + +function setShowMonthlyPrice() { const showMonthlyPriceCheckbox = document.querySelector("#monthlyprice"); const value = showMonthlyPriceCheckbox.checked; - document.cookie = `showMonthlyPrice=${value}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; + + storeSettingsOnDB('monthly_price', value); } -function setConvertCurrencyCookie() { +function setConvertCurrency() { const convertCurrencyCheckbox = document.querySelector("#convertcurrency"); const value = convertCurrencyCheckbox.checked; - document.cookie = `convertCurrency=${value}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; + + storeSettingsOnDB('convert_currency', value); } -function setRemoveBackgroundCookie() { +function setRemoveBackground() { const removeBackgroundCheckbox = document.querySelector("#removebackground"); const value = removeBackgroundCheckbox.checked; - document.cookie = `removeBackground=${value}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`; -} \ No newline at end of file + + storeSettingsOnDB('remove_background', value); +} + +function exportToJson() { + window.location.href = "endpoints/subscriptions/export.php"; +} diff --git a/scripts/stats.js b/scripts/stats.js index 98bbc628f..d67db3097 100644 --- a/scripts/stats.js +++ b/scripts/stats.js @@ -8,7 +8,13 @@ function loadGraph(container, dataPoints, currency, run) { datasets: [{ data: dataPoints.map(point => point.y), }], - labels: dataPoints.map(point => `${point.label} (${new Intl.NumberFormat(navigator.language, { style: 'currency', currency }).format(point.y)})`), + labels: dataPoints.map(point => { + if (currency) { + return `${point.label} (${new Intl.NumberFormat(navigator.language, { style: 'currency', currency }).format(point.y)})`; + } else { + return `${point.label} (${new Intl.NumberFormat(navigator.language).format(point.y)})`; + } + }), }, options: { animation: { diff --git a/settings.php b/settings.php index e286b1bf3..31000da74 100644 --- a/settings.php +++ b/settings.php @@ -1,6 +1,7 @@ +
+ +
diff --git a/stats.php b/stats.php index 2d3e97412..52d66d2f9 100644 --- a/stats.php +++ b/stats.php @@ -60,6 +60,17 @@ function getPriceConverted($price, $currency, $database) { $categoryCost[$categoryId]['name'] = $row['name']; } +// Get payment methods +$categories = array(); +$query = "SELECT * FROM payment_methods WHERE enabled = 1"; +$result = $db->query($query); +while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $paymentMethodId = $row['id']; + $paymentMethodCount[$paymentMethodId] = $row; + $paymentMethodCount[$paymentMethodId]['count'] = 0; + $paymentMethodCount[$paymentMethodId]['name'] = $row['name']; +} + // Get code of main currency to display on statistics $query = "SELECT c.code FROM currencies c @@ -108,11 +119,13 @@ function getPriceConverted($price, $currency, $database) { $next_payment = $subscription['next_payment']; $payerId = $subscription['payer_user_id']; $categoryId = $subscription['category_id']; + $paymentMethodId = $subscription['payment_method_id']; $originalSubscriptionPrice = getPriceConverted($price, $currency, $db); $price = getPricePerMonth($cycle, $frequency, $originalSubscriptionPrice); $totalCostPerMonth += $price; $memberCost[$payerId]['cost'] += $price; $categoryCost[$categoryId]['cost'] += $price; + $paymentMethodCount[$paymentMethodId]['count'] += 1; if ($price > $mostExpensiveSubscription) { $mostExpensiveSubscription = $price; } @@ -224,6 +237,18 @@ function getPriceConverted($price, $currency, $database) { $showMemberCostGraph = count($memberDataPoints) > 1; + $paymentMethodDataPoints = []; + foreach ($paymentMethodCount as $paymentMethod) { + if ($paymentMethod['count'] != 0) { + $paymentMethodDataPoints[] = [ + "label" => $paymentMethod['name'], + "y" => $paymentMethod["count"], + ]; + } + } + + $showPaymentMethodCountGraph = count($paymentMethodDataPoints) > 1; + if ($showMemberCostGraph) { ?>
@@ -248,6 +273,17 @@ function getPriceConverted($price, $currency, $database) { +
+
+ +
+ +
+
@@ -259,6 +295,7 @@ function getPriceConverted($price, $currency, $database) { window.onload = function() { loadGraph("categorySplitChart", , "", ); loadGraph("memberSplitChart", , "", ); + loadGraph("paymentMethidSplitChart", , "", ); } .contain { background-color: #FFFFFF; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); padding: 12px 15px; - border-radius: 8px; + border-radius: 16px; cursor: pointer; } @@ -225,6 +225,13 @@ main > .contain { overflow: hidden; } +.subscription-notes { + display: none; + flex-direction: row; + padding: 6px 5px; + overflow: hidden; +} + .subscription-main > span, .subscription-secondary > span { display: flex; @@ -301,6 +308,12 @@ main > .contain { cursor: pointer; } +.subscription-notes > span { + display: flex; + align-items: center; + font-size: 14px; +} + @media (max-width: 768px) { .subscription-main > .name { display: none; @@ -317,11 +330,19 @@ main > .contain { } } -.subscription.is-open .subscription-secondary { +@media (max-width: 375px) { + .subscription-main > .cycle { + display: none; + } +} + +.subscription.is-open .subscription-secondary, +.subscription.is-open .subscription-notes { display: flex; } -.subscription-secondary img { +.subscription-secondary img, +.subscription-notes img { height: 20px; margin-right: 10px; } @@ -355,7 +376,7 @@ main > .contain { border: 1px solid #eee; padding: 20px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - border-radius: 8px; + border-radius: 16px; } .account-section header h2 { @@ -829,7 +850,7 @@ input[type="checkbox"] { background-color: #FFFFFF; padding: 22px; border: 1px solid #EEEEEE; - border-radius: 8px; + border-radius: 16px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-sizing: border-box; position: fixed; @@ -1115,7 +1136,7 @@ input[type="checkbox"] { .statistic { background-color: #FFFFFF; border: 1px solid #EEEEEE; - border-radius: 8px; + border-radius: 16px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); padding: 20px 24px 30px; display: flex; @@ -1162,7 +1183,7 @@ input[type="checkbox"] { .graph { background-color: #FFFFFF; border: 1px solid #EEEEEE; - border-radius: 8px; + border-radius: 16px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); flex-basis: 48%; align-items: center;