diff --git a/api/subscriptions/get_ical_feed.php b/api/subscriptions/get_ical_feed.php new file mode 100644 index 000000000..c19a248d7 --- /dev/null +++ b/api/subscriptions/get_ical_feed.php @@ -0,0 +1,210 @@ + false, + "title" => "Missing parameters" + ]; + echo json_encode($response); + exit; + } + + function getPriceConverted($price, $currency, $database) + { + $query = "SELECT rate FROM currencies WHERE id = :currency"; + $stmt = $database->prepare($query); + $stmt->bindParam(':currency', $currency, SQLITE3_INTEGER); + $result = $stmt->execute(); + + $exchangeRate = $result->fetchArray(SQLITE3_ASSOC); + if ($exchangeRate === false) { + return $price; + } else { + $fromRate = $exchangeRate['rate']; + return $price / $fromRate; + } + } + + $apiKey = $_REQUEST['api_key']; + + // Get user from API key + $sql = "SELECT * FROM user WHERE api_key = :apiKey"; + $stmt = $db->prepare($sql); + $stmt->bindValue(':apiKey', $apiKey); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + // If the user is not found, return an error + if (!$user) { + $response = [ + "success" => false, + "title" => "Invalid API key" + ]; + echo json_encode($response); + exit; + } + + $userId = $user['id']; + $userCurrencyId = $user['main_currency']; + + // Get last exchange update date for user + $sql = "SELECT * FROM last_exchange_update WHERE user_id = :userId"; + $stmt = $db->prepare($sql); + $stmt->bindValue(':userId', $userId); + $result = $stmt->execute(); + $lastExchangeUpdate = $result->fetchArray(SQLITE3_ASSOC); + + $canConvertCurrency = empty($lastExchangeUpdate['date']) ? false : true; + + // Get currencies for user + $sql = "SELECT * FROM currencies WHERE user_id = :userId"; + $stmt = $db->prepare($sql); + $stmt->bindValue(':userId', $userId); + $result = $stmt->execute(); + $currencies = []; + while ($currency = $result->fetchArray(SQLITE3_ASSOC)) { + $currencies[$currency['id']] = $currency; + } + + // Get categories for user + $sql = "SELECT * FROM categories WHERE user_id = :userId"; + $stmt = $db->prepare($sql); + $stmt->bindValue(':userId', $userId); + $result = $stmt->execute(); + $categories = []; + while ($category = $result->fetchArray(SQLITE3_ASSOC)) { + $categories[$category['id']] = $category['name']; + } + + // Get members for user + $sql = "SELECT * FROM household WHERE user_id = :userId"; + $stmt = $db->prepare($sql); + $stmt->bindValue(':userId', $userId); + $result = $stmt->execute(); + $members = []; + while ($member = $result->fetchArray(SQLITE3_ASSOC)) { + $members[$member['id']] = $member['name']; + } + + // Get payment methods for user + $sql = "SELECT * FROM payment_methods WHERE user_id = :userId"; + $stmt = $db->prepare($sql); + $stmt->bindValue(':userId', $userId); + $result = $stmt->execute(); + $paymentMethods = []; + while ($paymentMethod = $result->fetchArray(SQLITE3_ASSOC)) { + $paymentMethods[$paymentMethod['id']] = $paymentMethod['name']; + } + + $sql = "SELECT * FROM subscriptions WHERE user_id = :userId ORDER BY next_payment ASC"; + + $stmt = $db->prepare($sql); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($result) { + $subscriptions = array(); + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $subscriptions[] = $row; + } + } + + $subscriptionsToReturn = array(); + + foreach ($subscriptions as $subscription) { + $subscriptionToReturn = $subscription; + + if (isset($_REQUEST['convert_currency']) && $_REQUEST['convert_currency'] === 'true' && $canConvertCurrency && $subscription['currency_id'] != $userCurrencyId) { + $subscriptionToReturn['price'] = getPriceConverted($subscription['price'], $subscription['currency_id'], $db); + } else { + $subscriptionToReturn['price'] = $subscription['price']; + } + + $subscriptionToReturn['category_name'] = $categories[$subscription['category_id']]; + $subscriptionToReturn['payer_user_name'] = $members[$subscription['payer_user_id']]; + $subscriptionToReturn['payment_method_name'] = $paymentMethods[$subscription['payment_method_id']]; + + $subscriptionsToReturn[] = $subscriptionToReturn; + } + + $stmt->bindValue(':inactive', false, SQLITE3_INTEGER); + $result = $stmt->execute(); + + header('Content-Type: text/calendar; charset=utf-8'); + header('Content-Disposition: attachment; filename="subscriptions.ics"'); + + if ($result === false) { + die("BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:NAME:\nEND:VCALENDAR"); + } + + $icsContent = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Wallos//iCalendar//EN\nNAME:Wallos\nX-WR-CALNAME:Wallos\n"; + + while ($subscription = $result->fetchArray(SQLITE3_ASSOC)) { + $subscription['payer_user'] = $members[$subscription['payer_user_id']]; + $subscription['category'] = $categories[$subscription['category_id']]; + $subscription['payment_method'] = $paymentMethods[$subscription['payment_method_id']]; + $subscription['currency'] = $currencies[$subscription['currency_id']]['symbol']; + $subscription['trigger'] = $subscription['notify_days_before'] ? $subscription['notify_days_before'] : 1; + $subscription['price'] = number_format($subscription['price'], 2); + + $uid = uniqid(); + $summary = "Wallos: " . $subscription['name']; + $description = "Price: {$subscription['currency']}{$subscription['price']}\\nCategory: {$subscription['category']}\\nPayment Method: {$subscription['payment_method']}\\nPayer: {$subscription['payer_user']}\\nNotes: {$subscription['notes']}"; + $dtstart = (new DateTime($subscription['next_payment']))->format('Ymd\THis\Z'); + $dtend = (new DateTime($subscription['next_payment']))->modify('+1 hour')->format('Ymd\THis\Z'); + $location = isset($subscription['url']) ? $subscription['url'] : ''; + $alarm_trigger = '-' . $subscription['trigger'] . 'D'; + + $icsContent .= <<close(); + exit; + + + +} else { + $response = [ + "success" => false, + "title" => "Invalid request method" + ]; + echo json_encode($response); + exit; +} + + +?> \ No newline at end of file diff --git a/api/subscriptions/get_subscriptions.php b/api/subscriptions/get_subscriptions.php index d960f628f..2ecf6986d 100644 --- a/api/subscriptions/get_subscriptions.php +++ b/api/subscriptions/get_subscriptions.php @@ -306,64 +306,6 @@ function getPriceConverted($price, $currency, $database) $subscriptionsToReturn[] = $subscriptionToReturn; } - if (isset($_REQUEST['type'])) { - $type = $_REQUEST['type']; - $stmt->bindValue(':inactive', false, SQLITE3_INTEGER); - $result = $stmt->execute(); - - if ($type == "iCalendar") { - header('Content-Type: text/calendar; charset=utf-8'); - header('Content-Disposition: attachment; filename="subscriptions.ics"'); - - if ($result === false) { - die("BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:NAME:\nEND:VCALENDAR"); - } - - $icsContent = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Wallos//$type//EN\nNAME:Wallos\nX-WR-CALNAME:Wallos\n"; - - while ($subscription = $result->fetchArray(SQLITE3_ASSOC)) { - $subscription['payer_user'] = $members[$subscription['payer_user_id']]; - $subscription['category'] = $categories[$subscription['category_id']]; - $subscription['payment_method'] = $paymentMethods[$subscription['payment_method_id']]; - $subscription['currency'] = $currencies[$subscription['currency_id']]['symbol']; - $subscription['trigger'] = $subscription['notify_days_before'] ? $subscription['notify_days_before'] : 1; - $subscription['price'] = number_format($subscription['price'], 2); - - $uid = uniqid(); - $summary = "Wallos: " . $subscription['name']; - $description = "Price: {$subscription['currency']}{$subscription['price']}\\nCategory: {$subscription['category']}\\nPayment Method: {$subscription['payment_method']}\\nPayer: {$subscription['payer_user']}\\nNotes: {$subscription['notes']}"; - $dtstart = (new DateTime($subscription['next_payment']))->format('Ymd\THis\Z'); - $dtend = (new DateTime($subscription['next_payment']))->modify('+1 hour')->format('Ymd\THis\Z'); - $location = isset($subscription['url']) ? $subscription['url'] : ''; - $alarm_trigger = '-' . $subscription['trigger'] . 'D'; - - $icsContent .= <<close(); - exit; - } - } - $response = [ "success" => true, "title" => "subscriptions", diff --git a/calendar.php b/calendar.php index 8d0ce802b..f36dd7f43 100644 --- a/calendar.php +++ b/calendar.php @@ -104,51 +104,23 @@ function getPriceConverted($price, $currency, $database, $userId) } ?>
-

Calendar

- -
- -
- - -
-
- - +

+ Calendar + +

+
+ +
+ + + +
+
+ + + diff --git a/includes/i18n/de.php b/includes/i18n/de.php index 1dc631fca..38111b6b5 100644 --- a/includes/i18n/de.php +++ b/includes/i18n/de.php @@ -370,6 +370,7 @@ "month-11" => "November", "month-12" => "Dezember", "total_cost" => "Gesamtkosten", + "export_icalendar" => "iCalendar exportieren", // TOTP Page "insert_totp_code" => "Bitte geben Sie den TOTP-Code ein", diff --git a/includes/i18n/el.php b/includes/i18n/el.php index 17f75d291..10b4d504b 100644 --- a/includes/i18n/el.php +++ b/includes/i18n/el.php @@ -370,6 +370,7 @@ "month-11" => "Νοέμβριος", "month-12" => "Δεκέμβριος", "total_cost" => "Συνολικό κόστος", + "export_icalendar" => "Εξαγωγή iCalendar", // TOTP Page "insert_totp_code" => "Εισάγετε τον κωδικό TOTP", diff --git a/includes/i18n/en.php b/includes/i18n/en.php index ced661377..2ec145d5b 100644 --- a/includes/i18n/en.php +++ b/includes/i18n/en.php @@ -371,6 +371,7 @@ "month-11" => "November", "month-12" => "December", "total_cost" => "Total Cost", + "export_icalendar" => "Export iCalendar", // TOTP Page "insert_totp_code" => "Insert TOTP code", diff --git a/includes/i18n/es.php b/includes/i18n/es.php index 7be4e440f..016097c44 100644 --- a/includes/i18n/es.php +++ b/includes/i18n/es.php @@ -370,6 +370,7 @@ "month-11" => "Noviembre", "month-12" => "Diciembre", "total_cost" => "Costo Total", + "export_icalendar" => "Exportar iCalendar", // TOTP Page "insert_totp_code" => "Introduce el código TOTP", diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php index 52eb81d09..e931e7480 100644 --- a/includes/i18n/fr.php +++ b/includes/i18n/fr.php @@ -370,6 +370,7 @@ "month-11" => "Novembre", "month-12" => "Décembre", "total_cost" => "Coût total", + "export_icalendar" => "Exporter en iCalendar", // TOTP Page "insert_totp_code" => "Veuillez insérer le code TOTP", diff --git a/includes/i18n/it.php b/includes/i18n/it.php index f483ea183..7da313554 100644 --- a/includes/i18n/it.php +++ b/includes/i18n/it.php @@ -390,6 +390,7 @@ "month-11" => "Novembre", "month-12" => "Dicembre", "total_cost" => "Costo totale", + "export_icalendar" => "Esporta iCal", // TOTP Page "insert_totp_code" => "Inserisci il codice TOTP", diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php index 775bb2100..7576be64b 100644 --- a/includes/i18n/jp.php +++ b/includes/i18n/jp.php @@ -363,6 +363,7 @@ "month-11" => "11月", "month-12" => "12月", "total_cost" => "合計費用", + "export_icalendar" => "iCalendarをエクスポート", // TOTP Page "insert_totp_code" => "TOTPコードを入力してください", diff --git a/includes/i18n/ko.php b/includes/i18n/ko.php index 092d7f23d..3a6ef3b4f 100644 --- a/includes/i18n/ko.php +++ b/includes/i18n/ko.php @@ -370,6 +370,7 @@ "month-11" => "11월", "month-12" => "12월", "total_cost" => "총 비용", + "export_icalendar" => "iCalendar 내보내기", // TOTP Page "insert_totp_code" => "2단계 인증 코드를 입력하세요", diff --git a/includes/i18n/pl.php b/includes/i18n/pl.php index 4541d76bd..1200453b8 100644 --- a/includes/i18n/pl.php +++ b/includes/i18n/pl.php @@ -370,6 +370,7 @@ "month-11" => "Listopad", "month-12" => "Grudzień", "total_cost" => "Całkowity koszt", + "export_icalendar" => "Eksportuj do iCalendar", // TOTP Page "insert_totp_code" => "Wprowadź kod TOTP", diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php index dd252e059..b85c51353 100644 --- a/includes/i18n/pt.php +++ b/includes/i18n/pt.php @@ -370,6 +370,7 @@ "month-11" => "Novembro", "month-12" => "Dezembro", "total_cost" => "Custo Total", + "export_icalendar" => "Exportar iCalendar", // TOTP Page "insert_totp_code" => "Insira o código TOTP", diff --git a/includes/i18n/pt_br.php b/includes/i18n/pt_br.php index 5bb6c7117..dbec2474b 100644 --- a/includes/i18n/pt_br.php +++ b/includes/i18n/pt_br.php @@ -370,6 +370,7 @@ "month-11" => "Novembro", "month-12" => "Dezembro", "total_cost" => "Custo total", + "export_icalendar" => "Exportar iCalendar", // TOTP Page "insert_totp_code" => "Insira o código TOTP", diff --git a/includes/i18n/ru.php b/includes/i18n/ru.php index f2a0a57aa..2a74574b8 100644 --- a/includes/i18n/ru.php +++ b/includes/i18n/ru.php @@ -370,6 +370,7 @@ "month-11" => "Ноябрь", "month-12" => "Декабрь", "total_cost" => "Общая стоимость", + "export_icalendar" => "Экспорт в iCalendar", // TOTP Page "insert_totp_code" => "Введите код TOTP", diff --git a/includes/i18n/sl.php b/includes/i18n/sl.php index ee889309f..a55d521c0 100644 --- a/includes/i18n/sl.php +++ b/includes/i18n/sl.php @@ -363,6 +363,7 @@ "month-11" => "November", "month-12" => "December", "total_cost" => "Skupni stroški", + "export_icalendar" => "Izvozi iCalendar", // TOTP Page "insert_totp_code" => "Vnesite kodo TOTP", diff --git a/includes/i18n/sr.php b/includes/i18n/sr.php index 4fe9f7cbe..7431cc33b 100644 --- a/includes/i18n/sr.php +++ b/includes/i18n/sr.php @@ -370,6 +370,7 @@ "month-11" => "Новембар", "month-12" => "Децембар", "total_cost" => "Укупан трошак", + "export_icalendar" => "Извоз у iCalendar формат", // TOTP Page "insert_totp_code" => "Унесите ТОТП код", diff --git a/includes/i18n/sr_lat.php b/includes/i18n/sr_lat.php index 6ca48c574..850a0d6cc 100644 --- a/includes/i18n/sr_lat.php +++ b/includes/i18n/sr_lat.php @@ -370,6 +370,7 @@ "month-11" => "Novembar", "month-12" => "Decembar", "total_cost" => "Ukupan trošak", + "export_icalendar" => "Izvezi iCalendar", // TOTP Page "insert_totp_code" => "Unesite TOTP kod", diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php index 5f50e3e91..6411b4d66 100644 --- a/includes/i18n/tr.php +++ b/includes/i18n/tr.php @@ -370,6 +370,7 @@ "month-11" => "Kasım", "month-12" => "Aralık", "total_cost" => "Toplam Maliyet", + "export_icalendar" => "iCalendar olarak dışa aktar", // TOTP Page "insert_totp_code" => "Lütfen TOTP kodunuzu girin", diff --git a/includes/i18n/vi.php b/includes/i18n/vi.php index 439245c3d..152b41cd3 100644 --- a/includes/i18n/vi.php +++ b/includes/i18n/vi.php @@ -371,6 +371,7 @@ "month-11" => "Tháng Mười Một", "month-12" => "Tháng Mười Hai", "total_cost" => "Tổng chi phí", + "export_icalendar" => "Xuất iCalendar", // TOTP Page "insert_totp_code" => "Nhập mã TOTP", ]; diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php index 20411848a..af7c5c27b 100644 --- a/includes/i18n/zh_cn.php +++ b/includes/i18n/zh_cn.php @@ -390,6 +390,7 @@ "month-11" => "十一月", "month-12" => "十二月", "total_cost" => "总费用", + "export_icalendar" => "导出 iCalendar", // TOTP Page "insert_totp_code" => "请输入 TOTP 代码", diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php index c58fc39ba..556e76905 100644 --- a/includes/i18n/zh_tw.php +++ b/includes/i18n/zh_tw.php @@ -370,6 +370,7 @@ "month-11" => "十一月", "month-12" => "十二月", "total_cost" => "總費用", + "export_icalendar" => "匯出 iCalendar", // TOTP Page "insert_totp_code" => "請輸入 TOTP 驗證碼", diff --git a/includes/version.php b/includes/version.php index 8a5179ed4..7d40fb946 100644 --- a/includes/version.php +++ b/includes/version.php @@ -1,3 +1,3 @@ \ No newline at end of file diff --git a/scripts/calendar.js b/scripts/calendar.js index 391d9aa14..fa3d9a1c0 100644 --- a/scripts/calendar.js +++ b/scripts/calendar.js @@ -98,4 +98,31 @@ function exportCalendar(subscriptionId) { } }) .catch(error => console.error('Error:', error)); +} + +function showExportPopup() { + const host = window.location.href; + const apiPath = "api/subscriptions/get_ical_feed.php"; + const apiKey = document.getElementById('apiKey').value; + const queryParams = `?api_key=${apiKey}`; + const fullUrl = host.replace('calendar.php', apiPath) + queryParams; + document.getElementById('iCalendarUrl').value = fullUrl; + document.getElementById('subscriptions_calendar').classList.add('is-open'); +} + +function closePopup() { + document.getElementById('subscriptions_calendar').classList.remove('is-open'); +} + +function copyToClipboard() { + const urlField = document.getElementById('iCalendarUrl'); + urlField.select(); + urlField.setSelectionRange(0, 99999); // For mobile devices + navigator.clipboard.writeText(urlField.value) + .then(() => { + showSuccessMessage(translate('copied_to_clipboard')); + }) + .catch(() => { + showErrorMessage(translate('unknown_error')); + }); } \ No newline at end of file diff --git a/styles/styles.css b/styles/styles.css index b78c020fe..69ecaba67 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -70,6 +70,7 @@ h3 { .split-header h2 { margin-right: 20px; + display: flex; } .split-header>h2 .header-subtitle { @@ -2682,6 +2683,14 @@ input[type="radio"]:checked+label::after { aspect-ratio: 16/9; } +.button.export-ical { + padding: 0px; + width: 30px; + height: 30px; + margin-left: 10px; + color: white; +} + @media (max-width: 768px) { .mobile-nav { position: fixed;