Files
taxbaik/legacy/smartadmin/scripts/pages/projectmanagementdashboard.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

1218 lines
47 KiB
JavaScript

// All imports for the Project Management Dashboard
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
/***************************************************************/
/* Development Phases Chart #development-phases-chart */
/***************************************************************/
if (document.getElementById('development-phases-chart')) {
// Generate complex development timeline data
const generateComplexTimelineData = () => {
// Project teams - reduced number of teams to make it less busy
const teams = [
{
name: 'Frontend Team',
color: window.colorMap.warning[400].hex,
members: [
{ id: 1, name: 'Alex Johnson', img: './img/demo/avatars/avatar-a.png' },
{ id: 2, name: 'Emma Wilson', img: './img/demo/avatars/avatar-b.png' },
{ id: 3, name: 'Michael Chen', img: './img/demo/avatars/avatar-c.png' }
]
},
{
name: 'Backend Team',
color: window.colorMap.success[400].hex,
members: [
{ id: 4, name: 'Sarah Williams', img: './img/demo/avatars/avatar-d.png' },
{ id: 5, name: 'James Lee', img: './img/demo/avatars/avatar-e.png' }
]
},
{
name: 'Database Team',
color: window.colorMap.info[400].hex,
members: [
{ id: 6, name: 'Olivia Smith', img: './img/demo/avatars/avatar-f.png' },
{ id: 7, name: 'David Rodriguez', img: './img/demo/avatars/avatar-g.png' }
]
},
{
name: 'QA Team',
color: window.colorMap.danger[100].hex,
members: [
{ id: 8, name: 'Sophia Martinez', img: './img/demo/avatars/avatar-h.png' },
{ id: 9, name: 'Oliver Wilson', img: './img/demo/avatars/avatar-i.png' }
]
},
{
name: 'DevOps Team',
color: window.colorMap.primary[200].hex,
members: [
{ id: 10, name: 'Jessica Brown', img: './img/demo/avatars/avatar-j.png' },
{ id: 11, name: 'Robert Taylor', img: './img/demo/avatars/avatar-k.png' }
]
}
];
// Project Phases (Broader categories) - simplified with fewer activities and expanded over 6 months
const phases = [
{ name: 'Research & Planning', startDate: '2025-01-15', activities: [
{ name: 'Market Research', duration: [10, 15] },
{ name: 'Requirement Analysis', duration: [12, 20] },
{ name: 'Feasibility Study', duration: [8, 14] }
]},
{ name: 'Design', startDate: '2025-02-10', activities: [
{ name: 'Information Architecture', duration: [15, 20] },
{ name: 'Wireframing', duration: [14, 21] },
{ name: 'UI Design', duration: [18, 30] }
]},
{ name: 'Development', startDate: '2025-03-05', activities: [
{ name: 'Core Architecture', duration: [20, 30] },
{ name: 'Frontend Framework', duration: [25, 40] },
{ name: 'Backend API Development', duration: [30, 45] },
{ name: 'Database Schema', duration: [15, 25] }
]},
{ name: 'Testing', startDate: '2025-04-20', activities: [
{ name: 'Unit Testing', duration: [20, 30] },
{ name: 'Integration Testing', duration: [15, 25] },
{ name: 'User Acceptance Testing', duration: [15, 25] }
]},
{ name: 'Deployment', startDate: '2025-05-25', activities: [
{ name: 'Staging Environment Setup', duration: [10, 15] },
{ name: 'Production Environment Prep', duration: [12, 18] },
{ name: 'Data Migration', duration: [10, 20] }
]},
{ name: 'Post-Launch', startDate: '2025-06-20', activities: [
{ name: 'Monitoring', duration: [14, 21] },
{ name: 'Bug Fixes', duration: [10, 30] },
{ name: 'Performance Optimization', duration: [15, 25] }
]}
];
// Complex data generation - ensure most tasks have at least 3 team members
const complexData = [];
let taskId = 1;
phases.forEach(phase => {
const phaseStartDate = new Date(phase.startDate);
let currentStartDate = new Date(phaseStartDate);
phase.activities.forEach(activity => {
// Assign different teams to different activities with some randomness
teams.forEach(team => {
// Increased probability to ~50% to reduce number of entries
if (Math.random() > 0.5) {
// Calculate a random start date within a reasonable range from the current date
const randomStartOffset = Math.floor(Math.random() * 5); // 0-4 days offset (slightly increased for more spread)
const startDate = new Date(currentStartDate);
startDate.setDate(startDate.getDate() + randomStartOffset);
// Calculate duration (days) within the specified range
const minDuration = activity.duration[0];
const maxDuration = activity.duration[1];
const actualDuration = Math.floor(Math.random() * (maxDuration - minDuration + 1)) + minDuration;
// Calculate end date
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + actualDuration);
// Assign team members to the task - ensure at least 3 members when possible
const assignedMembers = [];
// Determine number of members to assign (3-5 for most tasks)
const memberCount = Math.min(team.members.length, Math.floor(Math.random() * 3) + 3); // 3-5 members
const availableMembers = [...team.members];
// If team has fewer than 3 members, assign all of them
if (availableMembers.length <= 3) {
assignedMembers.push(...availableMembers);
} else {
// Otherwise, randomly select memberCount members
for (let i = 0; i < memberCount; i++) {
const randomIndex = Math.floor(Math.random() * availableMembers.length);
assignedMembers.push(availableMembers.splice(randomIndex, 1)[0]);
}
}
// Create the task
complexData.push({
id: taskId++,
name: team.name,
data: [{
x: `${phase.name}: ${activity.name}`,
y: [startDate.getTime(), endDate.getTime()],
fillColor: team.color,
team: team.name,
phase: phase.name,
activity: activity.name,
duration: actualDuration,
assignedMembers: assignedMembers
}]
});
}
});
// Move the current start date forward
currentStartDate.setDate(currentStartDate.getDate() + Math.floor(Math.random() * 7) + 3); // 3-9 days gap (increased for more spread)
});
});
return { complexData, phases, teams };
};
const { complexData, phases, teams } = generateComplexTimelineData();
// No longer need these functions for adding images to chart bars
const dataLabelsFormatter = function(val, opts) {
// Return empty string - we're not showing images in bars anymore
return '';
};
const developmentPhasesOptions = {
series: complexData,
chart: {
height: 600, // Increased height for better visualization
type: 'rangeBar',
fontFamily: 'inherit',
parentHeightOffset: 0,
animations: {
enabled: false // Disable animations for better performance with large dataset
},
zoom: {
enabled: true,
type: 'x',
autoScaleYaxis: false,
allowMouseWheelZoom: false,
zoomedArea: {
fill: {
color: '#90CAF9',
opacity: 0.4
},
stroke: {
color: '#0D47A1',
opacity: 0.4,
width: 1
}
}
},
toolbar: {
show: true,
tools: {
download: false,
selection: false,
zoom: false,
zoomin: true,
zoomout: true,
pan: true,
reset: true
},
autoSelected: 'pan'
}
},
plotOptions: {
bar: {
horizontal: true,
distributed: false,
dataLabels: {
hideOverflowingLabels: true
},
rangeBarGroupRows: true
}
},
dataLabels: {
enabled: false,
},
xaxis: {
type: 'datetime',
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '11px'
},
datetimeUTC: false,
format: 'MMM dd'
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '11px'
},
maxWidth: 300
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3,
xaxis: {
lines: {
show: true
}
},
yaxis: {
lines: {
show: false
}
}
},
tooltip: {
custom: function(opts) {
const data = opts.w.config.series[opts.seriesIndex].data[opts.dataPointIndex];
const from = new Date(opts.y1).toLocaleDateString();
const to = new Date(opts.y2).toLocaleDateString();
const duration = Math.floor((opts.y2 - opts.y1) / (1000 * 60 * 60 * 24));
// Create team member images HTML for tooltip
let assignedMembersHtml = '';
if (data.assignedMembers && data.assignedMembers.length > 0) {
// Get the first 4 members to display
const displayMembers = data.assignedMembers.slice(0, 4);
// Calculate remaining members count for +X label
const remainingCount = Math.max(0, data.assignedMembers.length - 4);
assignedMembersHtml = `
<div class="mt-2 fs-xs mb-1 fw-500">Assigned Members:</div>
<div class="fs-sm d-flex align-items-center mt-2">
${displayMembers.map(member => `
<span class="profile-image-md ms-1 rounded-circle d-inline-block"
style="background-image:url('${member.img}'); background-size: cover;"></span>
`).join('')}
${remainingCount > 0 ? `
<div data-hasmore="+${remainingCount}" class="rounded-circle profile-image-md ms-1">
<span class="profile-image-md ms-1 rounded-circle d-inline-block"
style="background-image:url('${data.assignedMembers[4].img}'); background-size: cover;"></span>
</div>
` : ''}
</div>
`;
}
return (
'<div class="arrow_box p-2">' +
'<div class="fw-bold mb-1">' + data.team + '</div>' +
'<div>' + data.activity + '</div>' +
'<div><small class="text-danger">Phase: ' + data.phase + '</small></div>' +
'<div class="mt-2"><small>Start: ' + from + '</small></div>' +
'<div><small>End: ' + to + '</small></div>' +
'<div><small>Duration: <span class="fw-bold">' + duration + ' days</span></small></div>' +
assignedMembersHtml +
'</div>'
);
}
},
legend: {
show: false,
},
// Advanced features
annotations: {
xaxis: [
{
x: new Date('2025-02-15').getTime(),
borderColor: window.colorMap.primary[500].hex,
strokeDashArray: 0,
label: {
borderColor: window.colorMap.primary[500].hex,
style: {
color: '#fff',
background: window.colorMap.primary[500].hex
},
text: 'Design Review'
}
},
{
x: new Date('2025-04-15').getTime(),
borderColor: window.colorMap.danger[500].hex,
strokeDashArray: 0,
label: {
borderColor: window.colorMap.danger[500].hex,
style: {
color: '#fff',
background: window.colorMap.danger[500].hex
},
text: 'Critical Milestone'
}
},
{
x: new Date('2025-06-15').getTime(),
borderColor: window.colorMap.success[500].hex,
strokeDashArray: 0,
label: {
borderColor: window.colorMap.success[500].hex,
style: {
color: '#fff',
background: window.colorMap.success[500].hex
},
text: 'Release Candidate'
}
}
]
}
};
const developmentPhasesChart = new ApexCharts(
document.getElementById('development-phases-chart'),
developmentPhasesOptions
);
developmentPhasesChart.render();
// Handler for team filter buttons
document.querySelectorAll('.development-phases-filters [data-filter]').forEach(button => {
button.addEventListener('click', function() {
// Update button states
document.querySelectorAll('.development-phases-filters [data-filter]').forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
const filter = this.getAttribute('data-filter');
let filteredData;
if (filter === 'all') {
filteredData = complexData;
} else {
filteredData = complexData.filter(series => series.name === filter);
}
// Update the chart
developmentPhasesChart.updateSeries(filteredData);
});
});
}
/***************************************************************/
/* Project Progress Chart #project-progress-chart */
/***************************************************************/
if (document.getElementById('project-progress-chart')) {
const categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
const plannedData = [42, 53, 66, 78, 86, 95, 100];
const actualData = [38, 45, 55, 72, 80, 84, 92];
const projectProgressChartOptions = {
series: [
{
name: 'Planned Progress',
type: 'line',
data: plannedData
},
{
name: 'Actual Progress',
type: 'area',
data: actualData
}
],
chart: {
height: 350,
type: 'line',
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.primary[500].hex, // Planned (blue line)
window.colorMap.success[400].hex // Actual (green area)
],
stroke: {
width: [3, 2],
curve: 'smooth',
dashArray: [0, 0]
},
fill: {
type: ['solid', 'gradient'],
opacity: [1, 0.8],
gradient: {
shade: 'dark',
type: 'vertical',
shadeIntensity: 0.5,
gradientToColors: [window.colorMap.success[400].hex], // End color (top)
opacityFrom: 0.8, // Higher opacity at bottom
opacityTo: 0.2, // Lower opacity at top
stops: [0, 90, 100],
colorStops: [
{
offset: 0,
color: window.colorMap.success[100].hex || '#333333', // Dark color at bottom
opacity: 0.8
},
{
offset: 90,
color: window.colorMap.success[400].hex, // Line color in middle
opacity: 0.2
}
]
}
},
markers: {
size: [4, 4],
colors: [window.colorMap.primary[500].hex, window.colorMap.success[400].hex],
strokeColors: ['#fff', '#fff'],
strokeWidth: 2,
hover: {
size: 7
}
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
min: 0,
max: 100,
title: {
text: 'Completion %',
style: {
fontWeight: 400
}
},
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
},
formatter: function (val) {
return val + '%';
}
}
},
legend: {
position: 'top',
horizontalAlign: 'right',
fontSize: '14px',
markers: {
width: 10,
height: 10,
radius: 0
},
itemMargin: {
horizontal: 15,
vertical: 5
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function (val) {
return val + '%';
}
}
}
};
const projectProgressChart = new ApexCharts(
document.getElementById('project-progress-chart'),
projectProgressChartOptions
);
projectProgressChart.render();
}
/***************************************************************/
/* Task Status Chart #task-status-chart */
/***************************************************************/
if (document.getElementById('task-status-chart')) {
const taskStatusChartOptions = {
series: [35, 20, 15, 30],
chart: {
type: 'donut',
height: 350,
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.success[400].hex, // Completed
window.colorMap.warning[400].hex, // In Progress
window.colorMap.danger[400].hex, // Overdue
window.colorMap.primary[400].hex // Not Started
],
labels: ['Completed', 'In Progress', 'Overdue', 'Not Started'],
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: {
show: true,
fontSize: '14px',
fontWeight: 600,
offsetY: -10
},
value: {
show: true,
fontSize: '24px',
fontWeight: 400,
formatter: function (val) {
return val + '%';
}
},
total: {
show: true,
label: 'Total Tasks',
fontSize: '16px',
fontWeight: 600,
formatter: function (w) {
return w.globals.seriesTotals.reduce((a, b) => a + b, 0) + ' Tasks';
}
}
}
}
}
},
legend: {
position: 'bottom',
fontSize: '14px',
markers: {
width: 12,
height: 12,
radius: 0
},
itemMargin: {
horizontal: 10,
vertical: 5
}
},
tooltip: {
enabled: true,
y: {
formatter: function (val) {
return val + '%';
}
}
}
};
const taskStatusChart = new ApexCharts(
document.getElementById('task-status-chart'),
taskStatusChartOptions
);
taskStatusChart.render();
}
/***************************************************************/
/* Team Performance Chart #team-performance-chart */
/***************************************************************/
if (document.getElementById('team-performance-chart')) {
const categories = ['Work Quality', 'Productivity', 'Communication', 'Problem Solving', 'Task Completion', 'Innovation', 'Collaboration'];
const teamPerformanceChartOptions = {
series: [
{
name: 'Team Performance',
data: [85, 75, 90, 70, 80, 65, 95]
}
],
chart: {
height: 350,
type: 'radar',
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [window.colorMap.primary[400].hex],
fill: {
opacity: 0.4
},
markers: {
size: 5,
colors: [window.colorMap.primary[400].hex],
strokeColors: "#fff",
strokeWidth: 2
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
}
},
yaxis: {
max: 100,
min: 0,
tickAmount: 5,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '10px'
}
}
},
plotOptions: {
radar: {
polygons: {
strokeColors: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
connectorColors: window.colorMap.bootstrapVars.bodyColor.rgba(0.1)
}
}
}
};
const teamPerformanceChart = new ApexCharts(
document.getElementById('team-performance-chart'),
teamPerformanceChartOptions
);
teamPerformanceChart.render();
}
/***************************************************************/
/* Weekly Activity Chart #weekly-activity-chart */
/***************************************************************/
if (document.getElementById('weekly-activity-chart')) {
const categories = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const completedData = [18, 25, 20, 30, 22, 5, 2];
const newData = [12, 15, 30, 20, 25, 8, 0];
const weeklyActivityChartOptions = {
series: [
{
name: 'Completed Tasks',
data: completedData
},
{
name: 'New Tasks',
data: newData
}
],
chart: {
type: 'bar',
height: 350,
stacked: false,
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.success[400].hex, // Completed
window.colorMap.primary[400].hex // New
],
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
borderRadius: 3,
dataLabels: {
total: {
enabled: false
}
}
}
},
dataLabels: {
enabled: false
},
stroke: {
width: 0,
colors: ['transparent']
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
title: {
text: 'Number of Tasks',
style: {
fontWeight: 400
}
},
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
}
},
legend: {
position: 'top',
horizontalAlign: 'right',
fontSize: '14px',
markers: {
width: 12,
height: 12,
radius: 2
},
itemMargin: {
horizontal: 10,
vertical: 5
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function (val) {
return val + ' Tasks';
}
}
}
};
const weeklyActivityChart = new ApexCharts(
document.getElementById('weekly-activity-chart'),
weeklyActivityChartOptions
);
weeklyActivityChart.render();
}
/***************************************************************/
/* Team Workload Heatmap #team-workload-heatmap */
/***************************************************************/
if (document.getElementById('team-workload-heatmap')) {
// Team members
const teamMembers = [
'Alex Johnson',
'Sarah Williams',
'Michael Chen',
'Olivia Smith',
'David Rodriguez',
'Emma Wilson',
'James Lee',
'Sophia Martinez'
];
// Days of the week
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
// Generate workload data (hours spent on tasks per day)
// Each cell represents hours spent on tasks by a team member on a specific day
function generateData(count, min, max) {
const data = [];
for (let i = 0; i < count; i++) {
const teamMemberData = [];
for (let j = 0; j < days.length; j++) {
const value = Math.floor(Math.random() * (max - min + 1)) + min;
teamMemberData.push({
x: days[j],
y: value
});
}
data.push({
name: teamMembers[i],
data: teamMemberData
});
}
return data;
}
const workloadData = generateData(teamMembers.length, 0, 10);
const teamWorkloadHeatmapOptions = {
series: workloadData,
chart: {
height: 370,
type: 'heatmap',
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
dataLabels: {
enabled: false
},
colors: [window.colorMap.primary[500].hex],
tooltip: {
enabled: true,
y: {
formatter: function(val) {
return val + " hours";
}
}
},
plotOptions: {
heatmap: {
shadeIntensity: 0.5,
radius: 0,
useFillColorAsStroke: false,
colorScale: {
ranges: [
{
from: 0,
to: 2,
name: 'Low',
color: window.colorMap.success[300].hex
},
{
from: 3,
to: 5,
name: 'Medium',
color: window.colorMap.primary[300].hex
},
{
from: 6,
to: 8,
name: 'High',
color: window.colorMap.warning[400].hex
},
{
from: 9,
to: 10,
name: 'Critical',
color: window.colorMap.danger[400].hex
}
]
}
}
},
xaxis: {
categories: days,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
}
},
legend: {
position: 'top',
horizontalAlign: 'center',
fontSize: '14px',
markers: {
radius: 0
},
itemMargin: {
horizontal: 15,
vertical: 5
}
},
stroke: {
width: 1,
colors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.05)]
}
};
const teamWorkloadHeatmap = new ApexCharts(
document.getElementById('team-workload-heatmap'),
teamWorkloadHeatmapOptions
);
teamWorkloadHeatmap.render();
// Event listeners for the filter buttons
document.querySelectorAll('.team-workload-filters [data-filter]').forEach(button => {
button.addEventListener('click', function() {
const filter = this.getAttribute('data-filter');
// Remove active class from all buttons
document.querySelectorAll('.team-workload-filters [data-filter]').forEach(btn => {
btn.classList.remove('active');
});
// Add active class to clicked button
this.classList.add('active');
// Generate new data based on filter
let newData;
if (filter === 'week') {
newData = generateData(teamMembers.length, 0, 10);
} else if (filter === 'month') {
newData = generateData(teamMembers.length, 10, 40); // Higher values for monthly view
}
// Update chart
teamWorkloadHeatmap.updateSeries(newData);
});
});
}
/***************************************************************/
/* Project Timeline Chart #project-timeline-chart */
/***************************************************************/
if (document.getElementById('project-timeline-chart')) {
const projectTimelineOptions = {
series: [
{
name: 'Frontend',
data: [
{
x: 'Design',
y: [
new Date('2023-03-05').getTime(),
new Date('2023-03-15').getTime()
]
},
{
x: 'Testing',
y: [
new Date('2023-03-25').getTime(),
new Date('2023-04-05').getTime()
]
},
{
x: 'Integration Testing',
y: [
new Date('2023-04-05').getTime(),
new Date('2023-04-20').getTime()
]
}
]
},
{
name: 'Backend',
data: [
{
x: 'Development',
y: [
new Date('2023-03-10').getTime(),
new Date('2023-03-30').getTime()
]
},
{
x: 'API Design',
y: [
new Date('2023-03-08').getTime(),
new Date('2023-03-20').getTime()
]
},
{
x: 'Implementation',
y: [
new Date('2023-03-20').getTime(),
new Date('2023-04-05').getTime()
]
}
]
},
{
name: 'Database',
data: [
{
x: 'Schema Design',
y: [
new Date('2023-03-10').getTime(),
new Date('2023-03-25').getTime()
]
},
{
x: 'Data Migration',
y: [
new Date('2023-03-25').getTime(),
new Date('2023-04-10').getTime()
]
},
{
x: 'Performance Testing',
y: [
new Date('2023-04-10').getTime(),
new Date('2023-04-25').getTime()
]
}
]
}
],
chart: {
height: 370,
type: 'rangeBar',
fontFamily: 'inherit',
parentHeightOffset: 0,
toolbar: {
show: false
},
zoom: {
enabled: false,
}
},
plotOptions: {
bar: {
horizontal: true,
barHeight: '50%',
rangeBarGroupRows: true
}
},
colors: [
window.colorMap.primary[100].hex, // Frontend
window.colorMap.primary[300].hex, // Backend
window.colorMap.primary[600].hex // Database
],
fill: {
type: 'solid'
},
xaxis: {
type: 'datetime',
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3,
xaxis: {
lines: {
show: true
}
},
yaxis: {
lines: {
show: false
}
}
},
tooltip: {
custom: function(opts) {
const from = new Date(opts.y1).toLocaleDateString();
const to = new Date(opts.y2).toLocaleDateString();
const duration = Math.floor((opts.y2 - opts.y1) / (1000 * 60 * 60 * 24));
const seriesName = opts.w.globals.seriesNames[opts.seriesIndex];
const taskName = opts.w.globals.labels[opts.dataPointIndex];
return (
'<div class="arrow_box p-2">' +
'<div class="font-weight-bold mb-1">' + seriesName + ': ' + taskName + '</div>' +
'<div>Start: ' + from + '</div>' +
'<div>End: ' + to + '</div>' +
'<div>Duration: ' + duration + ' days</div>' +
'</div>'
);
}
},
legend: {
position: 'top',
horizontalAlign: 'left',
fontSize: '14px',
markers: {
width: 12,
height: 12,
radius: 0
},
itemMargin: {
horizontal: 10,
vertical: 5
}
}
};
const projectTimelineChart = new ApexCharts(
document.getElementById('project-timeline-chart'),
projectTimelineOptions
);
projectTimelineChart.render();
// Handle view toggle buttons
const timelineViewButtons = document.querySelectorAll('.project-timeline-filters [data-timeline-view]');
timelineViewButtons.forEach(button => {
button.addEventListener('click', function() {
timelineViewButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
const view = this.getAttribute('data-timeline-view');
if (view === 'phases') {
// Show simplified view with only project phases
projectTimelineChart.updateSeries([
{
name: 'Project Phases',
data: [
{
x: 'Research',
y: [
new Date('2023-03-01').getTime(),
new Date('2023-03-05').getTime()
],
fillColor: '#4FC3F7'
},
{
x: 'Design',
y: [
new Date('2023-03-06').getTime(),
new Date('2023-03-10').getTime()
],
fillColor: '#4DB6AC'
},
{
x: 'Development',
y: [
new Date('2023-03-11').getTime(),
new Date('2023-03-25').getTime()
],
fillColor: '#FFB74D'
},
{
x: 'Testing',
y: [
new Date('2023-03-26').getTime(),
new Date('2023-04-10').getTime()
],
fillColor: '#F06292'
},
{
x: 'Deployment',
y: [
new Date('2023-04-11').getTime(),
new Date('2023-04-25').getTime()
],
fillColor: '#9575CD'
}
]
}
]);
} else {
// Restore detailed view
projectTimelineChart.updateSeries(projectTimelineOptions.series);
}
});
});
}
});