ysdsad
This commit is contained in:
@@ -674,9 +674,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SimpleChart(props) {
|
function SimpleChart(props) {
|
||||||
|
const [hoveredPoint, setHoveredPoint] = useState(null);
|
||||||
|
const touchLike = typeof window !== 'undefined'
|
||||||
|
&& typeof window.matchMedia === 'function'
|
||||||
|
&& window.matchMedia('(hover: none), (pointer: coarse)').matches;
|
||||||
|
|
||||||
function pointTooltip(seriesLabel, point) {
|
function pointTooltip(seriesLabel, point) {
|
||||||
const label = seriesLabel ? `${seriesLabel} · ` : '';
|
const label = seriesLabel ? `${seriesLabel} · ` : '';
|
||||||
return `${label}${String(point.x)}: ${fmtNumber(point.y, 6)}`;
|
return `${label}${String(point.x)}: ${point.formatted || fmtNumber(point.y, 6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function axisLabel(axis) {
|
||||||
|
return axis === 'right'
|
||||||
|
? (props.rightAxisLabel || 'Rechts')
|
||||||
|
: (props.leftAxisLabel || props.yLabel || 'Wert');
|
||||||
}
|
}
|
||||||
|
|
||||||
const series = Array.isArray(props.series)
|
const series = Array.isArray(props.series)
|
||||||
@@ -685,18 +696,36 @@
|
|||||||
key: item && item.key ? String(item.key) : `series-${index}`,
|
key: item && item.key ? String(item.key) : `series-${index}`,
|
||||||
label: item && item.label ? String(item.label) : `Serie ${index + 1}`,
|
label: item && item.label ? String(item.label) : `Serie ${index + 1}`,
|
||||||
color: item && item.color ? String(item.color) : ['#60a5fa', '#2dd4bf', '#f59e0b', '#f472b6'][index % 4],
|
color: item && item.color ? String(item.color) : ['#60a5fa', '#2dd4bf', '#f59e0b', '#f472b6'][index % 4],
|
||||||
|
axis: item && item.axis === 'right' ? 'right' : 'left',
|
||||||
|
formatter: item && typeof item.formatter === 'function' ? item.formatter : null,
|
||||||
data: Array.isArray(item && item.data)
|
data: Array.isArray(item && item.data)
|
||||||
? item.data.filter((point) => point && point.y !== null && point.y !== undefined)
|
? item.data
|
||||||
|
.filter((point) => point && point.y !== null && point.y !== undefined)
|
||||||
|
.map((point) => ({
|
||||||
|
...point,
|
||||||
|
formatted: item && typeof item.formatter === 'function'
|
||||||
|
? item.formatter(Number(point.y), point)
|
||||||
|
: (point.formatted || fmtNumber(point.y, 6)),
|
||||||
|
}))
|
||||||
: [],
|
: [],
|
||||||
}))
|
}))
|
||||||
.filter((item) => item.data.length > 0)
|
.filter((item) => item.data.length > 0)
|
||||||
: [];
|
: [];
|
||||||
const points = Array.isArray(props.data) ? props.data.filter((point) => point && point.y !== null && point.y !== undefined) : [];
|
const points = Array.isArray(props.data)
|
||||||
|
? props.data
|
||||||
|
.filter((point) => point && point.y !== null && point.y !== undefined)
|
||||||
|
.map((point) => ({
|
||||||
|
...point,
|
||||||
|
formatted: point.formatted || fmtNumber(point.y, 6),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
const isMultiSeries = series.length > 0;
|
const isMultiSeries = series.length > 0;
|
||||||
const activeSeries = isMultiSeries ? series : [{
|
const activeSeries = isMultiSeries ? series : [{
|
||||||
key: 'default',
|
key: 'default',
|
||||||
label: props.yLabel || 'Wert',
|
label: props.yLabel || 'Wert',
|
||||||
color: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
|
color: props.type === 'area' ? '#2dd4bf' : '#60a5fa',
|
||||||
|
axis: 'left',
|
||||||
|
formatter: null,
|
||||||
data: points,
|
data: points,
|
||||||
}];
|
}];
|
||||||
const allPoints = activeSeries.flatMap((item) => item.data);
|
const allPoints = activeSeries.flatMap((item) => item.data);
|
||||||
@@ -725,15 +754,32 @@
|
|||||||
const width = 640;
|
const width = 640;
|
||||||
const height = 220;
|
const height = 220;
|
||||||
const padding = 24;
|
const padding = 24;
|
||||||
const values = allPoints.map((point) => Number(point.y));
|
const axisStats = activeSeries.reduce((stats, item) => {
|
||||||
const minY = Math.min.apply(null, values);
|
const values = item.data.map((point) => Number(point.y)).filter((value) => Number.isFinite(value));
|
||||||
const maxY = Math.max.apply(null, values);
|
if (!values.length) {
|
||||||
const range = maxY - minY || 1;
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const axis = item.axis === 'right' ? 'right' : 'left';
|
||||||
|
const min = Math.min.apply(null, values);
|
||||||
|
const max = Math.max.apply(null, values);
|
||||||
|
const existing = stats[axis];
|
||||||
|
stats[axis] = existing
|
||||||
|
? {
|
||||||
|
min: Math.min(existing.min, min),
|
||||||
|
max: Math.max(existing.max, max),
|
||||||
|
}
|
||||||
|
: { min, max };
|
||||||
|
return stats;
|
||||||
|
}, { left: null, right: null });
|
||||||
const seriesCoords = activeSeries.map((item) => {
|
const seriesCoords = activeSeries.map((item) => {
|
||||||
|
const axis = item.axis === 'right' ? 'right' : 'left';
|
||||||
|
const axisMeta = axisStats[axis] || { min: 0, max: 1 };
|
||||||
|
const range = axisMeta.max - axisMeta.min || 1;
|
||||||
const stepX = item.data.length > 1 ? (width - padding * 2) / (item.data.length - 1) : 0;
|
const stepX = item.data.length > 1 ? (width - padding * 2) / (item.data.length - 1) : 0;
|
||||||
const coords = item.data.map((point, index) => {
|
const coords = item.data.map((point, index) => {
|
||||||
const x = padding + stepX * index;
|
const x = padding + stepX * index;
|
||||||
const y = height - padding - ((Number(point.y) - minY) / range) * (height - padding * 2);
|
const y = height - padding - ((Number(point.y) - axisMeta.min) / range) * (height - padding * 2);
|
||||||
return [x, y];
|
return [x, y];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -747,18 +793,32 @@
|
|||||||
: '',
|
: '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const leftAxis = axisStats.left || axisStats.right || { min: 0, max: 1 };
|
||||||
|
const rightAxis = axisStats.right;
|
||||||
|
|
||||||
return h('div', { className: 'mc-chart space-y-3' }, [
|
return h('div', { className: 'mc-chart space-y-3', style: { position: 'relative' } }, [
|
||||||
h('div', { key: 'meta', className: 'mc-flex-split mc-kicker' }, [
|
h('div', { key: 'meta', className: 'mc-flex-split mc-kicker' }, [
|
||||||
h('span', { key: 'min' }, 'Min ' + fmtNumber(minY, 4)),
|
h('span', { key: 'left' }, `${axisLabel('left')} · Min ${fmtNumber(leftAxis.min, 4)} · Max ${fmtNumber(leftAxis.max, 4)}`),
|
||||||
h('span', { key: 'max' }, 'Max ' + fmtNumber(maxY, 4)),
|
rightAxis
|
||||||
|
? h('span', { key: 'right' }, `${axisLabel('right')} · Min ${fmtNumber(rightAxis.min, 4)} · Max ${fmtNumber(rightAxis.max, 4)}`)
|
||||||
|
: null,
|
||||||
]),
|
]),
|
||||||
|
hoveredPoint
|
||||||
|
? h('div', { key: 'hover', className: 'mc-mini-card' }, [
|
||||||
|
h('div', { key: 'label', className: 'mc-kicker', style: { color: hoveredPoint.color } }, hoveredPoint.label),
|
||||||
|
h('div', { key: 'value' }, `${hoveredPoint.x}: ${hoveredPoint.value}`),
|
||||||
|
])
|
||||||
|
: null,
|
||||||
h('svg', { key: 'svg', viewBox: `0 0 ${width} ${height}`, className: 'overflow-visible' }, [
|
h('svg', { key: 'svg', viewBox: `0 0 ${width} ${height}`, className: 'overflow-visible' }, [
|
||||||
h('g', { key: 'grid', stroke: 'rgba(255,255,255,0.08)' }, [
|
h('g', { key: 'grid', stroke: 'rgba(255,255,255,0.08)' }, [
|
||||||
h('line', { key: 'top', x1: padding, x2: width - padding, y1: padding, y2: padding }),
|
h('line', { key: 'top', x1: padding, x2: width - padding, y1: padding, y2: padding }),
|
||||||
h('line', { key: 'mid', x1: padding, x2: width - padding, y1: height / 2, y2: height / 2 }),
|
h('line', { key: 'mid', x1: padding, x2: width - padding, y1: height / 2, y2: height / 2 }),
|
||||||
h('line', { key: 'base', x1: padding, x2: width - padding, y1: height - padding, y2: height - padding }),
|
h('line', { key: 'base', x1: padding, x2: width - padding, y1: height - padding, y2: height - padding }),
|
||||||
]),
|
]),
|
||||||
|
h('g', { key: 'axis-labels', fill: 'rgba(148,163,184,0.95)', fontSize: 10 }, [
|
||||||
|
h('text', { key: 'left-top', x: padding, y: 14, textAnchor: 'start' }, axisLabel('left')),
|
||||||
|
rightAxis ? h('text', { key: 'right-top', x: width - padding, y: 14, textAnchor: 'end' }, axisLabel('right')) : null,
|
||||||
|
]),
|
||||||
props.type === 'bar' && !isMultiSeries
|
props.type === 'bar' && !isMultiSeries
|
||||||
? h('g', { key: 'bars' }, seriesCoords[0].coords.map((coord, index) => {
|
? h('g', { key: 'bars' }, seriesCoords[0].coords.map((coord, index) => {
|
||||||
const barWidth = Math.max(10, (((width - padding * 2) / Math.max(seriesCoords[0].coords.length - 1, 1)) * 0.6) || 24);
|
const barWidth = Math.max(10, (((width - padding * 2) / Math.max(seriesCoords[0].coords.length - 1, 1)) * 0.6) || 24);
|
||||||
@@ -770,6 +830,19 @@
|
|||||||
height: height - padding - coord[1],
|
height: height - padding - coord[1],
|
||||||
rx: 8,
|
rx: 8,
|
||||||
fill: 'rgba(59, 130, 246, 0.75)',
|
fill: 'rgba(59, 130, 246, 0.75)',
|
||||||
|
onMouseEnter: () => setHoveredPoint({
|
||||||
|
label: seriesCoords[0].label,
|
||||||
|
x: String(seriesCoords[0].data[index].x),
|
||||||
|
value: seriesCoords[0].data[index].formatted,
|
||||||
|
color: seriesCoords[0].color,
|
||||||
|
}),
|
||||||
|
onMouseLeave: () => !touchLike && setHoveredPoint(null),
|
||||||
|
onClick: () => setHoveredPoint({
|
||||||
|
label: seriesCoords[0].label,
|
||||||
|
x: String(seriesCoords[0].data[index].x),
|
||||||
|
value: seriesCoords[0].data[index].formatted,
|
||||||
|
color: seriesCoords[0].color,
|
||||||
|
}),
|
||||||
}, [
|
}, [
|
||||||
h('title', { key: 'title' }, pointTooltip(seriesCoords[0].label, seriesCoords[0].data[index])),
|
h('title', { key: 'title' }, pointTooltip(seriesCoords[0].label, seriesCoords[0].data[index])),
|
||||||
]);
|
]);
|
||||||
@@ -796,10 +869,24 @@
|
|||||||
key: `${item.key}-${pointIndex}`,
|
key: `${item.key}-${pointIndex}`,
|
||||||
cx: coord[0],
|
cx: coord[0],
|
||||||
cy: coord[1],
|
cy: coord[1],
|
||||||
r: 4,
|
r: 5,
|
||||||
fill: '#f8fafc',
|
fill: '#f8fafc',
|
||||||
stroke: item.color,
|
stroke: item.color,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
style: { cursor: 'pointer' },
|
||||||
|
onMouseEnter: () => setHoveredPoint({
|
||||||
|
label: item.label,
|
||||||
|
x: String(item.data[pointIndex].x),
|
||||||
|
value: item.data[pointIndex].formatted,
|
||||||
|
color: item.color,
|
||||||
|
}),
|
||||||
|
onMouseLeave: () => !touchLike && setHoveredPoint(null),
|
||||||
|
onClick: () => setHoveredPoint({
|
||||||
|
label: item.label,
|
||||||
|
x: String(item.data[pointIndex].x),
|
||||||
|
value: item.data[pointIndex].formatted,
|
||||||
|
color: item.color,
|
||||||
|
}),
|
||||||
}, [
|
}, [
|
||||||
h('title', { key: 'title' }, pointTooltip(item.label, item.data[pointIndex])),
|
h('title', { key: 'title' }, pointTooltip(item.label, item.data[pointIndex])),
|
||||||
]))));
|
]))));
|
||||||
@@ -1040,6 +1127,7 @@
|
|||||||
const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : [];
|
const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : [];
|
||||||
const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : [];
|
const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : [];
|
||||||
const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||||
|
const overviewPerDayLabel = `${currentMiningCurrency} pro Tag`;
|
||||||
const latestMeasurementDate = latest && latest.measured_at ? parseStoredUtcDate(latest.measured_at) : null;
|
const latestMeasurementDate = latest && latest.measured_at ? parseStoredUtcDate(latest.measured_at) : null;
|
||||||
const latestMeasurementAgeMs = latestMeasurementDate ? (Date.now() - latestMeasurementDate.getTime()) : null;
|
const latestMeasurementAgeMs = latestMeasurementDate ? (Date.now() - latestMeasurementDate.getTime()) : null;
|
||||||
const canTransferAll = !!latest
|
const canTransferAll = !!latest
|
||||||
@@ -1789,32 +1877,87 @@
|
|||||||
const overviewWindowDays = Number(payload?.bootstrap_meta?.overview_window_days || 15);
|
const overviewWindowDays = Number(payload?.bootstrap_meta?.overview_window_days || 15);
|
||||||
const overviewRows = recentMeasurementWindow(measurements, overviewWindowDays);
|
const overviewRows = recentMeasurementWindow(measurements, overviewWindowDays);
|
||||||
const performanceByDay = new Map();
|
const performanceByDay = new Map();
|
||||||
|
const pricingByDay = new Map();
|
||||||
|
|
||||||
overviewRows.forEach((row) => {
|
overviewRows.forEach((row) => {
|
||||||
const rate = Number(row.doge_per_day_interval);
|
const rate = Number(row.doge_per_day_interval);
|
||||||
|
const hashrate = Number(row.active_hashrate_mh);
|
||||||
const dayKey = String(row.measured_date || '').trim() || String(row.measured_at || '').slice(0, 10);
|
const dayKey = String(row.measured_date || '').trim() || String(row.measured_at || '').slice(0, 10);
|
||||||
if (!dayKey || !Number.isFinite(rate) || rate < 0) {
|
if (dayKey) {
|
||||||
|
if (Number.isFinite(rate) && rate >= 0) {
|
||||||
|
const existing = performanceByDay.get(dayKey) || { rateSum: 0, rateCount: 0, hashrateSum: 0, hashrateCount: 0, label: fmtDate(dayKey) };
|
||||||
|
existing.rateSum += rate;
|
||||||
|
existing.rateCount += 1;
|
||||||
|
if (Number.isFinite(hashrate) && hashrate >= 0) {
|
||||||
|
existing.hashrateSum += hashrate;
|
||||||
|
existing.hashrateCount += 1;
|
||||||
|
}
|
||||||
|
performanceByDay.set(dayKey, existing);
|
||||||
|
} else if (Number.isFinite(hashrate) && hashrate >= 0) {
|
||||||
|
const existing = performanceByDay.get(dayKey) || { rateSum: 0, rateCount: 0, hashrateSum: 0, hashrateCount: 0, label: fmtDate(dayKey) };
|
||||||
|
existing.hashrateSum += hashrate;
|
||||||
|
existing.hashrateCount += 1;
|
||||||
|
performanceByDay.set(dayKey, existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectivePrice = Number(row.effective_price_per_coin);
|
||||||
|
const convertedPrice = Number.isFinite(effectivePrice)
|
||||||
|
? convertMeasurementMoney(row, effectivePrice, reportCurrency)
|
||||||
|
: null;
|
||||||
|
if (!dayKey || !Number.isFinite(Number(convertedPrice))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = performanceByDay.get(dayKey) || { sum: 0, count: 0, label: fmtDate(dayKey) };
|
const priceEntry = pricingByDay.get(dayKey) || { sum: 0, count: 0, label: fmtDate(dayKey) };
|
||||||
existing.sum += rate;
|
priceEntry.sum += Number(convertedPrice);
|
||||||
existing.count += 1;
|
priceEntry.count += 1;
|
||||||
performanceByDay.set(dayKey, existing);
|
pricingByDay.set(dayKey, priceEntry);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
performance: Array.from(performanceByDay.entries())
|
performance: {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
key: 'doge-per-day',
|
||||||
|
label: overviewPerDayLabel,
|
||||||
|
color: '#2dd4bf',
|
||||||
|
axis: 'left',
|
||||||
|
formatter: (value) => `${fmtNumber(value, 4)} ${currentMiningCurrency}`,
|
||||||
|
data: Array.from(performanceByDay.entries())
|
||||||
|
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
||||||
|
.map(([, entry]) => ({
|
||||||
|
x: entry.label,
|
||||||
|
y: entry.rateCount > 0 ? (entry.rateSum / entry.rateCount) : null,
|
||||||
|
}))
|
||||||
|
.filter((point) => Number.isFinite(Number(point.y))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hashrate',
|
||||||
|
label: 'Aktive Hashrate',
|
||||||
|
color: '#60a5fa',
|
||||||
|
axis: 'right',
|
||||||
|
formatter: (value) => `${fmtNumber(value, 4)} MH/s`,
|
||||||
|
data: Array.from(performanceByDay.entries())
|
||||||
|
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
||||||
|
.map(([, entry]) => ({
|
||||||
|
x: entry.label,
|
||||||
|
y: entry.hashrateCount > 0 ? (entry.hashrateSum / entry.hashrateCount) : null,
|
||||||
|
}))
|
||||||
|
.filter((point) => Number.isFinite(Number(point.y))),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
pricing: Array.from(pricingByDay.entries())
|
||||||
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
.sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
||||||
.map(([, entry]) => ({
|
.map(([, entry]) => ({
|
||||||
x: entry.label,
|
x: entry.label,
|
||||||
y: entry.count > 0 ? (entry.sum / entry.count) : null,
|
y: entry.count > 0 ? (entry.sum / entry.count) : null,
|
||||||
|
formatted: entry.count > 0 ? fmtMoney(entry.sum / entry.count, reportCurrency) : null,
|
||||||
}))
|
}))
|
||||||
.filter((point) => Number.isFinite(Number(point.y))),
|
.filter((point) => Number.isFinite(Number(point.y))),
|
||||||
pricing: overviewRows.filter((row) => row.price_per_coin !== null)
|
|
||||||
.map((row) => ({ x: fmtDate(row.measured_at), y: row.price_per_coin })),
|
|
||||||
};
|
};
|
||||||
}, [currentSettings.crypto_currency, measurements, payload?.bootstrap_meta?.overview_window_days]);
|
}, [currentMiningCurrency, measurements, overviewPerDayLabel, payload?.bootstrap_meta?.overview_window_days, reportCurrency]);
|
||||||
|
|
||||||
async function saveMeasurement(raw, successMessage) {
|
async function saveMeasurement(raw, successMessage) {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -2866,14 +3009,6 @@
|
|||||||
value: latest ? fmtNumber(latest.coins_total_visible ?? latest.coins_total, 6) : 'n/a',
|
value: latest ? fmtNumber(latest.coins_total_visible ?? latest.coins_total, 6) : 'n/a',
|
||||||
sub: latest ? `Stand ${fmtDate(latest.measured_at)}` : '',
|
sub: latest ? `Stand ${fmtDate(latest.measured_at)}` : '',
|
||||||
}),
|
}),
|
||||||
h(StatCard, {
|
|
||||||
key: 'holdings',
|
|
||||||
label: `Theoretischer Bestand ${currentCoinCurrency}`,
|
|
||||||
value: payload?.summary?.payouts ? fmtNumber(holdingsCurrentAsset, 6) : 'n/a',
|
|
||||||
sub: payload?.summary?.payouts
|
|
||||||
? `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, {
|
h(StatCard, {
|
||||||
key: 'earned-overall',
|
key: 'earned-overall',
|
||||||
label: `Bisher verdient gesamt ${currentCoinCurrency}`,
|
label: `Bisher verdient gesamt ${currentCoinCurrency}`,
|
||||||
@@ -2912,17 +3047,24 @@
|
|||||||
h(StatCard, {
|
h(StatCard, {
|
||||||
key: 'value',
|
key: 'value',
|
||||||
label: 'Aktueller Gegenwert',
|
label: 'Aktueller Gegenwert',
|
||||||
value: latestValue !== null ? fmtMoney(latestValue, reportCurrency) : 'n/a',
|
value: totalHoldingsValue !== null ? fmtMoney(totalHoldingsValue, reportCurrency) : 'n/a',
|
||||||
sub: latestPrice !== null
|
sub: [
|
||||||
? `Kurs ${fmtNumber(latestPrice, 6)} ${reportCurrency}${latest && latest.price_is_fallback ? ' · Fallback aus letztem Kurs' : ''}`
|
holdingsCurrentAsset !== null && holdingsCurrentAsset !== undefined
|
||||||
: 'Kein umrechenbarer Kurs am letzten Punkt',
|
? `Gesamtbestand ${fmtNumber(holdingsCurrentAsset, 6)} ${currentCoinCurrency}`
|
||||||
|
: null,
|
||||||
|
latestValue !== null ? `Miner ${fmtMoney(latestValue, reportCurrency)}` : null,
|
||||||
|
walletValue !== null ? `Wallet ${fmtMoney(walletValue, reportCurrency)}` : null,
|
||||||
|
latestPrice !== null
|
||||||
|
? `Kurs ${fmtNumber(latestPrice, 6)} ${reportCurrency}${latest && latest.price_is_fallback ? ' · Fallback aus letztem Kurs' : ''}`
|
||||||
|
: null,
|
||||||
|
].filter(Boolean).join(' · ') || 'Kein umrechenbarer Kurs am letzten Punkt',
|
||||||
}),
|
}),
|
||||||
h(StatCard, {
|
h(StatCard, {
|
||||||
key: 'profit',
|
key: 'profit',
|
||||||
label: 'Theoretischer Tagesgewinn',
|
label: 'Theoretischer Tagesgewinn',
|
||||||
value: dailyProfit !== null ? fmtMoney(dailyProfit, reportCurrency) : 'n/a',
|
value: dailyProfit !== null ? fmtMoney(dailyProfit, reportCurrency) : 'n/a',
|
||||||
sub: dailyCost !== null
|
sub: dailyCost !== null
|
||||||
? `Tageskosten ${fmtMoney(dailyCost, reportCurrency)} · Walletwert ${walletValue !== null ? fmtMoney(walletValue, reportCurrency) : 'n/a'}`
|
? `Tageskosten ${fmtMoney(dailyCost, reportCurrency)}`
|
||||||
: 'Kein aktiver Miner fuer diese Waehrung',
|
: 'Kein aktiver Miner fuer diese Waehrung',
|
||||||
}),
|
}),
|
||||||
h(StatCard, {
|
h(StatCard, {
|
||||||
@@ -2934,7 +3076,7 @@
|
|||||||
? `${fmtNumber(breakEvenDaysOverall, 2)} Tage`
|
? `${fmtNumber(breakEvenDaysOverall, 2)} Tage`
|
||||||
: (investedCapital === null ? 'Keine Mietbasis' : 'Nicht erreichbar'),
|
: (investedCapital === null ? 'Keine Mietbasis' : 'Nicht erreichbar'),
|
||||||
sub: investedCapital !== null
|
sub: investedCapital !== null
|
||||||
? `${breakEvenEta ? `ETA ${breakEvenEta} · ` : ''}Cash ${fmtMoney(investedCapital, reportCurrency)}${reinvestedCapital !== null ? ` · Reinvest ${fmtMoney(reinvestedCapital, reportCurrency)}` : ''}${totalHoldingsValue !== null ? ` · Bestand ${fmtMoney(totalHoldingsValue, reportCurrency)}` : ''}`
|
? `${breakEvenEta ? `ETA ${breakEvenEta} · ` : ''}Cash ${fmtMoney(investedCapital, reportCurrency)}${reinvestedCapital !== null ? ` · Reinvest ${fmtMoney(reinvestedCapital, reportCurrency)}` : ''}`
|
||||||
: (breakEvenPrice !== null
|
: (breakEvenPrice !== null
|
||||||
? `Break-even-Kurs ${fmtNumber(breakEvenPrice, 6)} ${reportCurrency}`
|
? `Break-even-Kurs ${fmtNumber(breakEvenPrice, 6)} ${reportCurrency}`
|
||||||
: (investedCapital === null
|
: (investedCapital === null
|
||||||
@@ -2943,8 +3085,17 @@
|
|||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
h('div', { key: 'charts', className: 'mc-overview-grid' }, [
|
h('div', { key: 'charts', className: 'mc-overview-grid' }, [
|
||||||
panel('Performance-Verlauf', `${perDayLabel}-Raten der letzten 15 Tage als Tagesdurchschnitt ohne negative Ausreißer.`, h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
|
panel('Performance-Verlauf', `${perDayLabel}-Raten der letzten 15 Tage als Tagesdurchschnitt ohne negative Ausreißer plus aktive MH/s pro Tag.`, h(SimpleChart, {
|
||||||
panel('Kurs-Verlauf', 'Preiswerte der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })),
|
type: 'line',
|
||||||
|
series: overviewCharts.performance.series,
|
||||||
|
leftAxisLabel: perDayLabel,
|
||||||
|
rightAxisLabel: 'Hashrate MH/s',
|
||||||
|
})),
|
||||||
|
panel('Kurs-Verlauf', `Durchschnittlicher Kurs pro Tag der letzten 15 Tage in ${reportCurrency}.`, h(SimpleChart, {
|
||||||
|
type: 'line',
|
||||||
|
data: overviewCharts.pricing,
|
||||||
|
yLabel: `Kurs ${reportCurrency}`,
|
||||||
|
})),
|
||||||
]),
|
]),
|
||||||
panel('Zielmonitor', `Rest-${currentCoinCurrency} und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.`,
|
panel('Zielmonitor', `Rest-${currentCoinCurrency} und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.`,
|
||||||
h('div', { className: 'mc-target-grid' },
|
h('div', { className: 'mc-target-grid' },
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_cost_plans (
|
|||||||
payment_type VARCHAR(10) NOT NULL DEFAULT 'fiat',
|
payment_type VARCHAR(10) NOT NULL DEFAULT 'fiat',
|
||||||
total_cost_amount DECIMAL(20,8) NOT NULL,
|
total_cost_amount DECIMAL(20,8) NOT NULL,
|
||||||
currency VARCHAR(10) NOT NULL,
|
currency VARCHAR(10) NOT NULL,
|
||||||
|
daily_cost_amount DECIMAL(20,10) NULL,
|
||||||
|
daily_cost_currency VARCHAR(10) NULL,
|
||||||
note TEXT NULL,
|
note TEXT NULL,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -220,6 +222,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_purchased_miners (
|
|||||||
usd_reference_amount DECIMAL(20,8) NULL,
|
usd_reference_amount DECIMAL(20,8) NULL,
|
||||||
reference_price_amount DECIMAL(20,8) NULL,
|
reference_price_amount DECIMAL(20,8) NULL,
|
||||||
reference_price_currency VARCHAR(10) NULL,
|
reference_price_currency VARCHAR(10) NULL,
|
||||||
|
daily_cost_amount DECIMAL(20,10) NULL,
|
||||||
|
daily_cost_currency VARCHAR(10) NULL,
|
||||||
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_cost_plans (
|
|||||||
payment_type VARCHAR(10) NOT NULL DEFAULT 'fiat',
|
payment_type VARCHAR(10) NOT NULL DEFAULT 'fiat',
|
||||||
total_cost_amount NUMERIC(20,8) NOT NULL,
|
total_cost_amount NUMERIC(20,8) NOT NULL,
|
||||||
currency VARCHAR(10) NOT NULL,
|
currency VARCHAR(10) NOT NULL,
|
||||||
|
daily_cost_amount NUMERIC(20,10),
|
||||||
|
daily_cost_currency VARCHAR(10),
|
||||||
note TEXT,
|
note TEXT,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -237,6 +239,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_purchased_miners (
|
|||||||
usd_reference_amount NUMERIC(20,8),
|
usd_reference_amount NUMERIC(20,8),
|
||||||
reference_price_amount NUMERIC(20,8),
|
reference_price_amount NUMERIC(20,8),
|
||||||
reference_price_currency VARCHAR(10),
|
reference_price_currency VARCHAR(10),
|
||||||
|
daily_cost_amount NUMERIC(20,10),
|
||||||
|
daily_cost_currency VARCHAR(10),
|
||||||
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_cost_plans (
|
|||||||
payment_type VARCHAR(10) NOT NULL DEFAULT 'fiat',
|
payment_type VARCHAR(10) NOT NULL DEFAULT 'fiat',
|
||||||
total_cost_amount DECIMAL(20,8) NOT NULL,
|
total_cost_amount DECIMAL(20,8) NOT NULL,
|
||||||
currency VARCHAR(10) NOT NULL,
|
currency VARCHAR(10) NOT NULL,
|
||||||
|
daily_cost_amount DECIMAL(20,10) NULL,
|
||||||
|
daily_cost_currency VARCHAR(10) NULL,
|
||||||
note TEXT NULL,
|
note TEXT NULL,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -220,6 +222,8 @@ CREATE TABLE IF NOT EXISTS miningcheck_purchased_miners (
|
|||||||
usd_reference_amount DECIMAL(20,8) NULL,
|
usd_reference_amount DECIMAL(20,8) NULL,
|
||||||
reference_price_amount DECIMAL(20,8) NULL,
|
reference_price_amount DECIMAL(20,8) NULL,
|
||||||
reference_price_currency VARCHAR(10) NULL,
|
reference_price_currency VARCHAR(10) NULL,
|
||||||
|
daily_cost_amount DECIMAL(20,10) NULL,
|
||||||
|
daily_cost_currency VARCHAR(10) NULL,
|
||||||
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
|||||||
@@ -594,6 +594,8 @@ final class Router
|
|||||||
'auto_renew' => !empty($plan['auto_renew']) ? 1 : 0,
|
'auto_renew' => !empty($plan['auto_renew']) ? 1 : 0,
|
||||||
'total_cost_amount' => $plan['total_cost_amount'],
|
'total_cost_amount' => $plan['total_cost_amount'],
|
||||||
'currency' => $plan['currency'],
|
'currency' => $plan['currency'],
|
||||||
|
'daily_cost_amount' => $plan['daily_cost_amount'] ?? null,
|
||||||
|
'daily_cost_currency' => $plan['daily_cost_currency'] ?? null,
|
||||||
'note' => $plan['note'] ?? null,
|
'note' => $plan['note'] ?? null,
|
||||||
'is_active' => !empty($plan['is_active']) ? 1 : 0,
|
'is_active' => !empty($plan['is_active']) ? 1 : 0,
|
||||||
]);
|
]);
|
||||||
@@ -731,6 +733,8 @@ final class Router
|
|||||||
'usd_reference_amount' => $miner['usd_reference_amount'] ?? null,
|
'usd_reference_amount' => $miner['usd_reference_amount'] ?? null,
|
||||||
'reference_price_amount' => $miner['reference_price_amount'] ?? null,
|
'reference_price_amount' => $miner['reference_price_amount'] ?? null,
|
||||||
'reference_price_currency' => $miner['reference_price_currency'] ?? null,
|
'reference_price_currency' => $miner['reference_price_currency'] ?? null,
|
||||||
|
'daily_cost_amount' => $miner['daily_cost_amount'] ?? null,
|
||||||
|
'daily_cost_currency' => $miner['daily_cost_currency'] ?? null,
|
||||||
'auto_renew' => !empty($miner['auto_renew']) ? 1 : 0,
|
'auto_renew' => !empty($miner['auto_renew']) ? 1 : 0,
|
||||||
'note' => $miner['note'] ?? null,
|
'note' => $miner['note'] ?? null,
|
||||||
'is_active' => !empty($miner['is_active']) ? 1 : 0,
|
'is_active' => !empty($miner['is_active']) ? 1 : 0,
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ final class AnalyticsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$effectiveDailyCost = $includeFullDetail ? $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row) : null;
|
$effectiveDailyCost = $includeFullDetail ? $this->effectiveDailyCost($costPlans, $measuredTs, $priceCurrency, $row, $purchasedMiners) : null;
|
||||||
$currentValue = ($includeFullDetail && $price !== null) ? $visibleCoinsTotal * $price : null;
|
$currentValue = ($includeFullDetail && $price !== null) ? $visibleCoinsTotal * $price : null;
|
||||||
$currentValueEffective = ($includeFullDetail && $price !== null) ? $effectiveCoinsTotal * $price : null;
|
$currentValueEffective = ($includeFullDetail && $price !== null) ? $effectiveCoinsTotal * $price : null;
|
||||||
$theoreticalDailyRevenue = ($includeFullDetail && $price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null;
|
$theoreticalDailyRevenue = ($includeFullDetail && $price !== null && $perDayInterval !== null) ? $perDayInterval * $price : null;
|
||||||
@@ -636,7 +636,7 @@ final class AnalyticsService
|
|||||||
return $value === null ? null : round($value, $precision);
|
return $value === null ? null : round($value, $precision);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function effectiveDailyCost(array $costPlans, int $measurementTs, ?string $currency, ?array $fxContext = null): ?float
|
private function effectiveDailyCost(array $costPlans, int $measurementTs, ?string $currency, ?array $fxContext = null, array $purchasedMiners = []): ?float
|
||||||
{
|
{
|
||||||
if ($currency === null) {
|
if ($currency === null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -644,31 +644,56 @@ final class AnalyticsService
|
|||||||
|
|
||||||
$dailyTotal = 0.0;
|
$dailyTotal = 0.0;
|
||||||
$matched = false;
|
$matched = false;
|
||||||
|
|
||||||
|
$entries = [];
|
||||||
foreach ($costPlans as $plan) {
|
foreach ($costPlans as $plan) {
|
||||||
if (empty($plan['is_active'])) {
|
$entries[] = [
|
||||||
|
'entry' => $plan,
|
||||||
|
'start_field' => 'starts_at',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
foreach ($purchasedMiners as $miner) {
|
||||||
|
$entries[] = [
|
||||||
|
'entry' => $miner,
|
||||||
|
'start_field' => 'purchased_at',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($entries as $wrappedEntry) {
|
||||||
|
$entry = is_array($wrappedEntry['entry'] ?? null) ? $wrappedEntry['entry'] : [];
|
||||||
|
$startField = (string) ($wrappedEntry['start_field'] ?? 'starts_at');
|
||||||
|
if (empty($entry['is_active'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$startTs = $this->utcTimestamp((string) ($plan['starts_at'] ?? ''));
|
$startTs = $this->utcTimestamp((string) ($entry[$startField] ?? ''));
|
||||||
$runtimeMonths = (int) ($plan['runtime_months'] ?? 0);
|
$runtimeMonths = (int) ($entry['runtime_months'] ?? 0);
|
||||||
if ($startTs <= 0 || $runtimeMonths <= 0 || $measurementTs < $startTs) {
|
if ($startTs <= 0 || $runtimeMonths <= 0 || $measurementTs < $startTs) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$runtimeDays = $runtimeMonths * 30.4375;
|
if (!$this->entryIsCovered($entry, $measurementTs, $startField)) {
|
||||||
$endTs = (int) round($startTs + ($runtimeDays * 86400));
|
|
||||||
$isCovered = !empty($plan['auto_renew']) || $measurementTs <= $endTs;
|
|
||||||
if (!$isCovered) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$planDailyCost = (float) $plan['total_cost_amount'] / $runtimeDays;
|
$storedDailyCost = is_numeric($entry['daily_cost_amount'] ?? null) ? (float) $entry['daily_cost_amount'] : null;
|
||||||
$convertedDailyCost = $this->convertAmount(
|
$entryCurrency = strtoupper(trim((string) ($entry['daily_cost_currency'] ?? $entry['currency'] ?? '')));
|
||||||
$planDailyCost,
|
if ($entryCurrency === '') {
|
||||||
(string) ($plan['currency'] ?? ''),
|
continue;
|
||||||
$currency,
|
}
|
||||||
$fxContext
|
|
||||||
);
|
$planDailyCost = $storedDailyCost;
|
||||||
|
if ($planDailyCost === null || $planDailyCost <= 0) {
|
||||||
|
$amount = is_numeric($entry['total_cost_amount'] ?? null) ? (float) $entry['total_cost_amount'] : null;
|
||||||
|
if ($amount === null || $amount <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$runtimeDays = $runtimeMonths * 30.4375;
|
||||||
|
$planDailyCost = $amount / $runtimeDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
$convertedDailyCost = $this->convertAmount($planDailyCost, $entryCurrency, $currency, $fxContext);
|
||||||
if ($convertedDailyCost === null) {
|
if ($convertedDailyCost === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -996,7 +1021,7 @@ final class AnalyticsService
|
|||||||
|
|
||||||
$runtimeDays = $runtimeMonths * 30.4375;
|
$runtimeDays = $runtimeMonths * 30.4375;
|
||||||
$endTs = (int) round($startTs + ($runtimeDays * 86400));
|
$endTs = (int) round($startTs + ($runtimeDays * 86400));
|
||||||
return !empty($entry['auto_renew']) || $checkTs <= $endTs;
|
return $this->entryAutoRenewEnabled($entry) || $checkTs <= $endTs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeHashrateMh(mixed $value, mixed $unit): float
|
private function normalizeHashrateMh(mixed $value, mixed $unit): float
|
||||||
@@ -1018,6 +1043,29 @@ final class AnalyticsService
|
|||||||
return array_map(fn (mixed $entry): mixed => is_array($entry) ? $this->normalizeLegacyHashrateEntry($entry) : $entry, $entries);
|
return array_map(fn (mixed $entry): mixed => is_array($entry) ? $this->normalizeLegacyHashrateEntry($entry) : $entry, $entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function entryAutoRenewEnabled(array $entry): bool
|
||||||
|
{
|
||||||
|
if ($this->entryIsCryptoPaid($entry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !empty($entry['auto_renew']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entryIsCryptoPaid(array $entry): bool
|
||||||
|
{
|
||||||
|
$paymentType = strtolower(trim((string) ($entry['payment_type'] ?? '')));
|
||||||
|
if ($paymentType === 'crypto') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currency = strtoupper(trim((string) ($entry['currency'] ?? '')));
|
||||||
|
return in_array($currency, [
|
||||||
|
'ADA', 'ARB', 'AVAX', 'BNB', 'BTC', 'CTC', 'DAI', 'DOGE', 'DOT', 'ETH',
|
||||||
|
'HSH', 'LINK', 'LTC', 'MATIC', 'SOL', 'TRX', 'USDC', 'USDT', 'XMR', 'XRP',
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeLegacyHashrateEntry(array $entry): array
|
private function normalizeLegacyHashrateEntry(array $entry): array
|
||||||
{
|
{
|
||||||
$label = trim((string) ($entry['label'] ?? ''));
|
$label = trim((string) ($entry['label'] ?? ''));
|
||||||
@@ -1522,7 +1570,7 @@ final class AnalyticsService
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($entry['auto_renew']) || $runtimeMonths <= 0) {
|
if ($this->entryAutoRenewEnabled($entry) || $runtimeMonths <= 0) {
|
||||||
return $days - $startIndex;
|
return $days - $startIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1807,7 +1855,7 @@ final class AnalyticsService
|
|||||||
|
|
||||||
private function entryFundingSource(array $entry): string
|
private function entryFundingSource(array $entry): string
|
||||||
{
|
{
|
||||||
return !empty($entry['auto_renew']) ? 'cash' : 'reinvest';
|
return $this->entryIsCryptoPaid($entry) ? 'reinvest' : 'cash';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function investmentBasisAmount(array $entry): ?float
|
private function investmentBasisAmount(array $entry): ?float
|
||||||
@@ -1933,7 +1981,7 @@ final class AnalyticsService
|
|||||||
|
|
||||||
private function entryCoverageEndTimestamp(array $entry, string $startField = 'starts_at'): ?int
|
private function entryCoverageEndTimestamp(array $entry, string $startField = 'starts_at'): ?int
|
||||||
{
|
{
|
||||||
if (!empty($entry['auto_renew'])) {
|
if ($this->entryAutoRenewEnabled($entry)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,10 +239,12 @@ final class MiningRepository
|
|||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
'INSERT INTO ' . $this->table('cost_plans') . ' (
|
'INSERT INTO ' . $this->table('cost_plans') . ' (
|
||||||
project_key, owner_sub, label, starts_at, runtime_months, mining_speed_value, mining_speed_unit,
|
project_key, owner_sub, label, starts_at, runtime_months, mining_speed_value, mining_speed_unit,
|
||||||
bonus_speed_value, bonus_speed_unit, auto_renew, base_price_amount, payment_type, total_cost_amount, currency, note, is_active
|
bonus_speed_value, bonus_speed_unit, auto_renew, base_price_amount, payment_type, total_cost_amount, currency,
|
||||||
|
daily_cost_amount, daily_cost_currency, note, is_active
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_key, :owner_sub, :label, :starts_at, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
:project_key, :owner_sub, :label, :starts_at, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
||||||
:bonus_speed_value, :bonus_speed_unit, :auto_renew, :base_price_amount, :payment_type, :total_cost_amount, :currency, :note, :is_active
|
:bonus_speed_value, :bonus_speed_unit, :auto_renew, :base_price_amount, :payment_type, :total_cost_amount, :currency,
|
||||||
|
:daily_cost_amount, :daily_cost_currency, :note, :is_active
|
||||||
)
|
)
|
||||||
RETURNING *'
|
RETURNING *'
|
||||||
);
|
);
|
||||||
@@ -253,10 +255,12 @@ final class MiningRepository
|
|||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
'INSERT INTO ' . $this->table('cost_plans') . ' (
|
'INSERT INTO ' . $this->table('cost_plans') . ' (
|
||||||
project_key, owner_sub, label, starts_at, runtime_months, mining_speed_value, mining_speed_unit,
|
project_key, owner_sub, label, starts_at, runtime_months, mining_speed_value, mining_speed_unit,
|
||||||
bonus_speed_value, bonus_speed_unit, auto_renew, base_price_amount, payment_type, total_cost_amount, currency, note, is_active
|
bonus_speed_value, bonus_speed_unit, auto_renew, base_price_amount, payment_type, total_cost_amount, currency,
|
||||||
|
daily_cost_amount, daily_cost_currency, note, is_active
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_key, :owner_sub, :label, :starts_at, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
:project_key, :owner_sub, :label, :starts_at, :runtime_months, :mining_speed_value, :mining_speed_unit,
|
||||||
:bonus_speed_value, :bonus_speed_unit, :auto_renew, :base_price_amount, :payment_type, :total_cost_amount, :currency, :note, :is_active
|
:bonus_speed_value, :bonus_speed_unit, :auto_renew, :base_price_amount, :payment_type, :total_cost_amount, :currency,
|
||||||
|
:daily_cost_amount, :daily_cost_currency, :note, :is_active
|
||||||
)'
|
)'
|
||||||
);
|
);
|
||||||
$stmt->execute($this->normalizeInsertPayload($projectKey, $payload));
|
$stmt->execute($this->normalizeInsertPayload($projectKey, $payload));
|
||||||
@@ -1001,11 +1005,13 @@ final class MiningRepository
|
|||||||
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
||||||
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
||||||
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
||||||
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency,
|
||||||
|
daily_cost_amount, daily_cost_currency, auto_renew, note, is_active
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
||||||
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
||||||
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency,
|
||||||
|
:daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active
|
||||||
)
|
)
|
||||||
RETURNING *'
|
RETURNING *'
|
||||||
);
|
);
|
||||||
@@ -1017,11 +1023,13 @@ final class MiningRepository
|
|||||||
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
||||||
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
||||||
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
||||||
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency,
|
||||||
|
daily_cost_amount, daily_cost_currency, auto_renew, note, is_active
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
||||||
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
||||||
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency,
|
||||||
|
:daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active
|
||||||
)'
|
)'
|
||||||
);
|
);
|
||||||
$stmt->execute($this->normalizePurchasedPayload($projectKey, $offerId, $payload));
|
$stmt->execute($this->normalizePurchasedPayload($projectKey, $offerId, $payload));
|
||||||
@@ -1049,6 +1057,8 @@ final class MiningRepository
|
|||||||
'usd_reference_amount' => $payload['usd_reference_amount'] ?? null,
|
'usd_reference_amount' => $payload['usd_reference_amount'] ?? null,
|
||||||
'reference_price_amount' => $payload['reference_price_amount'] ?? null,
|
'reference_price_amount' => $payload['reference_price_amount'] ?? null,
|
||||||
'reference_price_currency' => $payload['reference_price_currency'] ?? null,
|
'reference_price_currency' => $payload['reference_price_currency'] ?? null,
|
||||||
|
'daily_cost_amount' => $payload['daily_cost_amount'] ?? $this->deriveDailyCostAmount($payload['total_cost_amount'] ?? null, $payload['runtime_months'] ?? null),
|
||||||
|
'daily_cost_currency' => $payload['daily_cost_currency'] ?? ($payload['currency'] ?? null),
|
||||||
'auto_renew' => $payload['auto_renew'] ?? 0,
|
'auto_renew' => $payload['auto_renew'] ?? 0,
|
||||||
'note' => $payload['note'] ?? null,
|
'note' => $payload['note'] ?? null,
|
||||||
'is_active' => $payload['is_active'] ?? 1,
|
'is_active' => $payload['is_active'] ?? 1,
|
||||||
@@ -1059,11 +1069,13 @@ final class MiningRepository
|
|||||||
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
||||||
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
||||||
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
||||||
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency,
|
||||||
|
daily_cost_amount, daily_cost_currency, auto_renew, note, is_active
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
||||||
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
||||||
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency,
|
||||||
|
:daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active
|
||||||
)
|
)
|
||||||
RETURNING *'
|
RETURNING *'
|
||||||
);
|
);
|
||||||
@@ -1075,11 +1087,13 @@ final class MiningRepository
|
|||||||
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
'INSERT INTO ' . $this->table('purchased_miners') . ' (
|
||||||
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
project_key, owner_sub, miner_offer_id, purchased_at, label, runtime_months,
|
||||||
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
mining_speed_value, mining_speed_unit, bonus_speed_value, bonus_speed_unit,
|
||||||
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency, auto_renew, note, is_active
|
total_cost_amount, currency, usd_reference_amount, reference_price_amount, reference_price_currency,
|
||||||
|
daily_cost_amount, daily_cost_currency, auto_renew, note, is_active
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
:project_key, :owner_sub, :miner_offer_id, :purchased_at, :label, :runtime_months,
|
||||||
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
:mining_speed_value, :mining_speed_unit, :bonus_speed_value, :bonus_speed_unit,
|
||||||
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency, :auto_renew, :note, :is_active
|
:total_cost_amount, :currency, :usd_reference_amount, :reference_price_amount, :reference_price_currency,
|
||||||
|
:daily_cost_amount, :daily_cost_currency, :auto_renew, :note, :is_active
|
||||||
)'
|
)'
|
||||||
);
|
);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
@@ -1567,6 +1581,9 @@ final class MiningRepository
|
|||||||
|
|
||||||
private function normalizeInsertPayload(string $projectKey, array $payload): array
|
private function normalizeInsertPayload(string $projectKey, array $payload): array
|
||||||
{
|
{
|
||||||
|
$dailyCostAmount = $payload['daily_cost_amount'] ?? $this->deriveDailyCostAmount($payload['total_cost_amount'] ?? null, $payload['runtime_months'] ?? null);
|
||||||
|
$dailyCostCurrency = $payload['daily_cost_currency'] ?? ($payload['currency'] ?? null);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'project_key' => $projectKey,
|
'project_key' => $projectKey,
|
||||||
'owner_sub' => $this->ownerSub,
|
'owner_sub' => $this->ownerSub,
|
||||||
@@ -1582,6 +1599,8 @@ final class MiningRepository
|
|||||||
'payment_type' => $payload['payment_type'] ?? 'fiat',
|
'payment_type' => $payload['payment_type'] ?? 'fiat',
|
||||||
'total_cost_amount' => $payload['total_cost_amount'],
|
'total_cost_amount' => $payload['total_cost_amount'],
|
||||||
'currency' => $payload['currency'],
|
'currency' => $payload['currency'],
|
||||||
|
'daily_cost_amount' => $dailyCostAmount,
|
||||||
|
'daily_cost_currency' => $dailyCostCurrency,
|
||||||
'note' => $payload['note'],
|
'note' => $payload['note'],
|
||||||
'is_active' => $payload['is_active'],
|
'is_active' => $payload['is_active'],
|
||||||
];
|
];
|
||||||
@@ -1608,6 +1627,9 @@ final class MiningRepository
|
|||||||
|
|
||||||
private function normalizePurchasedPayload(string $projectKey, ?int $offerId, array $payload): array
|
private function normalizePurchasedPayload(string $projectKey, ?int $offerId, array $payload): array
|
||||||
{
|
{
|
||||||
|
$dailyCostAmount = $payload['daily_cost_amount'] ?? $this->deriveDailyCostAmount($payload['total_cost_amount'] ?? null, $payload['runtime_months'] ?? null);
|
||||||
|
$dailyCostCurrency = $payload['daily_cost_currency'] ?? ($payload['currency'] ?? null);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'project_key' => $projectKey,
|
'project_key' => $projectKey,
|
||||||
'owner_sub' => $this->ownerSub,
|
'owner_sub' => $this->ownerSub,
|
||||||
@@ -1624,12 +1646,33 @@ final class MiningRepository
|
|||||||
'usd_reference_amount' => $payload['usd_reference_amount'],
|
'usd_reference_amount' => $payload['usd_reference_amount'],
|
||||||
'reference_price_amount' => $payload['reference_price_amount'] ?? null,
|
'reference_price_amount' => $payload['reference_price_amount'] ?? null,
|
||||||
'reference_price_currency' => $payload['reference_price_currency'] ?? null,
|
'reference_price_currency' => $payload['reference_price_currency'] ?? null,
|
||||||
|
'daily_cost_amount' => $dailyCostAmount,
|
||||||
|
'daily_cost_currency' => $dailyCostCurrency,
|
||||||
'auto_renew' => $payload['auto_renew'] ?? 0,
|
'auto_renew' => $payload['auto_renew'] ?? 0,
|
||||||
'note' => $payload['note'],
|
'note' => $payload['note'],
|
||||||
'is_active' => $payload['is_active'],
|
'is_active' => $payload['is_active'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function deriveDailyCostAmount(mixed $totalCostAmount, mixed $runtimeMonths): ?float
|
||||||
|
{
|
||||||
|
if (!is_numeric($totalCostAmount) || !is_numeric($runtimeMonths)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$months = (float) $runtimeMonths;
|
||||||
|
if ($months <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$runtimeDays = $months * 30.4375;
|
||||||
|
if ($runtimeDays <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round(((float) $totalCostAmount) / $runtimeDays, 10);
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeRows(array $rows): array
|
private function normalizeRows(array $rows): array
|
||||||
{
|
{
|
||||||
return array_map(fn (array $row): array => $this->normalizeRow($row), $rows);
|
return array_map(fn (array $row): array => $this->normalizeRow($row), $rows);
|
||||||
|
|||||||
@@ -178,6 +178,19 @@ final class SchemaManager
|
|||||||
$applied[] = 'cost_plan_columns';
|
$applied[] = 'cost_plan_columns';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
($this->tableExists($this->prefix . 'cost_plans') && (
|
||||||
|
!$this->columnExists($this->prefix . 'cost_plans', 'daily_cost_amount') ||
|
||||||
|
!$this->columnExists($this->prefix . 'cost_plans', 'daily_cost_currency')
|
||||||
|
)) ||
|
||||||
|
($this->tableExists($this->prefix . 'purchased_miners') && (
|
||||||
|
!$this->columnExists($this->prefix . 'purchased_miners', 'daily_cost_amount') ||
|
||||||
|
!$this->columnExists($this->prefix . 'purchased_miners', 'daily_cost_currency')
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
$this->upgradeServerDailyCostColumns();
|
||||||
|
$applied[] = 'server_daily_cost_columns';
|
||||||
|
}
|
||||||
|
|
||||||
$settingsColumns = $this->existingColumns($this->prefix . 'settings', ['preferred_currencies', 'report_currency', 'crypto_currency', 'display_timezone', 'fx_max_age_hours', 'module_theme_mode', 'module_theme_accent']);
|
$settingsColumns = $this->existingColumns($this->prefix . 'settings', ['preferred_currencies', 'report_currency', 'crypto_currency', 'display_timezone', 'fx_max_age_hours', 'module_theme_mode', 'module_theme_accent']);
|
||||||
if (!in_array('preferred_currencies', $settingsColumns, true) || !in_array('report_currency', $settingsColumns, true) || !in_array('crypto_currency', $settingsColumns, true) || !in_array('display_timezone', $settingsColumns, true) || !in_array('fx_max_age_hours', $settingsColumns, true) || !in_array('module_theme_mode', $settingsColumns, true) || !in_array('module_theme_accent', $settingsColumns, true)) {
|
if (!in_array('preferred_currencies', $settingsColumns, true) || !in_array('report_currency', $settingsColumns, true) || !in_array('crypto_currency', $settingsColumns, true) || !in_array('display_timezone', $settingsColumns, true) || !in_array('fx_max_age_hours', $settingsColumns, true) || !in_array('module_theme_mode', $settingsColumns, true) || !in_array('module_theme_accent', $settingsColumns, true)) {
|
||||||
@@ -665,6 +678,19 @@ final class SchemaManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ([$this->prefix . 'cost_plans', $this->prefix . 'purchased_miners'] as $serverTable) {
|
||||||
|
if (
|
||||||
|
in_array($serverTable, $presentTables, true) &&
|
||||||
|
(
|
||||||
|
!$this->columnExists($serverTable, 'daily_cost_amount') ||
|
||||||
|
!$this->columnExists($serverTable, 'daily_cost_currency')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$upgrades[] = 'server_daily_cost_columns';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->tableExists($this->prefix . 'settings')) {
|
if ($this->tableExists($this->prefix . 'settings')) {
|
||||||
$requiredSettingsColumns = ['preferred_currencies', 'report_currency', 'crypto_currency', 'display_timezone', 'fx_max_age_hours', 'module_theme_mode', 'module_theme_accent'];
|
$requiredSettingsColumns = ['preferred_currencies', 'report_currency', 'crypto_currency', 'display_timezone', 'fx_max_age_hours', 'module_theme_mode', 'module_theme_accent'];
|
||||||
$existingSettingsColumns = $this->existingColumns($this->prefix . 'settings', $requiredSettingsColumns);
|
$existingSettingsColumns = $this->existingColumns($this->prefix . 'settings', $requiredSettingsColumns);
|
||||||
@@ -1252,6 +1278,8 @@ final class SchemaManager
|
|||||||
usd_reference_amount NUMERIC(20,8),
|
usd_reference_amount NUMERIC(20,8),
|
||||||
reference_price_amount NUMERIC(20,8),
|
reference_price_amount NUMERIC(20,8),
|
||||||
reference_price_currency VARCHAR(10),
|
reference_price_currency VARCHAR(10),
|
||||||
|
daily_cost_amount NUMERIC(20,10),
|
||||||
|
daily_cost_currency VARCHAR(10),
|
||||||
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
@@ -1278,6 +1306,8 @@ final class SchemaManager
|
|||||||
usd_reference_amount DECIMAL(20,8) NULL,
|
usd_reference_amount DECIMAL(20,8) NULL,
|
||||||
reference_price_amount DECIMAL(20,8) NULL,
|
reference_price_amount DECIMAL(20,8) NULL,
|
||||||
reference_price_currency VARCHAR(10) NULL,
|
reference_price_currency VARCHAR(10) NULL,
|
||||||
|
daily_cost_amount DECIMAL(20,10) NULL,
|
||||||
|
daily_cost_currency VARCHAR(10) NULL,
|
||||||
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
@@ -1451,6 +1481,40 @@ final class SchemaManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function upgradeServerDailyCostColumns(): void
|
||||||
|
{
|
||||||
|
foreach ([$this->prefix . 'cost_plans', $this->prefix . 'purchased_miners'] as $table) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statements = $this->driver === 'pgsql'
|
||||||
|
? [
|
||||||
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS daily_cost_amount NUMERIC(20,10)',
|
||||||
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS daily_cost_currency VARCHAR(10)',
|
||||||
|
'UPDATE ' . $table . ' SET daily_cost_amount = ROUND((total_cost_amount / NULLIF(runtime_months * 30.4375, 0))::numeric, 10) WHERE daily_cost_amount IS NULL AND total_cost_amount IS NOT NULL AND runtime_months IS NOT NULL AND runtime_months > 0',
|
||||||
|
'UPDATE ' . $table . ' SET daily_cost_currency = COALESCE(daily_cost_currency, currency) WHERE currency IS NOT NULL',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'ALTER TABLE `' . $table . '` ADD COLUMN daily_cost_amount DECIMAL(20,10) NULL',
|
||||||
|
'ALTER TABLE `' . $table . '` ADD COLUMN daily_cost_currency VARCHAR(10) NULL',
|
||||||
|
'UPDATE `' . $table . '` SET daily_cost_amount = ROUND(total_cost_amount / NULLIF(runtime_months * 30.4375, 0), 10) WHERE daily_cost_amount IS NULL AND total_cost_amount IS NOT NULL AND runtime_months IS NOT NULL AND runtime_months > 0',
|
||||||
|
'UPDATE `' . $table . '` SET daily_cost_currency = COALESCE(daily_cost_currency, currency) WHERE currency IS NOT NULL',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($statements as $statement) {
|
||||||
|
try {
|
||||||
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer Server-Tageskosten fehlgeschlagen.');
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function upgradeTargetOfferColumn(): void
|
private function upgradeTargetOfferColumn(): void
|
||||||
{
|
{
|
||||||
$table = $this->prefix . 'targets';
|
$table = $this->prefix . 'targets';
|
||||||
|
|||||||
Reference in New Issue
Block a user