diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js
index 14cd972e..87a73496 100644
--- a/modules/mining-checker/assets/js/app.js
+++ b/modules/mining-checker/assets/js/app.js
@@ -932,6 +932,7 @@
note: '',
});
const [payoutModalOpen, setPayoutModalOpen] = useState(false);
+ const [payoutMode, setPayoutMode] = useState('partial');
const [walletTransferRows, setWalletTransferRows] = useState([]);
const [walletWithdrawalForm, setWalletWithdrawalForm] = useState({
withdrawal_at: '',
@@ -1006,6 +1007,21 @@
const currentMinerOffers = Array.isArray(currentSettings.miner_offers) ? currentSettings.miner_offers : [];
const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : [];
const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
+ const latestMeasurementDate = latest && latest.measured_at ? parseStoredUtcDate(latest.measured_at) : null;
+ const latestMeasurementAgeMs = latestMeasurementDate ? (Date.now() - latestMeasurementDate.getTime()) : null;
+ const canTransferAll = !!latestMeasurementDate
+ && Number.isFinite(latestMeasurementAgeMs)
+ && latestMeasurementAgeMs >= 0
+ && latestMeasurementAgeMs <= 180000
+ && Number(latest?.coins_total_visible ?? latest?.coins_total ?? 0) > 0;
+ const fullTransferCoins = Number(latest?.coins_total_visible ?? latest?.coins_total ?? 0);
+ const transferAllHint = !latestMeasurementDate
+ ? 'Kein Mining-Upload vorhanden.'
+ : latestMeasurementAgeMs < 0
+ ? `Letzter Upload ${fmtDate(latest.measured_at)} liegt in der Zukunft.`
+ : canTransferAll
+ ? `Verwendet den letzten Upload von ${fmtDate(latest.measured_at)} mit ${fmtNumber(fullTransferCoins, 6)} ${currentMiningCurrency}.`
+ : `Alles uebertragen ist nur bis 3 Minuten nach dem letzten Upload moeglich. Letzter Upload: ${fmtDate(latest.measured_at)}.`;
const latestWalletSnapshot = currentWalletSnapshots.length ? currentWalletSnapshots[0] : null;
const currentWalletMiningBalance = walletAssetBalance(latestWalletSnapshot, currentMiningCurrency);
const renewableOfferIds = new Set(
@@ -1949,6 +1965,36 @@
}
}
+ async function submitFullPayout() {
+ if (!canTransferAll) {
+ setError(transferAllHint);
+ return;
+ }
+
+ setSaving(true);
+ setError('');
+ setMessage('');
+ try {
+ const savedPayout = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ transfer_mode: 'full_latest',
+ note: 'Kompletttransfer aus letztem Upload',
+ }),
+ timeoutMs: 8000,
+ });
+ applySavedPayout(savedPayout);
+ setWalletTransferRows((previous) => previous.filter((row) => String(row.id) !== String(savedPayout.id)).concat(savedPayout).sort((left, right) => String(left?.payout_at || '').localeCompare(String(right?.payout_at || ''))));
+ invalidateProjectBootstrapCache(projectKey);
+ setMessage('Gesamter aktueller Miner-Bestand wurde ins NC Wallet uebertragen.');
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setSaving(false);
+ }
+ }
+
async function deleteWalletTransfer(transfer) {
if (!transfer || !transfer.id) {
return;
@@ -2758,11 +2804,20 @@
: h('div', { className: 'mc-empty' }, 'Noch keine Wallet-Assets erkannt.')),
panel('Wallet-Transfers', 'Transfers zum hier verfolgten Wallet erhoehen den Wallet-Bestand und reduzieren den sichtbaren Miner-Bestand. Reinvestierte Krypto-Minerkaeufe werden davon automatisch wieder abgezogen.', [
h('div', { key: 'actions', className: 'mc-inline-row' }, [
+ h('button', {
+ key: 'transfer-all',
+ type: 'button',
+ className: 'mc-button mc-button--secondary',
+ onClick: submitFullPayout,
+ disabled: saving || !canTransferAll,
+ title: transferAllHint,
+ }, 'Alles uebertragen'),
h('button', {
key: 'add-transfer',
type: 'button',
className: 'mc-button mc-button--secondary',
onClick: () => {
+ setPayoutMode('partial');
setPayoutForm((previous) => ({
payout_at: previous.payout_at || nowDateTimeLocalValue(),
coins_amount: previous.coins_amount || '',
@@ -2771,8 +2826,9 @@
}));
setPayoutModalOpen(true);
},
- }, 'Zu Wallet transferieren'),
+ }, 'Teiluebertragung'),
]),
+ h('div', { key: 'transfer-help', className: 'mc-text' }, transferAllHint),
h('div', { key: 'transfer-list', className: 'mc-table-shell' }, [
h('table', { key: 'transfer-table', className: 'mc-table' }, [
h('thead', { key: 'head' }, h('tr', null, ['Zeit', 'Coins', 'Waehrung', 'Notiz', 'Aktion'].map((label) => h('th', { key: label }, label)))),
@@ -3213,8 +3269,9 @@
]),
]),
], () => setCostPlanModalOpen(false)) : null,
- payoutModalOpen ? renderModal('Zu Wallet transferieren', [
+ payoutModalOpen ? renderModal(payoutMode === 'partial' ? 'Teiluebertragung ins NC Wallet' : 'Zu Wallet transferieren', [
h('form', { key: 'form', className: 'mc-form', onSubmit: submitPayout }, [
+ h('div', { key: 'hint', className: 'mc-text' }, 'Hier kannst du einen Teilbetrag vom aktuellen Miner-Bestand in das NC Wallet uebertragen.'),
inputField('Transferzeitpunkt', 'datetime-local', payoutForm.payout_at, (value) => setPayoutForm({ ...payoutForm, payout_at: value })),
inputField('Coins', 'number', payoutForm.coins_amount, (value) => setPayoutForm({ ...payoutForm, coins_amount: value }), '0.000001'),
selectField('Waehrung', payoutForm.payout_currency, [currentCoinCurrency].concat(selectableCurrencies.map((currency) => currency.code).filter((code) => code !== currentCoinCurrency)), (value) => setPayoutForm({ ...payoutForm, payout_currency: value })),
diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php
index c90524fb..2471f868 100644
--- a/modules/mining-checker/src/Api/Router.php
+++ b/modules/mining-checker/src/Api/Router.php
@@ -1788,6 +1788,40 @@ final class Router
private function savePayout(string $projectKey, array $input): array
{
+ $transferMode = $this->enumValue($input['transfer_mode'] ?? 'manual', ['manual', 'full_latest'], 'transfer_mode');
+ if ($transferMode === 'full_latest') {
+ $measurements = $this->repository()->listRecentMeasurements($projectKey, 1);
+ $latestMeasurement = $measurements[0] ?? null;
+ if (!is_array($latestMeasurement)) {
+ throw new ApiException('Es ist kein aktueller Mining-Upload vorhanden.', 422);
+ }
+
+ $latestTs = strtotime((string) ($latestMeasurement['measured_at'] ?? ''));
+ if ($latestTs === false || (time() - $latestTs) > 180) {
+ throw new ApiException('Alles uebertragen ist nur moeglich, wenn der letzte Upload hoechstens 3 Minuten alt ist.', 422);
+ }
+
+ $settings = $this->settings($projectKey, [
+ 'currencies' => false,
+ 'miner_offers' => false,
+ ]);
+ $summary = $this->analytics()->buildSummary($measurements, $settings, [], ['include_offer_scenarios' => false]);
+ $visibleCoins = (float) ($summary['payouts']['current_visible_coins'] ?? 0.0);
+ if ($visibleCoins <= 0) {
+ throw new ApiException('Es stehen keine Coins fuer einen Kompletttransfer zur Verfuegung.', 422);
+ }
+
+ $coinCurrency = strtoupper(trim((string) ($latestMeasurement['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')));
+ $payload = [
+ 'payout_at' => (string) ($latestMeasurement['measured_at'] ?? ''),
+ 'coins_amount' => $visibleCoins,
+ 'payout_currency' => $coinCurrency !== '' ? $coinCurrency : 'DOGE',
+ 'note' => $this->optionalString($input['note'] ?? 'Kompletttransfer aus letztem Upload', 1000),
+ ];
+
+ return $this->repository()->savePayout($projectKey, $payload);
+ }
+
$payload = [
'payout_at' => $this->requiredDateTime($input['payout_at'] ?? null, 'payout_at', $this->projectTimezone($projectKey)),
'coins_amount' => $this->requiredDecimal($input['coins_amount'] ?? null, 'coins_amount'),
diff --git a/public/admin/users/index.php b/public/admin/users/index.php
index 841b1f1d..9d856f9f 100644
--- a/public/admin/users/index.php
+++ b/public/admin/users/index.php
@@ -133,6 +133,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pendingRequests = $registration->pendingApprovals();
$availableGroups = $registration->availableGroups();
+$existingUsers = $registration->searchExistingUsers('', 250);
$selected = $selectedId !== ''
? $registration->find($selectedId)
: ($pendingRequests[0] ?? null);
@@ -246,6 +247,8 @@ ob_start();
+
+
@@ -337,7 +340,10 @@ ob_start();