From 743c743c2ca8ac70bb1061992e9bcc63c9a6a346 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Fri, 14 Aug 2026 00:40:45 +0200 Subject: [PATCH] sdsad --- custom/apps/mining-checker/assets/js/app.js | 143 ++++++++++++------ custom/apps/mining-checker/src/Api/Router.php | 22 +++ .../src/Infrastructure/MiningRepository.php | 91 ++++++++++- 3 files changed, 212 insertions(+), 44 deletions(-) diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 04af10f3..5f98267c 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -299,6 +299,29 @@ return `${map.year}-${map.month}-${map.day}`; } + function isCryptoCurrencyCode(code) { + const normalized = String(code || '').toUpperCase(); + return [ + 'ADA', 'ARB', 'AVAX', 'BNB', 'BTC', 'CTC', 'DAI', 'DOGE', 'DOT', 'ETH', + 'HSH', 'LINK', 'LTC', 'MATIC', 'SOL', 'TRX', 'USDC', 'USDT', 'XMR', 'XRP', + ].includes(normalized); + } + + function entryCoverageMeta(startAt, runtimeMonths, autoRenew) { + const normalizedRuntime = Number(runtimeMonths); + const startDate = parseStoredUtcDate(startAt); + if (!startDate || !Number.isFinite(normalizedRuntime) || normalizedRuntime <= 0) { + return { endAt: null, isCovered: true }; + } + + const runtimeMs = normalizedRuntime * 30.4375 * 86400 * 1000; + const endAtDate = new Date(startDate.getTime() + runtimeMs); + return { + endAt: endAtDate.toISOString(), + isCovered: !!autoRenew || Date.now() <= endAtDate.getTime(), + }; + } + function currentSectionLabel(sectionId) { return sectionMap.get(String(sectionId || '').trim()) || 'Bereich'; } @@ -1106,45 +1129,54 @@ const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies; const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies; const selectedMinerScenario = scenarioMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null; - const activeMinerRows = currentPurchasedMiners.map((miner) => ({ - id: `purchase-${miner.id}`, - source: 'miete', - starts_at: miner.purchased_at, - label: miner.label, - runtime_months: miner.runtime_months, - auto_renew: !!miner.auto_renew, - effective_amount: miner.total_cost_amount, - effective_currency: miner.currency, - base_amount: miner.reference_price_amount, - base_currency: miner.reference_price_currency, - miner_id: miner.id, - miner_offer_id: miner.miner_offer_id, - payment_type: miner.reference_price_currency - && miner.currency - && String(miner.reference_price_currency).toUpperCase() !== String(miner.currency).toUpperCase() - ? 'crypto' - : 'fiat', - is_active: miner.is_active !== false, - can_toggle_auto_renew: Number(miner.runtime_months) > 0, - hashrate_text: formatHashrateWithBonus(miner.mining_speed_value, miner.mining_speed_unit, miner.bonus_speed_value, miner.bonus_speed_unit), - type_label: 'Aus Angebot gemietet', - })).concat(currentCostPlans.map((plan) => ({ - id: `plan-${plan.id}`, - source: 'manual', - starts_at: plan.starts_at, - label: plan.label, - runtime_months: plan.runtime_months, - auto_renew: !!plan.auto_renew, - effective_amount: plan.total_cost_amount, - effective_currency: plan.currency, - base_amount: plan.base_price_amount, - base_currency: currentSettings.report_currency || 'EUR', - payment_type: plan.payment_type, - is_active: !!plan.is_active, - can_toggle_auto_renew: false, - hashrate_text: formatHashrateWithBonus(plan.mining_speed_value, plan.mining_speed_unit, plan.bonus_speed_value, plan.bonus_speed_unit), - type_label: 'Manuell eingetragen', - }))).sort((left, right) => String(right.starts_at || '').localeCompare(String(left.starts_at || ''))); + const activeMinerRows = currentPurchasedMiners.map((miner) => { + const coverage = entryCoverageMeta(miner.purchased_at, miner.runtime_months, miner.auto_renew); + const effectiveCurrency = String(miner.currency || '').toUpperCase(); + return { + id: `purchase-${miner.id}`, + source: 'miete', + starts_at: miner.purchased_at, + label: miner.label, + runtime_months: miner.runtime_months, + auto_renew: !!miner.auto_renew, + effective_amount: miner.total_cost_amount, + effective_currency: miner.currency, + base_amount: miner.reference_price_amount, + base_currency: miner.reference_price_currency, + miner_id: miner.id, + miner_offer_id: miner.miner_offer_id, + payment_type: isCryptoCurrencyCode(effectiveCurrency) ? 'crypto' : 'fiat', + is_active: miner.is_active !== false && coverage.isCovered, + end_at: coverage.endAt, + can_toggle_auto_renew: Number(miner.runtime_months) > 0, + toggle_resource: 'purchased-miners', + toggle_id: miner.id, + hashrate_text: formatHashrateWithBonus(miner.mining_speed_value, miner.mining_speed_unit, miner.bonus_speed_value, miner.bonus_speed_unit), + type_label: 'Aus Angebot gemietet', + }; + }).concat(currentCostPlans.map((plan) => { + const coverage = entryCoverageMeta(plan.starts_at, plan.runtime_months, plan.auto_renew); + return { + id: `plan-${plan.id}`, + source: 'manual', + starts_at: plan.starts_at, + label: plan.label, + runtime_months: plan.runtime_months, + auto_renew: !!plan.auto_renew, + effective_amount: plan.total_cost_amount, + effective_currency: plan.currency, + base_amount: plan.base_price_amount, + base_currency: currentSettings.report_currency || 'EUR', + payment_type: plan.payment_type, + is_active: !!plan.is_active && coverage.isCovered, + end_at: coverage.endAt, + can_toggle_auto_renew: Number(plan.runtime_months) > 0, + toggle_resource: 'cost-plans', + toggle_id: plan.id, + hashrate_text: formatHashrateWithBonus(plan.mining_speed_value, plan.mining_speed_unit, plan.bonus_speed_value, plan.bonus_speed_unit), + type_label: 'Manuell eingetragen', + }; + })).sort((left, right) => String(right.starts_at || '').localeCompare(String(left.starts_at || ''))); useEffect(() => { if (reportCurrencyOverride) { @@ -2060,15 +2092,15 @@ } } - async function togglePurchasedMinerAutoRenew(row) { - if (!row || !row.can_toggle_auto_renew || !row.miner_id) { + async function toggleMinerAutoRenew(row) { + if (!row || !row.can_toggle_auto_renew || !row.toggle_resource || !row.toggle_id) { return; } setSaving(true); setError(''); try { - await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/purchased-miners/${encodeURIComponent(row.miner_id)}`, { + await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/${encodeURIComponent(row.toggle_resource)}/${encodeURIComponent(row.toggle_id)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auto_renew: !row.auto_renew }), @@ -2721,6 +2753,16 @@ const reinvestedCapital = latest ? convertMeasurementMoney(latest, latest.reinvested_capital, reportCurrency) : null; const walletValue = latest ? convertMeasurementMoney(latest, latest.wallet_value, reportCurrency) : null; const totalHoldingsValue = latest ? convertMeasurementMoney(latest, latest.total_holdings_value, reportCurrency) : null; + const totalEarnedOverall = latest && latest.growth_since_baseline !== null && latest.growth_since_baseline !== undefined + ? Number(latest.growth_since_baseline) + : null; + const totalSpentOverallCurrentAsset = latest + ? convertMeasurementMoney( + latest, + (Number(latest.cash_invested_capital) || 0) + (Number(latest.reinvested_capital) || 0), + currentCoinCurrency + ) + : null; const breakEvenReached = breakEvenRemainingAmount !== null && breakEvenRemainingAmount <= 0; const breakEvenEta = latest && latest.break_even_eta_at ? fmtDate(latest.break_even_eta_at) : null; const walletBalanceCurrentAsset = payload?.summary?.payouts?.wallet_balance_current_asset; @@ -2759,6 +2801,20 @@ ? `Wallet-Gegenwert ${fmtNumber(walletBalanceCurrentAsset, 6)} ${currentCoinCurrency}${currentWalletPrimaryCurrency !== currentCoinCurrency ? ` · direkt ${fmtNumber(walletBalanceCurrentAssetDirect, 6)} ${currentCoinCurrency}` : ''} · Miner ${fmtNumber(latest?.coins_total_visible ?? latest?.coins_total, 6)} ${currentCoinCurrency}` : '', }), + h(StatCard, { + key: 'earned-overall', + label: `Bisher verdient gesamt ${currentCoinCurrency}`, + value: totalEarnedOverall !== null ? fmtNumber(totalEarnedOverall, 6) : 'n/a', + sub: currentSettings?.baseline_measured_at + ? `Seit Baseline ${fmtDate(currentSettings.baseline_measured_at)}` + : 'Benötigt Baseline', + }), + h(StatCard, { + key: 'spent-overall', + label: `Bisher ausgegeben gesamt ${currentCoinCurrency}`, + value: totalSpentOverallCurrentAsset !== null ? fmtNumber(totalSpentOverallCurrentAsset, 6) : 'n/a', + sub: 'FIAT und Krypto auf die aktuelle Mining-Währung reduziert', + }), h(StatCard, { key: 'perday', label: perDayLabel, @@ -3182,6 +3238,7 @@ h('td', { key: 'runtime' }, [ h('div', { key: 'months' }, `${row.runtime_months} Monate`), h('div', { key: 'hash', className: 'mc-kicker' }, row.hashrate_text), + row.end_at ? h('div', { key: 'end', className: 'mc-kicker' }, `Ende ${fmtDate(row.end_at)}`) : null, ]), h('td', { key: 'renew' }, row.auto_renew ? 'ja' : 'nein'), h('td', { key: 'cost' }, [ @@ -3200,7 +3257,7 @@ ? h('button', { type: 'button', className: 'mc-button mc-button--ghost', - onClick: () => togglePurchasedMinerAutoRenew(row), + onClick: () => toggleMinerAutoRenew(row), disabled: saving, }, row.auto_renew ? 'Verlaengerung aus' : 'Verlaengerung an') : '—' diff --git a/custom/apps/mining-checker/src/Api/Router.php b/custom/apps/mining-checker/src/Api/Router.php index 5580f043..6825e7f1 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -265,6 +265,10 @@ final class Router Http::json(['data' => $this->saveCostPlan($projectKey, Http::input())], 201); } + if (preg_match('~^cost-plans/(\d+)$~', $resource, $matches) && $method === 'PATCH') { + Http::json(['data' => $this->updateCostPlan($projectKey, (int) $matches[1], Http::input())]); + } + if ($resource === 'payouts' && $method === 'GET') { Http::json(['data' => $this->payouts($projectKey)]); } @@ -1912,6 +1916,24 @@ final class Router ]); } + private function updateCostPlan(string $projectKey, int $planId, array $input): array + { + $plan = $this->repository()->getCostPlan($projectKey, $planId); + if (!is_array($plan)) { + throw new ApiException('Manuell eingetragener Miner nicht gefunden.', 404); + } + + if (!array_key_exists('auto_renew', $input)) { + throw new ApiException('Es kann aktuell nur auto_renew geaendert werden.', 422, ['field' => 'auto_renew']); + } + + return $this->repository()->updateCostPlanAutoRenew( + $projectKey, + $planId, + !empty($input['auto_renew']) + ); + } + private function savePayout(string $projectKey, array $input): array { $transferMode = $this->enumValue($input['transfer_mode'] ?? 'manual', ['manual', 'full_latest'], 'transfer_mode'); diff --git a/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php b/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php index 6c7a3c3d..66752b15 100644 --- a/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php +++ b/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php @@ -267,6 +267,70 @@ final class MiningRepository return $this->normalizeRow($fetch->fetch() ?: []); } + public function getCostPlan(string $projectKey, int $planId): ?array + { + $stmt = $this->pdo->prepare( + 'SELECT * FROM ' . $this->table('cost_plans') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id LIMIT 1' + ); + $stmt->execute([ + 'project_key' => $projectKey, + 'owner_sub' => $this->ownerSub, + 'id' => $planId, + ]); + $row = $stmt->fetch(); + if (is_array($row)) { + return $this->normalizeRow($row); + } + + $legacyStmt = $this->pdo->prepare( + 'SELECT * FROM ' . $this->table('cost_plans') . ' WHERE project_key = :project_key AND id = :id LIMIT 1' + ); + $legacyStmt->execute([ + 'project_key' => $projectKey, + 'id' => $planId, + ]); + $legacyRow = $legacyStmt->fetch(); + return is_array($legacyRow) ? $this->normalizeRow($legacyRow) : null; + } + + public function updateCostPlanAutoRenew(string $projectKey, int $planId, bool $autoRenew): array + { + $stmt = $this->pdo->prepare( + 'UPDATE ' . $this->table('cost_plans') . ' + SET auto_renew = :auto_renew + WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id' + ); + $stmt->execute([ + 'auto_renew' => $autoRenew ? 1 : 0, + 'project_key' => $projectKey, + 'owner_sub' => $this->ownerSub, + 'id' => $planId, + ]); + + if ($stmt->rowCount() === 0) { + $legacyUpdate = $this->pdo->prepare( + 'UPDATE ' . $this->table('cost_plans') . ' + SET auto_renew = :auto_renew + WHERE project_key = :project_key AND id = :id' + ); + $legacyUpdate->execute([ + 'auto_renew' => $autoRenew ? 1 : 0, + 'project_key' => $projectKey, + 'id' => $planId, + ]); + } + + $fetch = $this->pdo->prepare( + 'SELECT * FROM ' . $this->table('cost_plans') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id LIMIT 1' + ); + $fetch->execute([ + 'project_key' => $projectKey, + 'owner_sub' => $this->ownerSub, + 'id' => $planId, + ]); + return $this->normalizeRow($fetch->fetch() ?: []); + } + public function listMeasurements(string $projectKey, int $limit = 200): array { $stmt = $this->pdo->prepare( @@ -915,7 +979,19 @@ final class MiningRepository 'id' => $minerId, ]); $row = $stmt->fetch(); - return is_array($row) ? $this->normalizeRow($row) : null; + if (is_array($row)) { + return $this->normalizeRow($row); + } + + $legacyStmt = $this->pdo->prepare( + 'SELECT * FROM ' . $this->table('purchased_miners') . ' WHERE project_key = :project_key AND id = :id LIMIT 1' + ); + $legacyStmt->execute([ + 'project_key' => $projectKey, + 'id' => $minerId, + ]); + $legacyRow = $legacyStmt->fetch(); + return is_array($legacyRow) ? $this->normalizeRow($legacyRow) : null; } public function purchaseMiner(string $projectKey, ?int $offerId, array $payload): array @@ -1027,6 +1103,19 @@ final class MiningRepository 'id' => $minerId, ]); + if ($stmt->rowCount() === 0) { + $legacyUpdate = $this->pdo->prepare( + 'UPDATE ' . $this->table('purchased_miners') . ' + SET auto_renew = :auto_renew + WHERE project_key = :project_key AND id = :id' + ); + $legacyUpdate->execute([ + 'auto_renew' => $autoRenew ? 1 : 0, + 'project_key' => $projectKey, + 'id' => $minerId, + ]); + } + $fetch = $this->pdo->prepare( 'SELECT * FROM ' . $this->table('purchased_miners') . ' WHERE project_key = :project_key AND owner_sub = :owner_sub AND id = :id LIMIT 1' );