Files
taxbaik/legacy/smartadmin/scripts/pages/subscriptiondashboard.js
T
kjh2064 40cffb3beb
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m26s
fix: implement Blazor-native login form to properly update authentication state
Problem: JavaScript login form saved tokens to localStorage but didn't notify
CustomAuthenticationStateProvider, causing [Authorize] pages to remain in
'loading' state indefinitely. The provider only reads tokens when:
1. GetAuthenticationStateAsync() is called (page load)
2. NotifyAuthenticationStateChanged() is triggered (UI updates)

But JavaScript login didn't trigger either, leaving the authentication state
stale.

Solution: Convert AdminLoginForm from HTML+JavaScript to pure Blazor component.
Now the login flow is:
1. User enters credentials in Blazor form
2. HttpClient POST to /api/auth/login
3. Save tokens to localStorage
4. Call CustomAuthenticationStateProvider.LoginAsync() directly
5. Blazor detects auth state change and re-evaluates [Authorize] pages
6. Dashboard [Authorize] page renders successfully

Result: Immediate authentication state update, no loading timeout on protected pages.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-03 13:03:53 +09:00

727 lines
26 KiB
JavaScript

import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function () {
'use strict';
/***************************************************************/
/* Subscription Dashboard Chart #subscription-chart */
/***************************************************************/
if (document.getElementById('subscription-chart')) {
const categories = ['2025-01', '2025-02', '2025-03', '2025-04', '2025-05', '2025-06', '2025-07', '2025-08', '2025-09', '2025-10', '2025-11', '2025-12'];
const visitsData = [23686, 30820, 59622, 146465, 78160, 79520, 36148, 48721, 158303, 155174, 104830, 86895];
const subscriptionsData = [1545, 1350, 1270, 1830, 1955, 1865, 2034, 2544, 1956, 2211, 1540, 1670];
const subscriptionChartOptions = {
series: [
{
name: 'Visits',
type: 'area',
data: visitsData
},
{
name: 'Subscriptions',
type: 'line',
data: subscriptionsData
}
],
chart: {
height: 335,
type: 'line',
zoom: {
enabled: false
},
stacked: false,
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.bootstrapVars.bodyColor.rgba(0.1), // Visits (gray, area)
window.colorMap.primary[400].hex // Subscriptions (teal, line)
],
stroke: {
width: [1, 2],
curve: 'smooth',
colors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.8), window.colorMap.primary[400].hex],
dashArray: [4, 0], // Visits dashed, Subscriptions solid
},
fill: {
type: ['solid', 'solid'],
opacity: [0.15, 1],
},
markers: {
size: [3, 3],
colors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.7), window.colorMap.primary[600].hex],
strokeColors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.7), window.colorMap.primary[600].hex],
strokeWidth: 2,
hover: {
sizeOffset: 2
}
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: [
{
seriesName: 'Visits',
min: 20000,
max: 170000,
tickAmount: 6,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
},
formatter: function (val) {
return val.toLocaleString();
}
}
},
{
seriesName: 'Subscriptions',
opposite: true,
min: 1200,
max: 2700,
tickAmount: 6,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
},
formatter: function (val) {
return val.toLocaleString();
}
}
}
],
legend: {
show: true,
position: 'top',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 18,
height: 6,
radius: 2
},
itemMargin: {
horizontal: 12,
vertical: 0
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3,
yaxis: {
lines: {
show: true
}
},
xaxis: {
lines: {
show: false
}
}
},
tooltip: {
shared: true,
intersect: false,
theme: 'dark',
y: [
{
formatter: function (val) {
return val.toLocaleString();
}
},
{
formatter: function (val) {
return val.toLocaleString();
}
}
]
}
};
const subscriptionChart = new ApexCharts(
document.getElementById('subscription-chart'),
subscriptionChartOptions
);
subscriptionChart.render();
}
/***************************************************************/
/* User Activity Chart #user-activity-chart */
/***************************************************************/
if (document.getElementById('user-activity-chart')) {
const categories = ['Blogging', 'Videos', 'Ads', 'Comments', 'Shares', 'Likes', 'Funny'];
const userActivityChartOptions = {
series: [
{
name: 'Morning',
data: [65, 59, 90, 81, 56, 55, 40]
},
{
name: 'Night',
data: [28, 48, 40, 19, 96, 27, 100]
}
],
chart: {
height: 350,
width: '100%',
type: 'radar',
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0,
sparkline: {
enabled: false
},
margin: 0,
padding: {
top: -10,
right: -10,
bottom: -10,
left: -10
}
},
colors: [
window.colorMap.success[400].hex, // Morning (light purple)
window.colorMap.primary[400].hex // Night (teal)
],
stroke: {
width: 0,
colors: [window.colorMap.success[400].hex, window.colorMap.primary[400].hex]
},
fill: {
opacity: 0.2
},
markers: {
size: 5,
colors: [window.colorMap.success[400].hex, window.colorMap.primary[400].hex],
strokeColors: '#fff',
strokeWidth: 2,
hover: {
sizeOffset: 2
}
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '10px'
}
}
},
yaxis: {
max: 100,
tickAmount: 5,
show: true,
labels: {
show: true,
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '10px'
}
}
},
legend: {
show: true,
position: 'top',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 18,
height: 6,
radius: 2
},
itemMargin: {
horizontal: 12,
vertical: 0
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function (val) {
return val;
}
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3
},
plotOptions: {
radar: {
size: undefined,
offsetX: 0,
offsetY: 0,
padding: 0,
polygons: {
strokeColors: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
connectorColors: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
fill: {
colors: undefined
}
}
}
}
};
const userActivityChart = new ApexCharts(
document.getElementById('user-activity-chart'),
userActivityChartOptions
);
userActivityChart.render();
}
/***************************************************************/
/* Data Stream Chart #data-stream-chart */
/***************************************************************/
if (document.getElementById('data-stream-chart')) {
const categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
const positiveData = [0, 25, 70, 85, 25, 0, 60];
const negativeData = [-45, -50, -30, -25, -60, -120, 0];
const dataStreamChartOptions = {
series: [
{
name: 'Positive',
data: positiveData
},
{
name: 'Negative',
data: negativeData
}
],
chart: {
type: 'bar',
height: 350,
maxHeight: '100%',
stacked: true,
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.success[400].hex, // Positive (teal)
window.colorMap.primary[500].hex // Negative (blue)
],
plotOptions: {
bar: {
columnWidth: '50%',
borderRadius: 1
}
},
dataLabels: {
enabled: false
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
min: -150,
max: 150,
tickAmount: 6,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
},
formatter: function (val) {
return val;
}
}
},
legend: {
show: true,
position: 'top',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 18,
height: 6,
radius: 2
},
itemMargin: {
horizontal: 12,
vertical: 0
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3,
yaxis: {
lines: {
show: true
}
},
xaxis: {
lines: {
show: true
}
}
},
tooltip: {
shared: true,
intersect: false,
theme: 'dark',
y: {
formatter: function (val) {
return val;
}
}
}
};
const dataStreamChart = new ApexCharts(
document.getElementById('data-stream-chart'),
dataStreamChartOptions
);
dataStreamChart.render();
}
/***************************************************************/
/* Demographic Marketing Chart #demographic-marketing-chart */
/***************************************************************/
if (document.getElementById('demographic-marketing-chart')) {
const countries = ['USA', 'Germany', 'Australia', 'Canada', 'France'];
const data = [25, 30, 15, 10, 20]; // Percentages
const demographicMarketingChartOptions = {
series: data,
chart: {
type: 'pie',
height: 350,
maxHeight: '100%',
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.primary[100].hex, // USA (light purple)
window.colorMap.primary[400].hex, // Germany (darker purple)
window.colorMap.success[100].hex, // Australia (light blue)
window.colorMap.success[300].hex, // Canada (light teal)
window.colorMap.success[500].hex // France (teal)
],
labels: countries,
dataLabels: {
enabled: false
},
plotOptions: {
pie: {
donut: {
size: '0%'
}
}
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 12,
height: 12,
radius: 2
},
itemMargin: {
horizontal: 10,
vertical: 2
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function (val) {
return val + '%';
}
},
style: {
fontSize: '14px',
fontFamily: 'inherit'
},
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const country = w.globals.labels[seriesIndex];
const value = series[seriesIndex];
return '<div class="apexcharts-tooltip-box" style="padding: 2px 7px; background-color: var(--bs-body-bg);">' +
'<span style="color: var(--bs-body-color); font-size: 0.825rem !important;">' + country + ': ' + value + '%</span>' +
'</div>';
}
},
stroke: {
width: 1,
colors: ['var(--bs-body-bg)']
}
};
const demographicMarketingChart = new ApexCharts(
document.getElementById('demographic-marketing-chart'),
demographicMarketingChartOptions
);
demographicMarketingChart.render();
}
/***************************************************************/
/* Campaign Modal Functionality */
/***************************************************************/
const discountSlider = document.getElementById('discountAmount');
const discountValue = document.getElementById('discountAmountValue');
if (discountSlider && discountValue) {
discountSlider.addEventListener('input', function () {
discountValue.textContent = this.value;
});
}
// Toggle discount amount field visibility based on offer type
const offerTypeRadios = document.querySelectorAll('input[name="offerType"]');
const discountAmountContainer = document.getElementById('discountAmount')?.closest('.mb-3');
if (offerTypeRadios.length && discountAmountContainer) {
offerTypeRadios.forEach(radio => {
radio.addEventListener('change', function () {
// Only show discount slider for discount type offers
discountAmountContainer.style.display = (this.value === 'discount') ? 'block' : 'none';
});
});
}
// Show/hide custom audience options when "Custom Segment" is selected
const targetAudienceSelect = document.getElementById('targetAudience');
if (targetAudienceSelect) {
targetAudienceSelect.addEventListener('change', function () {
if (this.value === 'custom') {
// Check if custom audience options already exist
if (!document.getElementById('customAudienceOptions')) {
const customOptions = document.createElement('div');
customOptions.id = 'customAudienceOptions';
customOptions.className = 'mt-3 p-3 border border-light rounded';
customOptions.innerHTML = `
<p class="fw-bold mb-2">Define Custom Audience</p>
<div class="mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="segmentRecent">
<label class="form-check-label" for="segmentRecent">
Recently active (last 30 days)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="segmentHighValue">
<label class="form-check-label" for="segmentHighValue">
High-value subscribers
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="segmentLocation">
<label class="form-check-label" for="segmentLocation">
Specific locations
</label>
</div>
</div>
`;
targetAudienceSelect.parentNode.appendChild(customOptions);
} else {
document.getElementById('customAudienceOptions').style.display = 'block';
}
} else if (document.getElementById('customAudienceOptions')) {
document.getElementById('customAudienceOptions').style.display = 'none';
}
});
}
// Update form fields based on campaign type
const campaignTypeSelect = document.getElementById('campaignType');
if (campaignTypeSelect) {
campaignTypeSelect.addEventListener('change', function () {
// Get offer type radio buttons
const offerRadios = document.querySelectorAll('input[name="offerType"]');
switch (this.value) {
case 'acquisition':
// For acquisition, preselect free trial
offerRadios.forEach(radio => {
if (radio.value === 'freeTrial') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerFreeTrial').dispatchEvent(new Event('change'));
break;
case 'retention':
// For retention, preselect discount
offerRadios.forEach(radio => {
if (radio.value === 'discount') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerDiscount').dispatchEvent(new Event('change'));
// Set a higher default discount for retention
if (discountSlider) {
discountSlider.value = 25;
discountValue.textContent = '25';
}
break;
case 'upgrade':
// For upgrade, preselect upgrade promotion
offerRadios.forEach(radio => {
if (radio.value === 'upgrade') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerUpgrade').dispatchEvent(new Event('change'));
break;
case 'winback':
// For winback, preselect bundle offer
offerRadios.forEach(radio => {
if (radio.value === 'bundle') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerBundle').dispatchEvent(new Event('change'));
break;
}
});
}
// Handle form submission - Save Draft
const saveDraftBtn = document.getElementById('saveDraftBtn');
if (saveDraftBtn) {
saveDraftBtn.addEventListener('click', function () {
const campaignName = document.getElementById('campaignName').value;
if (!campaignName) {
showAlert('danger', 'Please enter a campaign name');
return;
}
showAlert('success', 'Campaign draft saved successfully');
});
}
// Handle form submission - Launch Campaign
const launchCampaignBtn = document.getElementById('launchCampaignBtn');
if (launchCampaignBtn) {
launchCampaignBtn.addEventListener('click', function () {
const campaignName = document.getElementById('campaignName').value;
const campaignType = document.getElementById('campaignType').value;
const startDate = document.getElementById('startDate').value;
const targetAudience = document.getElementById('targetAudience').value;
// Basic validation
if (!campaignName || !campaignType || !startDate || !targetAudience) {
showAlert('danger', 'Please fill in all required fields');
return;
}
// Here you would normally send data to server
// For demo, we'll just show success and close modal
showAlert('success', 'Campaign launched successfully!');
// Close the modal after brief delay
setTimeout(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('buildCampaignModal'));
if (modal) modal.hide();
}, 1500);
});
}
// Update discount value display
document.getElementById('discountAmount').addEventListener('input', function () {
document.getElementById('discountAmountValue').textContent = this.value + '%';
});
// Toggle discount section based on offer type selection
document.querySelectorAll('input[name="offerType"]').forEach(radio => {
radio.addEventListener('change', function () {
document.getElementById('discountSection').style.display =
this.value === 'discount' ? 'block' : 'none';
});
});
// Show alert function
function showAlert(type, message) {
// Check if alert container exists, if not create it
let alertContainer = document.querySelector('.campaign-alert-container');
if (!alertContainer) {
alertContainer = document.createElement('div');
alertContainer.className = 'campaign-alert-container position-fixed top-0 end-0 p-3';
alertContainer.style.zIndex = '5000';
document.body.appendChild(alertContainer);
}
// Create alert element
const alertElement = document.createElement('div');
alertElement.className = `alert alert-${type} alert-dismissible fade show`;
alertElement.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
// Add alert to container
alertContainer.appendChild(alertElement);
// Auto dismiss after 5 seconds
setTimeout(() => {
alertElement.classList.remove('show');
setTimeout(() => {
alertElement.remove();
}, 150);
}, 5000);
}
});