updates
This commit is contained in:
114
modules/boersenchecker/assets/boersenchecker-native.js
Normal file
114
modules/boersenchecker/assets/boersenchecker-native.js
Normal file
@@ -0,0 +1,114 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function toPartialUrl(input, fallbackView) {
|
||||
const url = new URL(input, window.location.origin);
|
||||
if (!url.searchParams.has('view') && fallbackView) {
|
||||
url.searchParams.set('view', fallbackView);
|
||||
}
|
||||
url.searchParams.set('partial', '1');
|
||||
return url;
|
||||
}
|
||||
|
||||
async function fetchHtml(url, options) {
|
||||
const response = await fetch(url.toString(), {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
function enhanceHost(host, state) {
|
||||
host.querySelectorAll('a[href]').forEach((link) => {
|
||||
const href = link.getAttribute('href') || '';
|
||||
if (!href.startsWith('/apps/boersenchecker')) {
|
||||
return;
|
||||
}
|
||||
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
state.load(href);
|
||||
});
|
||||
});
|
||||
|
||||
host.querySelectorAll('form').forEach((form) => {
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const method = String(form.getAttribute('method') || 'get').toUpperCase();
|
||||
const action = form.getAttribute('action') || state.currentUrl.toString();
|
||||
const formData = new FormData(form);
|
||||
const url = toPartialUrl(action, state.defaultView);
|
||||
|
||||
try {
|
||||
if (method === 'GET') {
|
||||
const next = new URL(url.toString());
|
||||
next.search = '';
|
||||
next.searchParams.set('partial', '1');
|
||||
for (const [key, value] of formData.entries()) {
|
||||
next.searchParams.append(key, String(value));
|
||||
}
|
||||
await state.load(next.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const html = await fetchHtml(url, {
|
||||
method,
|
||||
body: formData
|
||||
});
|
||||
state.render(html, url);
|
||||
} catch (error) {
|
||||
state.renderError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.initBoersencheckerApp = function initBoersencheckerApp(host, options) {
|
||||
if (!(host instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryRoute = String(options?.entryRoute || '/apps/boersenchecker');
|
||||
const defaultView = String(options?.defaultView || 'overview');
|
||||
|
||||
const state = {
|
||||
defaultView,
|
||||
currentUrl: toPartialUrl(entryRoute, defaultView),
|
||||
async load(targetUrl) {
|
||||
const url = toPartialUrl(targetUrl, defaultView);
|
||||
this.currentUrl = url;
|
||||
host.innerHTML = '<div class="window-app-loading"><p>Börsenchecker wird geladen...</p></div>';
|
||||
try {
|
||||
const html = await fetchHtml(url);
|
||||
this.render(html, url);
|
||||
} catch (error) {
|
||||
this.renderError(error);
|
||||
}
|
||||
},
|
||||
render(html, url) {
|
||||
this.currentUrl = url;
|
||||
host.innerHTML = html;
|
||||
enhanceHost(host, this);
|
||||
if (typeof window.initBoersencheckerCharts === 'function') {
|
||||
window.initBoersencheckerCharts(host);
|
||||
}
|
||||
},
|
||||
renderError(error) {
|
||||
const message = error instanceof Error ? error.message : 'Börsenchecker konnte nicht geladen werden.';
|
||||
host.innerHTML = `<div class="window-app-error-state"><p>${message}</p></div>`;
|
||||
}
|
||||
};
|
||||
|
||||
state.load(entryRoute);
|
||||
};
|
||||
})();
|
||||
@@ -1,6 +1,11 @@
|
||||
(function () {
|
||||
const app = document.querySelector('[data-bc-home]');
|
||||
if (!app) return;
|
||||
'use strict';
|
||||
|
||||
function initBoersencheckerCharts(root) {
|
||||
const scope = root instanceof Element ? root : document;
|
||||
const app = scope.matches?.('[data-bc-home]') ? scope : scope.querySelector('[data-bc-home]');
|
||||
if (!app || app.dataset.bcChartBound === '1') return;
|
||||
app.dataset.bcChartBound = '1';
|
||||
|
||||
const chartShell = app.querySelector('[data-bc-chart]');
|
||||
const instrumentSelect = app.querySelector('[data-bc-instrument]');
|
||||
@@ -9,9 +14,10 @@
|
||||
const rangeButtons = Array.from(app.querySelectorAll('[data-range]'));
|
||||
const statusNode = app.querySelector('[data-bc-chart-status]');
|
||||
const summaryNode = app.querySelector('[data-bc-chart-summary]');
|
||||
const endpoint = app.getAttribute('data-chart-endpoint') || '';
|
||||
const endpoint = app.getAttribute('data-bc-chart-endpoint') || app.getAttribute('data-chart-endpoint') || '';
|
||||
const instrumentsScript = app.querySelector('[data-bc-instruments-json]');
|
||||
const instrumentMap = new Map();
|
||||
|
||||
if (instrumentsScript?.textContent) {
|
||||
try {
|
||||
const items = JSON.parse(instrumentsScript.textContent);
|
||||
@@ -20,6 +26,7 @@
|
||||
}
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
let activeRange = '1m';
|
||||
let currentPayload = null;
|
||||
|
||||
@@ -108,7 +115,10 @@
|
||||
}
|
||||
if (statusNode) statusNode.textContent = 'Chartdaten werden geladen...';
|
||||
try {
|
||||
const response = await fetch(`${endpoint}${endpoint.includes('?') ? '&' : '?'}instrument_id=${encodeURIComponent(instrumentId)}`, { headers: { Accept: 'application/json' } });
|
||||
const response = await fetch(`${endpoint}${endpoint.includes('?') ? '&' : '?'}instrument_id=${encodeURIComponent(instrumentId)}`, {
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!payload.ok) {
|
||||
throw new Error(payload.message || 'Chartdaten konnten nicht geladen werden.');
|
||||
@@ -124,7 +134,9 @@
|
||||
}
|
||||
} catch (error) {
|
||||
currentPayload = null;
|
||||
chartShell.innerHTML = `<div class="muted">${error.message}</div>`;
|
||||
if (chartShell) {
|
||||
chartShell.innerHTML = `<div class="muted">${error instanceof Error ? error.message : 'Chartdaten konnten nicht geladen werden.'}</div>`;
|
||||
}
|
||||
if (statusNode) statusNode.textContent = 'Fehler beim Laden der Chartdaten.';
|
||||
}
|
||||
}
|
||||
@@ -149,4 +161,8 @@
|
||||
}
|
||||
|
||||
loadChart();
|
||||
}
|
||||
|
||||
window.initBoersencheckerCharts = initBoersencheckerCharts;
|
||||
initBoersencheckerCharts(document);
|
||||
})();
|
||||
|
||||
@@ -15,11 +15,27 @@ return [
|
||||
'icon' => 'BC',
|
||||
'entry_route' => '/apps/boersenchecker',
|
||||
'window_mode' => 'window',
|
||||
'content_mode' => 'iframe',
|
||||
'content_mode' => 'native-module',
|
||||
'default_width' => 1220,
|
||||
'default_height' => 860,
|
||||
'supports_widget' => false,
|
||||
'supports_tray' => false,
|
||||
'module_name' => 'finance',
|
||||
'summary' => (string) ($manifest['description'] ?? 'Depotverwaltung fuer Aktien, Kaufdaten, Kursverlauf und Waehrungsumrechnung.'),
|
||||
'native_module' => [
|
||||
'initializer' => 'initBoersencheckerApp',
|
||||
'options' => [
|
||||
'entryRoute' => '/apps/boersenchecker',
|
||||
'defaultView' => 'overview',
|
||||
],
|
||||
'assets' => [
|
||||
'styles' => [
|
||||
['href' => '/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker.css&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/boersenchecker.css') ? filemtime(__DIR__ . '/assets/boersenchecker.css') : '0'))],
|
||||
],
|
||||
'scripts' => [
|
||||
['src' => '/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker.js&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/boersenchecker.js') ? filemtime(__DIR__ . '/assets/boersenchecker.js') : '0'))],
|
||||
['src' => '/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker-native.js&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/boersenchecker-native.js') ? filemtime(__DIR__ . '/assets/boersenchecker-native.js') : '0'))],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -11,6 +11,7 @@ session_start();
|
||||
|
||||
$projectRoot = dirname(__DIR__, 3);
|
||||
ModuleHttp::requireDesktopAccess($projectRoot);
|
||||
$isPartial = !empty($_GET['partial']);
|
||||
|
||||
$view = trim((string) ($_GET['view'] ?? 'overview'));
|
||||
$allowedViews = ['overview', 'depotverwaltung', 'aktienverwaltung', 'setup'];
|
||||
@@ -149,6 +150,36 @@ $renderSetup = static function (): void {
|
||||
<?= module_shell_footer() ?>
|
||||
<?php
|
||||
};
|
||||
|
||||
$renderContent = static function () use ($view, $renderSetup): void {
|
||||
?>
|
||||
<div class="boersenchecker-module-shell window-app-shell">
|
||||
<?php
|
||||
if ($view === 'setup') {
|
||||
$renderSetup();
|
||||
} elseif ($view === 'depotverwaltung') {
|
||||
$page = new \Modules\Boersenchecker\Support\DashboardPage();
|
||||
module_tpl('boersenchecker', 'dashboard', $page->handle());
|
||||
} elseif ($view === 'aktienverwaltung') {
|
||||
$page = new \Modules\Boersenchecker\Support\InstrumentPage();
|
||||
module_tpl('boersenchecker', 'instruments', $page->handle());
|
||||
} else {
|
||||
$page = new \Modules\Boersenchecker\Support\HomePage();
|
||||
module_tpl('boersenchecker', 'home', $page->handle());
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
};
|
||||
|
||||
ob_start();
|
||||
$renderContent();
|
||||
$pageContent = (string) ob_get_clean();
|
||||
|
||||
if ($isPartial) {
|
||||
echo $pageContent;
|
||||
return;
|
||||
}
|
||||
?><!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
@@ -250,22 +281,7 @@ $renderSetup = static function (): void {
|
||||
<link rel="stylesheet" href="/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker.css&v=<?= htmlspecialchars($assetVersion('assets/boersenchecker.css'), ENT_QUOTES) ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="boersenchecker-module-shell">
|
||||
<?php
|
||||
if ($view === 'setup') {
|
||||
$renderSetup();
|
||||
} elseif ($view === 'depotverwaltung') {
|
||||
$page = new \Modules\Boersenchecker\Support\DashboardPage();
|
||||
module_tpl('boersenchecker', 'dashboard', $page->handle());
|
||||
} elseif ($view === 'aktienverwaltung') {
|
||||
$page = new \Modules\Boersenchecker\Support\InstrumentPage();
|
||||
module_tpl('boersenchecker', 'instruments', $page->handle());
|
||||
} else {
|
||||
$page = new \Modules\Boersenchecker\Support\HomePage();
|
||||
module_tpl('boersenchecker', 'home', $page->handle());
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?= $pageContent ?>
|
||||
|
||||
<script src="/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker.js&v=<?= htmlspecialchars($assetVersion('assets/boersenchecker.js'), ENT_QUOTES) ?>"></script>
|
||||
</body>
|
||||
|
||||
@@ -178,6 +178,11 @@ $renderStartIcon = static function (array $profile): string {
|
||||
<button class="tray-menu-entry" type="button" data-account-action="login">Login</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="tray-menu" id="tray-fx-menu" hidden>
|
||||
<p class="tray-menu-copy" id="tray-fx-status">Kursstatus wird geladen ...</p>
|
||||
<button class="tray-menu-entry" type="button" data-fx-action="refresh">Refresh</button>
|
||||
<button class="tray-menu-entry" type="button" data-fx-action="force-refresh">Force-Refresh</button>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1405,6 +1405,14 @@ h1 {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.tray-menu-copy {
|
||||
margin: 0;
|
||||
padding: 6px 4px 2px;
|
||||
color: #cbd5e1;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.window-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ if (payloadNode) {
|
||||
const startMenuUserAvatar = document.getElementById('start-menu-user-avatar');
|
||||
const startMenuSessionAction = document.getElementById('start-menu-session-action');
|
||||
const accountMenu = document.getElementById('tray-account-menu');
|
||||
const fxTrayMenu = document.getElementById('tray-fx-menu');
|
||||
const fxTrayStatusNode = document.getElementById('tray-fx-status');
|
||||
const clockNode = document.getElementById('clock');
|
||||
const clockTopNode = document.getElementById('clock-top');
|
||||
const debugToggleNode = document.getElementById('debug-toggle');
|
||||
@@ -389,6 +391,88 @@ if (payloadNode) {
|
||||
accountButton.setAttribute('aria-expanded', String(isOpen));
|
||||
};
|
||||
|
||||
const setFxTrayMenuOpen = (isOpen) => {
|
||||
const fxButton = document.querySelector('.tray-pill[data-tray-app-id="fx-rates"]');
|
||||
|
||||
if (!(fxTrayMenu instanceof HTMLElement) || !(fxButton instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fxTrayMenu.hidden = !isOpen;
|
||||
fxButton.setAttribute('aria-expanded', String(isOpen));
|
||||
};
|
||||
|
||||
const loadFxTrayStatus = async () => {
|
||||
const fxButton = document.querySelector('.tray-pill[data-tray-app-id="fx-rates"]');
|
||||
|
||||
if (!(fxTrayStatusNode instanceof HTMLElement) || !(fxButton instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
fxTrayStatusNode.textContent = 'Kursstatus wird geladen ...';
|
||||
fxButton.setAttribute('title', 'Kursstatus wird geladen ...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/fx-rates/index.php?path=v1/status', {
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
const rows = Array.isArray(payload?.data) ? payload.data : [];
|
||||
const latest = rows.slice().sort((left, right) => {
|
||||
const leftTs = new Date(left?.fetched_at || 0).getTime();
|
||||
const rightTs = new Date(right?.fetched_at || 0).getTime();
|
||||
return rightTs - leftTs;
|
||||
})[0] || null;
|
||||
const statusText = latest
|
||||
? `Letzter Abruf: ${latest.fetched_at_display || latest.fetched_at} · ${latest.base_currency} · ${latest.provider}`
|
||||
: 'Noch keine gespeicherten Kurse vorhanden.';
|
||||
|
||||
fxTrayStatusNode.textContent = statusText;
|
||||
fxButton.setAttribute('title', statusText);
|
||||
return latest;
|
||||
} catch (_error) {
|
||||
const errorText = 'Kursstatus konnte nicht geladen werden.';
|
||||
fxTrayStatusNode.textContent = errorText;
|
||||
fxButton.setAttribute('title', errorText);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const runFxTrayRefresh = async (force) => {
|
||||
if (!(fxTrayStatusNode instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fxTrayStatusNode.textContent = 'Aktualisierung laeuft ...';
|
||||
try {
|
||||
const response = await fetch('/api/fx-rates/index.php?path=v1/refresh', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
force: force === true,
|
||||
trigger_source: 'tray',
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.context?.message || payload?.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const result = payload?.data || {};
|
||||
fxTrayStatusNode.textContent = result.reused
|
||||
? 'Kein neuer API-Abruf. Snapshot ist noch jung genug.'
|
||||
: `Kurse aktualisiert. ${result.updated_count || 0} Werte gespeichert.`;
|
||||
await loadFxTrayStatus();
|
||||
} catch (error) {
|
||||
fxTrayStatusNode.textContent = error instanceof Error ? error.message : 'Aktualisierung fehlgeschlagen.';
|
||||
}
|
||||
};
|
||||
|
||||
const findAppById = (appId) => payload.apps.find((app) => app.app_id === appId) || null;
|
||||
|
||||
const dispatchUserSettingsUpdate = (detail) => {
|
||||
@@ -1711,12 +1795,17 @@ if (payloadNode) {
|
||||
if (!(event.target instanceof Element) || !event.target.closest('.tray-pill[data-tray-id="account"], #tray-account-menu')) {
|
||||
setAccountMenuOpen(false);
|
||||
}
|
||||
|
||||
if (!(event.target instanceof Element) || !event.target.closest('.tray-pill[data-tray-app-id="fx-rates"], #tray-fx-menu')) {
|
||||
setFxTrayMenuOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeDesktopContextMenu();
|
||||
setAccountMenuOpen(false);
|
||||
setFxTrayMenuOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1729,6 +1818,7 @@ if (payloadNode) {
|
||||
const isClosed = startMenu.hasAttribute('hidden');
|
||||
setStartMenuOpen(isClosed);
|
||||
setAccountMenuOpen(false);
|
||||
setFxTrayMenuOpen(false);
|
||||
});
|
||||
|
||||
document.querySelector('.tray-pill[data-tray-id="account"]')?.addEventListener('click', () => {
|
||||
@@ -1738,20 +1828,50 @@ if (payloadNode) {
|
||||
|
||||
setStartMenuOpen(false);
|
||||
setAccountMenuOpen(accountMenu.hidden);
|
||||
setFxTrayMenuOpen(false);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tray-pill[data-tray-app-id]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const appId = button.getAttribute('data-tray-app-id') || '';
|
||||
if (appId === 'fx-rates') {
|
||||
setStartMenuOpen(false);
|
||||
setAccountMenuOpen(false);
|
||||
setFxTrayMenuOpen(fxTrayMenu instanceof HTMLElement ? fxTrayMenu.hidden : false);
|
||||
void loadFxTrayStatus();
|
||||
return;
|
||||
}
|
||||
const app = findAppById(appId);
|
||||
if (app) {
|
||||
openApp(app);
|
||||
setStartMenuOpen(false);
|
||||
setAccountMenuOpen(false);
|
||||
setFxTrayMenuOpen(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
fxTrayMenu?.addEventListener('click', (event) => {
|
||||
const target = event.target instanceof Element ? event.target.closest('[data-fx-action]') : null;
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = target.getAttribute('data-fx-action');
|
||||
|
||||
if (action === 'refresh') {
|
||||
void runFxTrayRefresh(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'force-refresh') {
|
||||
void runFxTrayRefresh(true);
|
||||
}
|
||||
});
|
||||
|
||||
void loadFxTrayStatus();
|
||||
|
||||
debugToggleNode?.addEventListener('click', () => {
|
||||
toggleDebugWindow();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user