fix(v2)(ui): rewire LP dashboard JS to call the new dca_lp endpoints
The static JS was orphaned at a prior backend refactor: it called
/registration-status, /register, /dashboard/summary, /dashboard/
transactions, /dashboard/analytics — none of which exist in views_api.
First render hit `Failed to check registration status` and the
"Welcome to DCA" form's submit failed with `Failed to register for DCA`.
This commit rewires the data layer to the v2 endpoints:
- GET /api/v1/dca-client/preferences (auto-onboards the LP on load
— the act of opening this dashboard is what creates the LP's
dca_lp row, which unlocks deposit creation on the operator side)
- GET /api/v1/dca-client/positions (404 → empty-state, not error)
- GET /api/v1/dca-client/transactions
The legacy "Welcome / register" wizard in index.html is now dead
(`isRegistered` defaults to true and `loadPreferences` always succeeds
because the backend auto-creates). The chart panel is also dead (no
backend /analytics endpoint). Stubs added for `registerClient`,
`loadChartData`, plus `chartLoading` / `chartTimeRange` /
`analyticsData` data fields so the template renders without
undefined-binding warnings even though those branches never execute.
Future cleanup: the registration card + chart panel HTML can be
deleted from the template, and a preferences-editor card (PUT
/preferences) added. Out of scope here — the priority was unblocking
E2E testing on v2-bitspire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7dac898a10
commit
18010de363
1 changed files with 160 additions and 704 deletions
|
|
@ -1,12 +1,28 @@
|
||||||
|
// Satoshi Machine Client v2 — LP dashboard JS.
|
||||||
|
//
|
||||||
|
// Maintenance-mode reminder: the rich LP UI is moving to ~/dev/webapp.
|
||||||
|
// This file is the lightweight in-LNbits dashboard kept functional for
|
||||||
|
// dev / E2E testing on the v2-bitspire branch. Endpoints:
|
||||||
|
// GET /satmachineclient/api/v1/dca-client/preferences (auto-onboards)
|
||||||
|
// PUT /satmachineclient/api/v1/dca-client/preferences
|
||||||
|
// GET /satmachineclient/api/v1/dca-client/positions
|
||||||
|
// GET /satmachineclient/api/v1/dca-client/transactions
|
||||||
|
//
|
||||||
|
// The "registration / welcome" wizard in the template is dead — every
|
||||||
|
// call to GET /preferences auto-creates the LP's dca_lp row on first
|
||||||
|
// hit, so `isRegistered` is always true after the initial load and the
|
||||||
|
// wizard never shows. Cleanup of the template is deferred.
|
||||||
|
|
||||||
window.app = Vue.createApp({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
delimiters: ['${', '}'],
|
delimiters: ['${', '}'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
// Registration state
|
// Registration / onboarding (legacy; always true after initial
|
||||||
isRegistered: false,
|
// /preferences call because the endpoint auto-creates).
|
||||||
registrationChecked: false,
|
isRegistered: true,
|
||||||
|
registrationChecked: true,
|
||||||
registrationForm: {
|
registrationForm: {
|
||||||
selectedWallet: null,
|
selectedWallet: null,
|
||||||
dca_mode: 'flow',
|
dca_mode: 'flow',
|
||||||
|
|
@ -14,7 +30,11 @@ window.app = Vue.createApp({
|
||||||
username: ''
|
username: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
// Admin configuration
|
// LP preferences (loaded from dca_lp on first call).
|
||||||
|
preferences: null,
|
||||||
|
|
||||||
|
// Admin configuration (legacy: no longer fetched — kept so the
|
||||||
|
// legacy template doesn't undefined-error on `adminConfig.*`).
|
||||||
adminConfig: {
|
adminConfig: {
|
||||||
max_daily_limit_gtq: 2000,
|
max_daily_limit_gtq: 2000,
|
||||||
currency: 'GTQ'
|
currency: 'GTQ'
|
||||||
|
|
@ -25,260 +45,165 @@ window.app = Vue.createApp({
|
||||||
transactions: [],
|
transactions: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
showFiatValues: false, // Hide fiat values by default
|
showFiatValues: false,
|
||||||
|
// Stubs for legacy template bindings (chart + registration form
|
||||||
|
// are dead branches but still in the .html — keep these defined
|
||||||
|
// so Vue doesn't emit warnings on initial render).
|
||||||
|
chartLoading: false,
|
||||||
|
chartTimeRange: '30d',
|
||||||
|
analyticsData: null,
|
||||||
transactionColumns: [
|
transactionColumns: [
|
||||||
{
|
{name: 'date', label: 'Date', align: 'left',
|
||||||
name: 'date',
|
field: row => row.transaction_time || row.created_at, sortable: false},
|
||||||
label: 'Date',
|
{name: 'amount_sats', label: 'Bitcoin', align: 'right',
|
||||||
align: 'left',
|
field: 'amount_sats', sortable: false},
|
||||||
field: row => row.transaction_time || row.created_at,
|
{name: 'amount_fiat', label: 'Fiat Amount', align: 'right',
|
||||||
sortable: false
|
field: 'amount_fiat', sortable: false},
|
||||||
},
|
{name: 'type', label: 'Type', align: 'center',
|
||||||
{
|
field: 'leg_type', sortable: false},
|
||||||
name: 'amount_sats',
|
{name: 'status', label: 'Status', align: 'center',
|
||||||
label: 'Bitcoin',
|
field: 'status', sortable: false}
|
||||||
align: 'right',
|
|
||||||
field: 'amount_sats',
|
|
||||||
sortable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'amount_fiat',
|
|
||||||
label: 'Fiat Amount',
|
|
||||||
align: 'right',
|
|
||||||
field: 'amount_fiat',
|
|
||||||
sortable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
label: 'Type',
|
|
||||||
align: 'center',
|
|
||||||
field: 'transaction_type',
|
|
||||||
sortable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'status',
|
|
||||||
label: 'Status',
|
|
||||||
align: 'center',
|
|
||||||
field: 'status',
|
|
||||||
sortable: false
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
transactionPagination: {
|
transactionPagination: {
|
||||||
sortBy: 'date',
|
sortBy: 'date',
|
||||||
descending: true,
|
descending: true,
|
||||||
page: 1,
|
page: 1,
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
},
|
}
|
||||||
chartTimeRange: '30d',
|
|
||||||
dcaChart: null,
|
|
||||||
analyticsData: null,
|
|
||||||
chartLoading: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// Configuration Methods
|
// -----------------------------------------------------------------
|
||||||
async loadClientLimits() {
|
// Onboarding + preferences
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
async loadPreferences() {
|
||||||
|
// GET /preferences auto-creates the LP's dca_lp row with the
|
||||||
|
// authenticated wallet as the default DCA destination. This is
|
||||||
|
// the structural enforcement of the "LP must onboard before
|
||||||
|
// deposits work" gate on the operator side.
|
||||||
try {
|
try {
|
||||||
const { data } = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/satmachineadmin/api/v1/dca/client-limits'
|
'/satmachineclient/api/v1/dca-client/preferences',
|
||||||
// No authentication required - public endpoint with safe data only
|
|
||||||
)
|
|
||||||
|
|
||||||
this.adminConfig = data
|
|
||||||
console.log('Client limits loaded:', this.adminConfig)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading client limits:', error)
|
|
||||||
// Keep default values if client limits fail to load
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Registration Methods
|
|
||||||
async checkRegistrationStatus() {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
'/satmachineclient/api/v1/registration-status',
|
|
||||||
this.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
|
this.preferences = data
|
||||||
this.isRegistered = data.is_registered
|
|
||||||
this.registrationChecked = true
|
|
||||||
|
|
||||||
if (!this.isRegistered) {
|
|
||||||
// Fetch current user info to get the username
|
|
||||||
await this.loadCurrentUser()
|
|
||||||
this.registrationForm.selectedWallet = this.g.user.wallets[0]?.id || null
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking registration status:', error)
|
|
||||||
this.error = 'Failed to check registration status'
|
|
||||||
this.registrationChecked = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadCurrentUser() {
|
|
||||||
try {
|
|
||||||
const { data } = await LNbits.api.getAuthenticatedUser()
|
|
||||||
|
|
||||||
// Set username from API response with priority: display_name > username > email > fallback
|
|
||||||
const username = data.extra?.display_name || data.username || data.email
|
|
||||||
this.registrationForm.username = (username !== null && username !== undefined && username !== '')
|
|
||||||
? username
|
|
||||||
: `user_${this.g.user.id.substring(0, 8)}`
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading current user:', error)
|
|
||||||
// Fallback to generated username
|
|
||||||
this.registrationForm.username = `user_${this.g.user.id.substring(0, 8)}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async registerClient() {
|
|
||||||
try {
|
|
||||||
// Prepare registration data using the form's username (already loaded from API)
|
|
||||||
const registrationData = {
|
|
||||||
dca_mode: this.registrationForm.dca_mode,
|
|
||||||
fixed_mode_daily_limit: this.registrationForm.fixed_mode_daily_limit,
|
|
||||||
username: this.registrationForm.username || `user_${this.g.user.id.substring(0, 8)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the selected wallet object to get the adminkey
|
|
||||||
const selectedWallet = this.g.user.wallets.find(w => w.id === this.registrationForm.selectedWallet)
|
|
||||||
if (!selectedWallet) {
|
|
||||||
throw new Error('Selected wallet not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/satmachineclient/api/v1/register',
|
|
||||||
selectedWallet.adminkey,
|
|
||||||
registrationData
|
|
||||||
)
|
|
||||||
|
|
||||||
this.isRegistered = true
|
this.isRegistered = true
|
||||||
|
this.registrationChecked = true
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: data.message || 'Successfully registered for DCA!',
|
|
||||||
icon: 'check_circle',
|
|
||||||
position: 'top'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Load dashboard data after successful registration
|
|
||||||
await this.loadDashboardData()
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error registering client:', error)
|
console.error('Error loading preferences:', error)
|
||||||
this.$q.notify({
|
this.error = 'Failed to load DCA preferences'
|
||||||
type: 'negative',
|
this.registrationChecked = true
|
||||||
message: error.detail || 'Failed to register for DCA',
|
|
||||||
position: 'top'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Dashboard Methods
|
// -----------------------------------------------------------------
|
||||||
|
// Formatting helpers
|
||||||
|
// -----------------------------------------------------------------
|
||||||
formatCurrency(amount) {
|
formatCurrency(amount) {
|
||||||
if (!amount) return 'Q 0.00';
|
if (!amount) return 'Q 0.00'
|
||||||
// Amount is already in GTQ
|
|
||||||
const gtqAmount = amount;
|
|
||||||
return new Intl.NumberFormat('es-GT', {
|
return new Intl.NumberFormat('es-GT', {
|
||||||
style: 'currency',
|
style: 'currency', currency: 'GTQ'
|
||||||
currency: 'GTQ',
|
}).format(amount)
|
||||||
}).format(gtqAmount);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatCurrencyWithCode(amount, currencyCode) {
|
formatCurrencyWithCode(amount, currencyCode) {
|
||||||
if (!amount) return `${currencyCode} 0.00`;
|
if (!amount) return `${currencyCode} 0.00`
|
||||||
// Amount is already in GTQ
|
|
||||||
const currencyAmount = amount;
|
|
||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat('en-US', {
|
return new Intl.NumberFormat('en-US', {
|
||||||
style: 'currency',
|
style: 'currency', currency: currencyCode
|
||||||
currency: currencyCode,
|
}).format(amount)
|
||||||
}).format(currencyAmount);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fallback if currency code is not supported
|
return `${currencyCode} ${amount.toFixed(2)}`
|
||||||
return `${currencyCode} ${currencyAmount.toFixed(2)}`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
formatDate(dateString) {
|
formatDate(dateString) {
|
||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) return 'Invalid Date'
|
||||||
console.warn('Invalid date string:', dateString)
|
|
||||||
return 'Invalid Date'
|
|
||||||
}
|
|
||||||
return date.toLocaleDateString()
|
return date.toLocaleDateString()
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime(dateString) {
|
formatTime(dateString) {
|
||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) return 'Invalid Time'
|
||||||
console.warn('Invalid time string:', dateString)
|
return date.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'})
|
||||||
return 'Invalid Time'
|
|
||||||
}
|
|
||||||
return date.toLocaleTimeString('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatSats(amount) {
|
formatSats(amount) {
|
||||||
if (!amount) return '0 sats'
|
if (!amount) return '0 sats'
|
||||||
const formatted = new Intl.NumberFormat('en-US').format(amount)
|
const formatted = new Intl.NumberFormat('en-US').format(amount)
|
||||||
// Add some excitement for larger amounts with consistent 5x→2x progression
|
if (amount >= 100000000) return formatted + ' sats 🏆'
|
||||||
if (amount >= 100000000) return formatted + ' sats 🏆' // Full coiner (1 BTC)
|
if (amount >= 50000000) return formatted + ' sats 🎆'
|
||||||
if (amount >= 50000000) return formatted + ' sats 🎆' // Bitcoin baron
|
if (amount >= 10000000) return formatted + ' sats 👑'
|
||||||
if (amount >= 10000000) return formatted + ' sats 👑' // Bitcoin royalty
|
if (amount >= 5000000) return formatted + ' sats 🏆'
|
||||||
if (amount >= 5000000) return formatted + ' sats 🏆' // Verified bag holder
|
if (amount >= 1000000) return formatted + ' sats 🌟'
|
||||||
if (amount >= 1000000) return formatted + ' sats 🌟' // Millionaire
|
if (amount >= 500000) return formatted + ' sats 🔥'
|
||||||
if (amount >= 500000) return formatted + ' sats 🔥' // Half million
|
if (amount >= 100000) return formatted + ' sats 🚀'
|
||||||
if (amount >= 100000) return formatted + ' sats 🚀' // Getting serious
|
if (amount >= 50000) return formatted + ' sats ⚡'
|
||||||
if (amount >= 50000) return formatted + ' sats ⚡' // Lightning quick
|
if (amount >= 10000) return formatted + ' sats 🎯'
|
||||||
if (amount >= 10000) return formatted + ' sats 🎯' // First milestone
|
|
||||||
return formatted + ' sats'
|
return formatted + ' sats'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Dashboard data
|
||||||
|
// -----------------------------------------------------------------
|
||||||
async loadDashboardData() {
|
async loadDashboardData() {
|
||||||
try {
|
try {
|
||||||
const { data } = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/satmachineclient/api/v1/dashboard/summary',
|
'/satmachineclient/api/v1/dca-client/positions',
|
||||||
this.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
|
// Backend returns ClientDashboardSummary with no dca_mode field
|
||||||
|
// at the top level any more (it's LP-wide and lives on
|
||||||
|
// preferences); echo `default_dca_mode` from prefs so the
|
||||||
|
// legacy template renderers (`dashboardData.dca_mode`) keep
|
||||||
|
// working until the template is rewritten.
|
||||||
|
data.dca_mode = this.preferences?.default_dca_mode || 'flow'
|
||||||
|
data.dca_status = 'active'
|
||||||
this.dashboardData = data
|
this.dashboardData = data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading dashboard data:', error)
|
// 404 from /positions = "LP isn't enrolled at any machine yet",
|
||||||
this.error = 'Failed to load dashboard data'
|
// which is a valid state, not an error. Show an empty dashboard.
|
||||||
|
if (error?.response?.status === 404) {
|
||||||
|
this.dashboardData = {
|
||||||
|
user_id: this.g.user.id,
|
||||||
|
total_sats_accumulated: 0,
|
||||||
|
total_fiat_invested: 0,
|
||||||
|
current_fiat_balance: 0,
|
||||||
|
pending_fiat_deposits: 0,
|
||||||
|
average_cost_basis: 0,
|
||||||
|
current_sats_fiat_value: 0,
|
||||||
|
total_transactions: 0,
|
||||||
|
total_machines: 0,
|
||||||
|
last_transaction_date: null,
|
||||||
|
currency: this.adminConfig.currency,
|
||||||
|
positions: [],
|
||||||
|
dca_mode: this.preferences?.default_dca_mode || 'flow',
|
||||||
|
dca_status: 'awaiting_enrolment'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error loading dashboard data:', error)
|
||||||
|
this.error = 'Failed to load dashboard data'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadTransactions() {
|
async loadTransactions() {
|
||||||
try {
|
try {
|
||||||
const { data } = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/satmachineclient/api/v1/dashboard/transactions?limit=50',
|
'/satmachineclient/api/v1/dca-client/transactions?limit=50',
|
||||||
this.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
|
|
||||||
// Debug: Log the first transaction to see date format
|
|
||||||
if (data.length > 0) {
|
|
||||||
console.log('Sample transaction data:', data[0])
|
|
||||||
console.log('transaction_time:', data[0].transaction_time)
|
|
||||||
console.log('created_at:', data[0].created_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by most recent first and store
|
|
||||||
this.transactions = data.sort((a, b) => {
|
this.transactions = data.sort((a, b) => {
|
||||||
const dateA = new Date(a.transaction_time || a.created_at)
|
const dateA = new Date(a.transaction_time || a.created_at)
|
||||||
const dateB = new Date(b.transaction_time || b.created_at)
|
const dateB = new Date(b.transaction_time || b.created_at)
|
||||||
return dateB - dateA // Most recent first
|
return dateB - dateA
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading transactions:', error)
|
console.error('Error loading transactions:', error)
|
||||||
|
|
@ -294,6 +219,7 @@ window.app = Vue.createApp({
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
this.loadPreferences(),
|
||||||
this.loadDashboardData(),
|
this.loadDashboardData(),
|
||||||
this.loadTransactions()
|
this.loadTransactions()
|
||||||
])
|
])
|
||||||
|
|
@ -315,502 +241,67 @@ window.app = Vue.createApp({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Milestone widget (purely cosmetic)
|
||||||
|
// -----------------------------------------------------------------
|
||||||
getNextMilestone() {
|
getNextMilestone() {
|
||||||
if (!this.dashboardData) return { target: 10000, name: '10k sats' }
|
if (!this.dashboardData) return {target: 10000, name: '10k sats'}
|
||||||
const sats = this.dashboardData.total_sats_accumulated
|
const sats = this.dashboardData.total_sats_accumulated
|
||||||
|
if (sats < 10000) return {target: 10000, name: '10k sats'}
|
||||||
// Consistent 5x→2x progression pattern
|
if (sats < 50000) return {target: 50000, name: '50k sats'}
|
||||||
if (sats < 10000) return { target: 10000, name: '10k sats' }
|
if (sats < 100000) return {target: 100000, name: '100k sats'}
|
||||||
if (sats < 50000) return { target: 50000, name: '50k sats' }
|
if (sats < 500000) return {target: 500000, name: '500k sats'}
|
||||||
if (sats < 100000) return { target: 100000, name: '100k sats' }
|
if (sats < 1000000) return {target: 1000000, name: '1M sats'}
|
||||||
if (sats < 500000) return { target: 500000, name: '500k sats' }
|
if (sats < 5000000) return {target: 5000000, name: '5M sats'}
|
||||||
if (sats < 1000000) return { target: 1000000, name: '1M sats' }
|
if (sats < 10000000) return {target: 10000000, name: '10M sats'}
|
||||||
if (sats < 5000000) return { target: 5000000, name: '5M sats' }
|
if (sats < 50000000) return {target: 50000000, name: '50M sats'}
|
||||||
if (sats < 10000000) return { target: 10000000, name: '10M sats' }
|
if (sats < 100000000) return {target: 100000000, name: '100M sats (1 BTC!)'}
|
||||||
if (sats < 50000000) return { target: 50000000, name: '50M sats' }
|
return {target: 500000000, name: '500M sats (5 BTC)'}
|
||||||
if (sats < 100000000) return { target: 100000000, name: '100M sats (1 BTC!)' }
|
|
||||||
return { target: 500000000, name: '500M sats (5 BTC)' }
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getMilestoneProgress() {
|
getMilestoneProgress() {
|
||||||
if (!this.dashboardData) {
|
if (!this.dashboardData) return 0
|
||||||
console.log('getMilestoneProgress: no dashboard data')
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
const sats = this.dashboardData.total_sats_accumulated
|
const sats = this.dashboardData.total_sats_accumulated
|
||||||
const milestone = this.getNextMilestone()
|
const milestone = this.getNextMilestone()
|
||||||
|
|
||||||
// Show total progress toward the next milestone (from 0)
|
|
||||||
const progress = (sats / milestone.target) * 100
|
const progress = (sats / milestone.target) * 100
|
||||||
const result = Math.min(Math.max(progress, 0), 100)
|
return Math.min(Math.max(progress, 0), 100)
|
||||||
console.log('getMilestoneProgress:', { sats, milestone, progress, result })
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
async loadChartData() {
|
|
||||||
// Prevent multiple simultaneous requests
|
|
||||||
if (this.chartLoading) {
|
|
||||||
console.log('Chart already loading, ignoring request')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.chartLoading = true
|
|
||||||
|
|
||||||
// Destroy existing chart immediately to prevent conflicts
|
|
||||||
if (this.dcaChart) {
|
|
||||||
console.log('Destroying existing chart before loading new data')
|
|
||||||
this.dcaChart.destroy()
|
|
||||||
this.dcaChart = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/satmachineclient/api/v1/dashboard/analytics?time_range=${this.chartTimeRange}`,
|
|
||||||
this.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
|
|
||||||
// Debug: Log analytics data
|
|
||||||
console.log('Analytics data received:', data)
|
|
||||||
if (data && data.cost_basis_history && data.cost_basis_history.length > 0) {
|
|
||||||
console.log('Sample cost basis point:', data.cost_basis_history[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
this.analyticsData = data
|
|
||||||
|
|
||||||
// Wait for DOM update and ensure we're still in loading state
|
|
||||||
await this.$nextTick()
|
|
||||||
|
|
||||||
// Double-check we're still the active loading request
|
|
||||||
if (this.chartLoading) {
|
|
||||||
this.initDCAChart()
|
|
||||||
} else {
|
|
||||||
console.log('Chart loading was cancelled, skipping initialization')
|
|
||||||
this.chartLoading = false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading chart data:', error)
|
|
||||||
this.chartLoading = false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
initDCAChart() {
|
// -----------------------------------------------------------------
|
||||||
console.log('initDCAChart called')
|
// Stubs for the legacy registration wizard + chart panel.
|
||||||
console.log('analyticsData:', this.analyticsData)
|
// Those template branches are dead (the wizard never shows because
|
||||||
console.log('dcaChart ref:', this.$refs.dcaChart)
|
// isRegistered is true after auto-onboard; the chart panel has no
|
||||||
console.log('chartLoading state:', this.chartLoading)
|
// backend analytics endpoint to feed it) but the .html still
|
||||||
|
// references these handlers. Keep stubs so a stray click doesn't
|
||||||
// Skip if we're not in a loading state (indicates this is a stale call)
|
// throw an uncaught error.
|
||||||
if (!this.chartLoading && this.dcaChart) {
|
// -----------------------------------------------------------------
|
||||||
console.log('Chart already exists and not loading, skipping initialization')
|
async registerClient() {
|
||||||
return
|
// Old "register / pick wallet & mode" form is gone. Preferences
|
||||||
}
|
// are auto-created and editable via a separate path (TODO: add
|
||||||
|
// an editor card to this dashboard once the template gets a
|
||||||
if (!this.analyticsData) {
|
// proper rewrite).
|
||||||
console.log('No analytics data available')
|
await this.loadPreferences()
|
||||||
return
|
this.$q.notify({
|
||||||
}
|
type: 'info',
|
||||||
|
message: 'Your DCA account is already set up — refreshed.',
|
||||||
if (!this.$refs.dcaChart) {
|
position: 'top'
|
||||||
console.log('No chart ref available, waiting for DOM...')
|
|
||||||
// Try again after DOM update, but only if still loading
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.dcaChart && this.chartLoading) {
|
|
||||||
this.initDCAChart()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Chart.js is loaded
|
|
||||||
if (typeof Chart === 'undefined') {
|
|
||||||
console.error('Chart.js is not loaded')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Chart.js version:', Chart.version || 'unknown')
|
|
||||||
console.log('Chart.js available:', typeof Chart)
|
|
||||||
|
|
||||||
// Destroy existing chart (redundant safety check)
|
|
||||||
if (this.dcaChart) {
|
|
||||||
console.log('Destroying existing chart in initDCAChart')
|
|
||||||
this.dcaChart.destroy()
|
|
||||||
this.dcaChart = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = this.$refs.dcaChart.getContext('2d')
|
|
||||||
|
|
||||||
// Use accumulation_timeline data which is already grouped by day
|
|
||||||
const timelineData = this.analyticsData.accumulation_timeline || []
|
|
||||||
console.log('Timeline data sample:', timelineData.slice(0, 2)) // Debug first 2 records
|
|
||||||
|
|
||||||
// If we have timeline data, use it (already grouped by day)
|
|
||||||
if (timelineData.length > 0) {
|
|
||||||
// Calculate running totals from daily data
|
|
||||||
let runningSats = 0
|
|
||||||
const labels = []
|
|
||||||
const cumulativeSats = []
|
|
||||||
|
|
||||||
timelineData.forEach(point => {
|
|
||||||
// Ensure sats is a valid number
|
|
||||||
const sats = point.sats || 0
|
|
||||||
const validSats = typeof sats === 'number' ? sats : parseFloat(sats) || 0
|
|
||||||
runningSats += validSats
|
|
||||||
|
|
||||||
const date = new Date(point.date)
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
labels.push(date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
}))
|
|
||||||
cumulativeSats.push(runningSats)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Timeline chart data:', { labels, cumulativeSats })
|
|
||||||
|
|
||||||
this.createChart(labels, cumulativeSats)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to cost_basis_history but group by date to avoid duplicates
|
|
||||||
console.log('No timeline data, using cost_basis_history as fallback')
|
|
||||||
const chartData = this.analyticsData.cost_basis_history || []
|
|
||||||
console.log('Chart data sample:', chartData.slice(0, 2)) // Debug first 2 records
|
|
||||||
|
|
||||||
// Handle empty data case
|
|
||||||
if (chartData.length === 0) {
|
|
||||||
console.log('No chart data available')
|
|
||||||
// Create gradient for placeholder chart
|
|
||||||
const placeholderGradient = ctx.createLinearGradient(0, 0, 0, 300)
|
|
||||||
placeholderGradient.addColorStop(0, 'rgba(255, 149, 0, 0.3)')
|
|
||||||
placeholderGradient.addColorStop(1, 'rgba(255, 149, 0, 0.05)')
|
|
||||||
|
|
||||||
// Show placeholder chart with enhanced styling
|
|
||||||
this.dcaChart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: ['Start Your DCA Journey'],
|
|
||||||
datasets: [{
|
|
||||||
label: 'Total Sats Accumulated',
|
|
||||||
data: [0],
|
|
||||||
borderColor: '#FF9500',
|
|
||||||
backgroundColor: placeholderGradient,
|
|
||||||
borderWidth: 3,
|
|
||||||
fill: true,
|
|
||||||
tension: 0.4,
|
|
||||||
pointRadius: 8,
|
|
||||||
pointBackgroundColor: '#FFFFFF',
|
|
||||||
pointBorderColor: '#FF9500',
|
|
||||||
pointBorderWidth: 3,
|
|
||||||
pointHoverRadius: 10
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
titleColor: '#FFFFFF',
|
|
||||||
bodyColor: '#FFFFFF',
|
|
||||||
borderColor: '#FF9500',
|
|
||||||
borderWidth: 2,
|
|
||||||
cornerRadius: 8,
|
|
||||||
callbacks: {
|
|
||||||
label: function (context) {
|
|
||||||
return `${context.parsed.y.toLocaleString()} sats`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
grid: { display: false },
|
|
||||||
ticks: {
|
|
||||||
color: '#666666',
|
|
||||||
font: { size: 12, weight: '500' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
beginAtZero: true,
|
|
||||||
grid: {
|
|
||||||
color: 'rgba(255, 149, 0, 0.1)',
|
|
||||||
drawBorder: false
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: '#666666',
|
|
||||||
font: { size: 12, weight: '500' },
|
|
||||||
callback: function (value) {
|
|
||||||
return value.toLocaleString() + ' sats'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Clear loading state after creating placeholder chart
|
|
||||||
this.chartLoading = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group cost_basis_history by date to eliminate duplicates
|
|
||||||
const groupedData = new Map()
|
|
||||||
chartData.forEach(point => {
|
|
||||||
const dateStr = new Date(point.date).toDateString()
|
|
||||||
if (!groupedData.has(dateStr)) {
|
|
||||||
groupedData.set(dateStr, point)
|
|
||||||
} else {
|
|
||||||
// Use the latest cumulative values for the same date
|
|
||||||
const existing = groupedData.get(dateStr)
|
|
||||||
if (point.cumulative_sats > existing.cumulative_sats) {
|
|
||||||
groupedData.set(dateStr, point)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const uniqueChartData = Array.from(groupedData.values()).sort((a, b) =>
|
|
||||||
new Date(a.date).getTime() - new Date(b.date).getTime()
|
|
||||||
)
|
|
||||||
|
|
||||||
const labels = uniqueChartData.map(point => {
|
|
||||||
// Handle different date formats with enhanced timezone handling
|
|
||||||
let date;
|
|
||||||
if (point.date) {
|
|
||||||
console.log('Raw date from API:', point.date); // Debug the actual date string
|
|
||||||
|
|
||||||
// If it's an ISO string with timezone info, parse it correctly
|
|
||||||
if (typeof point.date === 'string' && point.date.includes('T')) {
|
|
||||||
// ISO string - parse and convert to local date
|
|
||||||
date = new Date(point.date);
|
|
||||||
// For display purposes, use the date part only to avoid timezone shifts
|
|
||||||
const localDateStr = date.getFullYear() + '-' +
|
|
||||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(date.getDate()).padStart(2, '0');
|
|
||||||
date = new Date(localDateStr + 'T00:00:00'); // Force local midnight
|
|
||||||
} else {
|
|
||||||
date = new Date(point.date);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if date is valid
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
date = new Date();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
date = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Formatted date:', date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
|
|
||||||
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const cumulativeSats = uniqueChartData.map(point => {
|
|
||||||
// Ensure cumulative_sats is a valid number
|
|
||||||
const sats = point.cumulative_sats || 0
|
|
||||||
return typeof sats === 'number' ? sats : parseFloat(sats) || 0
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Final chart data:', { labels, cumulativeSats })
|
|
||||||
console.log('Labels array:', labels)
|
|
||||||
console.log('CumulativeSats array:', cumulativeSats)
|
|
||||||
|
|
||||||
// Validate data before creating chart
|
|
||||||
if (labels.length === 0 || cumulativeSats.length === 0) {
|
|
||||||
console.warn('No valid data for chart, skipping creation')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labels.length !== cumulativeSats.length) {
|
|
||||||
console.warn('Mismatched data arrays:', { labelsLength: labels.length, dataLength: cumulativeSats.length })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for any invalid values in cumulativeSats
|
|
||||||
const hasInvalidValues = cumulativeSats.some(val => val === null || val === undefined || isNaN(val))
|
|
||||||
if (hasInvalidValues) {
|
|
||||||
console.warn('Invalid values found in cumulative sats:', cumulativeSats)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createChart(labels, cumulativeSats)
|
|
||||||
},
|
},
|
||||||
|
loadChartData() {
|
||||||
createChart(labels, cumulativeSats) {
|
// No backend analytics endpoint; chart panel is dead.
|
||||||
console.log('createChart called with loading state:', this.chartLoading)
|
|
||||||
|
|
||||||
if (!this.$refs.dcaChart) {
|
|
||||||
console.log('Chart ref not available for createChart')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if we're not in a loading state (indicates this is a stale call)
|
|
||||||
if (!this.chartLoading) {
|
|
||||||
console.log('Not in loading state, skipping createChart')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy existing chart
|
|
||||||
if (this.dcaChart) {
|
|
||||||
console.log('Destroying existing chart in createChart')
|
|
||||||
this.dcaChart.destroy()
|
|
||||||
this.dcaChart = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = this.$refs.dcaChart.getContext('2d')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create gradient for the area fill
|
|
||||||
const gradient = ctx.createLinearGradient(0, 0, 0, 300)
|
|
||||||
gradient.addColorStop(0, 'rgba(255, 149, 0, 0.4)')
|
|
||||||
gradient.addColorStop(0.5, 'rgba(255, 149, 0, 0.2)')
|
|
||||||
gradient.addColorStop(1, 'rgba(255, 149, 0, 0.05)')
|
|
||||||
|
|
||||||
// Small delay to ensure Chart.js is fully initialized
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
// Final check to ensure we're still in the correct loading state
|
|
||||||
if (!this.chartLoading) {
|
|
||||||
console.log('Loading state changed during timeout, aborting chart creation')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dcaChart = new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
label: 'Total Sats Accumulated',
|
|
||||||
data: cumulativeSats,
|
|
||||||
borderColor: '#FF9500',
|
|
||||||
backgroundColor: gradient,
|
|
||||||
borderWidth: 3,
|
|
||||||
fill: true,
|
|
||||||
tension: 0.4,
|
|
||||||
pointBackgroundColor: '#FFFFFF',
|
|
||||||
pointBorderColor: '#FF9500',
|
|
||||||
pointBorderWidth: 3,
|
|
||||||
pointRadius: 6,
|
|
||||||
pointHoverRadius: 8,
|
|
||||||
pointHoverBackgroundColor: '#FFFFFF',
|
|
||||||
pointHoverBorderColor: '#FF7700',
|
|
||||||
pointHoverBorderWidth: 4
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: 'index',
|
|
||||||
intersect: false,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
titleColor: '#FFFFFF',
|
|
||||||
bodyColor: '#FFFFFF',
|
|
||||||
borderColor: '#FF9500',
|
|
||||||
borderWidth: 2,
|
|
||||||
cornerRadius: 8,
|
|
||||||
displayColors: false,
|
|
||||||
callbacks: {
|
|
||||||
title: function (context) {
|
|
||||||
return `📅 ${context[0].label}`
|
|
||||||
},
|
|
||||||
label: function (context) {
|
|
||||||
return `⚡ ${context.parsed.y.toLocaleString()} sats accumulated`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
display: true,
|
|
||||||
grid: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: '#666666',
|
|
||||||
font: {
|
|
||||||
size: 12,
|
|
||||||
weight: '500'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
display: true,
|
|
||||||
beginAtZero: true,
|
|
||||||
grid: {
|
|
||||||
color: 'rgba(255, 149, 0, 0.1)',
|
|
||||||
drawBorder: false
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: '#666666',
|
|
||||||
font: {
|
|
||||||
size: 12,
|
|
||||||
weight: '500'
|
|
||||||
},
|
|
||||||
callback: function (value) {
|
|
||||||
if (value >= 1000000) {
|
|
||||||
return (value / 1000000).toFixed(1) + 'M sats'
|
|
||||||
} else if (value >= 1000) {
|
|
||||||
return (value / 1000).toFixed(0) + 'k sats'
|
|
||||||
}
|
|
||||||
return value.toLocaleString() + ' sats'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
interaction: {
|
|
||||||
mode: 'nearest',
|
|
||||||
axis: 'x',
|
|
||||||
intersect: false
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
hoverRadius: 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log('Chart created successfully in createChart!')
|
|
||||||
// Chart is now created, clear loading state
|
|
||||||
this.chartLoading = false
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in createChart setTimeout:', error)
|
|
||||||
this.chartLoading = false
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating Chart.js chart in createChart:', error)
|
|
||||||
console.log('Chart data that failed:', { labels, cumulativeSats })
|
|
||||||
// Clear loading state on error
|
|
||||||
this.chartLoading = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
// Auto-onboard on load (creates dca_lp row if missing).
|
||||||
// Load client limits first
|
await this.loadPreferences()
|
||||||
await this.loadClientLimits()
|
// Then load dashboard + transactions.
|
||||||
|
await Promise.all([
|
||||||
// Check registration status
|
this.loadDashboardData(),
|
||||||
await this.checkRegistrationStatus()
|
this.loadTransactions()
|
||||||
|
])
|
||||||
// Only load dashboard data if registered
|
|
||||||
if (this.isRegistered) {
|
|
||||||
await Promise.all([
|
|
||||||
this.loadDashboardData(),
|
|
||||||
this.loadTransactions(),
|
|
||||||
this.loadChartData()
|
|
||||||
])
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing dashboard:', error)
|
console.error('Error initializing dashboard:', error)
|
||||||
this.error = 'Failed to initialize dashboard'
|
this.error = 'Failed to initialize dashboard'
|
||||||
|
|
@ -819,23 +310,6 @@ window.app = Vue.createApp({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
// Initialize chart after DOM is ready and data is loaded
|
|
||||||
this.$nextTick(() => {
|
|
||||||
console.log('Component mounted, checking for chart initialization')
|
|
||||||
console.log('Loading state:', this.loading)
|
|
||||||
console.log('Chart ref available:', !!this.$refs.dcaChart)
|
|
||||||
console.log('Analytics data available:', !!this.analyticsData)
|
|
||||||
|
|
||||||
if (this.analyticsData && this.$refs.dcaChart) {
|
|
||||||
console.log('Initializing chart from mounted hook')
|
|
||||||
this.initDCAChart()
|
|
||||||
} else {
|
|
||||||
console.log('Chart will initialize after data loads')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hasData() {
|
hasData() {
|
||||||
return this.dashboardData && !this.loading && this.isRegistered
|
return this.dashboardData && !this.loading && this.isRegistered
|
||||||
|
|
@ -848,23 +322,5 @@ window.app = Vue.createApp({
|
||||||
value: wallet.id
|
value: wallet.id
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
analyticsData: {
|
|
||||||
handler(newData) {
|
|
||||||
if (newData && !this.chartLoading && !this.dcaChart) {
|
|
||||||
console.log('Analytics data changed and no chart exists, initializing chart...')
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// Only initialize if we don't have a chart and aren't currently loading
|
|
||||||
if (!this.dcaChart && !this.chartLoading) {
|
|
||||||
this.chartLoading = true
|
|
||||||
this.initDCAChart()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue