Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <base href="/" /> | |
| <link rel="icon" type="image/png" href="favicon.png" /> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script> | |
| <script src="firebase-app-compat.js"></script> | |
| <script src="firebase-messaging-compat.js"></script> | |
| <script src="firebase-config.js"></script> | |
| <script src="firebase-messaging.js"></script> | |
| <script> | |
| (() => { | |
| const storageKey = 'tts-theme'; | |
| let theme = 'light'; | |
| try { | |
| const storedTheme = localStorage.getItem(storageKey); | |
| theme = storedTheme === 'dark' || storedTheme === 'light' ? storedTheme : 'light'; | |
| localStorage.setItem(storageKey, theme); | |
| } catch { | |
| } | |
| document.documentElement.classList.toggle('dark', theme === 'dark'); | |
| document.documentElement.dataset.theme = theme; | |
| })(); | |
| </script> | |
| <script> | |
| (() => { | |
| const storageKey = 'tts-language'; | |
| let language = 'en'; | |
| try { | |
| language = localStorage.getItem(storageKey) === 'my' ? 'my' : 'en'; | |
| } catch { | |
| } | |
| document.documentElement.lang = language; | |
| })(); | |
| </script> | |
| <script> | |
| window.languagePreference = { | |
| normalize(code) { | |
| return code === 'my' ? 'my' : 'en'; | |
| }, | |
| apply(storageKey, code) { | |
| const normalized = this.normalize(code); | |
| document.documentElement.lang = normalized; | |
| try { | |
| localStorage.setItem(storageKey, normalized); | |
| } catch { | |
| } | |
| return normalized; | |
| }, | |
| init(storageKey) { | |
| let code = 'en'; | |
| try { | |
| code = this.normalize(localStorage.getItem(storageKey)); | |
| } catch { | |
| } | |
| return this.apply(storageKey, code); | |
| }, | |
| toggle(storageKey) { | |
| const next = document.documentElement.lang === 'my' ? 'en' : 'my'; | |
| return this.apply(storageKey, next); | |
| }, | |
| set(storageKey, code) { | |
| return this.apply(storageKey, code); | |
| } | |
| }; | |
| </script> | |
| <script> | |
| const brandDark = "#12312D"; | |
| const brandDarkAlt = "#115E59"; | |
| const brandPrimary = "#0F766E"; | |
| const brandPrimaryHover = "#115E59"; | |
| const brandAccent = "#B45309"; | |
| const brandLight = "#E6F4F1"; | |
| const brandMuted = "#64706D"; | |
| const semanticRedBg = "#FCE4E1"; | |
| const semanticRedText = "#B42318"; | |
| const semanticGreenBg = "#DFF6EE"; | |
| const semanticGreenText = "#0F766E"; | |
| const semanticNeutralBg = "#F1F5F4"; | |
| const semanticNeutralText = "#4F5D59"; | |
| const neutralBg = "#F7F8F7"; | |
| const neutralBorder = "#DDE5E2"; | |
| const neutralMuted = "#64706D"; | |
| const neutralDark = "#17211F"; | |
| const customGray = { | |
| 50: neutralBg, | |
| 100: "#EFF3F1", | |
| 200: neutralBorder, | |
| 300: "#B9C8C3", | |
| 400: "#8B9894", | |
| 500: neutralMuted, | |
| 600: "#4F5D59", | |
| 700: "#34413D", | |
| 800: "#222C29", | |
| 900: neutralDark, | |
| 950: "#101614" | |
| }; | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| colors: { | |
| transparent: 'transparent', | |
| current: 'currentColor', | |
| white: '#ffffff', | |
| black: '#000000', | |
| slate: customGray, | |
| gray: customGray, | |
| zinc: customGray, | |
| neutral: customGray, | |
| stone: customGray, | |
| red: { | |
| 50: semanticRedBg, | |
| 100: semanticRedBg, | |
| 200: "#FECDD3", | |
| 300: "#FDA4AF", | |
| 400: "#FB7185", | |
| 500: semanticRedText, | |
| 600: semanticRedText, | |
| 700: semanticRedText, | |
| 800: semanticRedText, | |
| 900: semanticRedText | |
| }, | |
| rose: { | |
| 50: semanticRedBg, | |
| 100: semanticRedBg, | |
| 200: "#FECDD3", | |
| 300: "#FDA4AF", | |
| 400: "#FB7185", | |
| 500: semanticRedText, | |
| 600: semanticRedText, | |
| 700: semanticRedText, | |
| 800: semanticRedText, | |
| 900: semanticRedText | |
| }, | |
| green: { | |
| 50: semanticGreenBg, | |
| 100: semanticGreenBg, | |
| 200: "#A7F3D0", | |
| 300: "#6EE7B7", | |
| 400: "#34D399", | |
| 500: semanticGreenText, | |
| 600: semanticGreenText, | |
| 700: semanticGreenText, | |
| 800: semanticGreenText, | |
| 900: semanticGreenText | |
| }, | |
| emerald: { | |
| 50: semanticGreenBg, | |
| 100: semanticGreenBg, | |
| 200: "#A7F3D0", | |
| 300: "#6EE7B7", | |
| 400: "#34D399", | |
| 500: semanticGreenText, | |
| 600: semanticGreenText, | |
| 700: semanticGreenText, | |
| 800: semanticGreenText, | |
| 900: semanticGreenText | |
| }, | |
| teal: { | |
| 50: "#F0FDFA", | |
| 100: "#CCFBF1", | |
| 200: "#99F6E4", | |
| 300: "#5EEAD4", | |
| 400: "#2DD4BF", | |
| 500: "#14B8A6", | |
| 600: "#0D9488", | |
| 700: "#0F766E", | |
| 800: "#115E59", | |
| 900: "#134E4A" | |
| }, | |
| cyan: { | |
| 50: "#ECFEFF", | |
| 100: "#CFFAFE", | |
| 200: "#A5F3FC", | |
| 300: "#67E8F9", | |
| 400: "#22D3EE", | |
| 500: "#06B6D4", | |
| 600: "#0891B2", | |
| 700: "#0E7490", | |
| 800: "#155E75", | |
| 900: "#164E63" | |
| }, | |
| sky: { | |
| 50: "#F0F9FF", | |
| 100: "#E0F2FE", | |
| 200: "#BAE6FD", | |
| 300: "#7DD3FC", | |
| 400: "#38BDF8", | |
| 500: "#0EA5E9", | |
| 600: "#0284C7", | |
| 700: "#0369A1", | |
| 800: "#075985", | |
| 900: "#0C4A6E" | |
| }, | |
| blue: { | |
| 50: brandLight, | |
| 100: "#CFE8E3", | |
| 200: "#A7D8CF", | |
| 300: "#76C3B8", | |
| 400: "#3AA89A", | |
| 500: "#16877E", | |
| 600: brandPrimary, | |
| 700: brandPrimaryHover, | |
| 800: brandDarkAlt, | |
| 900: brandDark | |
| }, | |
| amber: { | |
| 50: "#FEF3C7", | |
| 100: "#FDE68A", | |
| 200: "#FCD34D", | |
| 300: "#FBBF24", | |
| 400: "#F59E0B", | |
| 500: "#B45309", | |
| 600: "#92400E", | |
| 700: "#78350F", | |
| 800: "#78350F", | |
| 900: "#78350F" | |
| }, | |
| yellow: { | |
| 50: "#FEFCE8", | |
| 100: "#FEF3C7", | |
| 200: "#FDE68A", | |
| 300: "#FCD34D", | |
| 400: "#F59E0B", | |
| 500: "#B45309", | |
| 600: "#92400E", | |
| 700: "#78350F", | |
| 800: "#78350F", | |
| 900: "#78350F" | |
| }, | |
| violet: { | |
| 50: brandLight, | |
| 100: "#CFE8E3", | |
| 200: "#A7D8CF", | |
| 300: "#76C3B8", | |
| 400: "#3AA89A", | |
| 500: "#16877E", | |
| 600: brandPrimary, | |
| 700: brandPrimaryHover, | |
| 800: brandDark, | |
| 900: brandDark, | |
| 950: brandDark | |
| }, | |
| purple: { | |
| 50: brandLight, | |
| 100: "#CFE8E3", | |
| 200: "#A7D8CF", | |
| 300: "#76C3B8", | |
| 400: "#3AA89A", | |
| 500: "#16877E", | |
| 600: brandPrimary, | |
| 700: brandPrimaryHover, | |
| 800: brandDark, | |
| 900: brandDark, | |
| 950: brandDark | |
| }, | |
| indigo: { | |
| 50: brandLight, | |
| 100: "#CFE8E3", | |
| 200: "#A7D8CF", | |
| 300: "#76C3B8", | |
| 400: "#3AA89A", | |
| 500: "#16877E", | |
| 600: brandPrimary, | |
| 700: brandPrimaryHover, | |
| 800: brandDark, | |
| 900: brandDark, | |
| 950: brandDark | |
| }, | |
| chart: { | |
| purple: brandPrimary, | |
| teal: "#14B8A6", | |
| green: "#0F766E", | |
| neutral: neutralBorder | |
| }, | |
| background: neutralBg, | |
| foreground: neutralDark, | |
| card: "#FFFFFF", | |
| "card-foreground": neutralDark, | |
| popover: "#FFFFFF", | |
| "popover-foreground": neutralDark, | |
| primary: brandPrimary, | |
| "primary-foreground": "#FFFFFF", | |
| secondary: brandLight, | |
| "secondary-foreground": neutralDark, | |
| muted: "#F3F4F6", | |
| "muted-foreground": neutralMuted, | |
| accent: brandAccent, | |
| "accent-foreground": "#FFFFFF", | |
| border: neutralBorder, | |
| }, | |
| borderRadius: { | |
| none: "0", | |
| sm: "0.375rem", | |
| DEFAULT: "0.5rem", | |
| md: "0.5rem", | |
| lg: "0.5rem", | |
| xl: "0.5rem", | |
| "2xl": "0.5rem", | |
| "3xl": "0.5rem", | |
| full: "9999px" | |
| } | |
| } | |
| } | |
| </script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="TaskTrackingSystem.WebApp.styles.css" /> | |
| <link rel="stylesheet" href="app.css" /> | |
| <script src="theme-toggle.js?v=20260720-3" defer></script> | |
| <script src="https://code.highcharts.com/highcharts.js"></script> | |
| <script src="https://code.highcharts.com/gantt/modules/gantt.js"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <HeadOutlet /> | |
| </head> | |
| <body> | |
| <Routes /> | |
| <script src="_framework/blazor.web.js"></script> | |
| <script> | |
| window.renderPieChart = function (containerId, data) { | |
| const container = document.getElementById(containerId); | |
| if (!container) return; | |
| Highcharts.chart(containerId, { | |
| chart: { | |
| type: 'pie', | |
| backgroundColor: 'transparent', | |
| height: 260, | |
| margin: [0, 0, 0, 0], | |
| spacing: [0, 0, 0, 0] | |
| }, | |
| title: { | |
| text: null | |
| }, | |
| tooltip: { | |
| backgroundColor: 'rgba(255, 255, 255, 0.97)', | |
| borderWidth: 1, | |
| borderColor: '#dde5e2', | |
| borderRadius: 8, | |
| shadow: true, | |
| style: { | |
| fontFamily: 'Inter, sans-serif', | |
| fontSize: '13px', | |
| color: '#17211f' | |
| }, | |
| pointFormat: '<b>{point.name}</b>: <b>{point.y}</b> tasks ({point.percentage:.1f}%)' | |
| }, | |
| plotOptions: { | |
| pie: { | |
| borderWidth: 0, | |
| borderColor: 'transparent', | |
| allowPointSelect: true, | |
| cursor: 'pointer', | |
| dataLabels: { | |
| enabled: true, | |
| distance: -30, | |
| format: '{point.percentage:.0f}%', | |
| style: { | |
| color: 'white', | |
| textOutline: 'none', | |
| fontFamily: 'Inter, sans-serif', | |
| fontSize: '12px', | |
| fontWeight: 'bold' | |
| }, | |
| filter: { | |
| property: 'percentage', | |
| operator: '>', | |
| value: 5 | |
| } | |
| }, | |
| showInLegend: false | |
| } | |
| }, | |
| series: [{ | |
| name: 'Tasks', | |
| colorByPoint: true, | |
| data: data | |
| }], | |
| credits: { | |
| enabled: false | |
| } | |
| }); | |
| }; | |
| window.renderGanttChart = function (containerId, data, options) { | |
| options = options || {}; | |
| const container = document.getElementById(containerId); | |
| if (!container) return; | |
| const minWidth = options.minWidth || 1200; | |
| const chartHeight = Math.max(options.height || 420, 120 + (data.length * 44)); | |
| Highcharts.ganttChart(containerId, { | |
| chart: { | |
| backgroundColor: 'transparent', | |
| height: chartHeight, | |
| style: { | |
| fontFamily: 'Inter, sans-serif' | |
| }, | |
| scrollablePlotArea: { | |
| minWidth: minWidth, | |
| scrollPositionX: 0 | |
| } | |
| }, | |
| title: { | |
| text: null | |
| }, | |
| credits: { | |
| enabled: false | |
| }, | |
| navigator: { | |
| enabled: false | |
| }, | |
| scrollbar: { | |
| enabled: true | |
| }, | |
| rangeSelector: { | |
| enabled: false | |
| }, | |
| legend: { | |
| enabled: false | |
| }, | |
| tooltip: { | |
| backgroundColor: 'rgba(255, 255, 255, 0.97)', | |
| borderWidth: 1, | |
| borderColor: '#dde5e2', | |
| borderRadius: 8, | |
| shadow: true, | |
| style: { | |
| fontSize: '13px', | |
| color: '#17211f' | |
| }, | |
| pointFormatter: function() { | |
| const start = Highcharts.dateFormat('%b %e, %Y', this.start); | |
| const end = Highcharts.dateFormat('%b %e, %Y', this.end); | |
| const progress = this.completed && this.completed.amount !== undefined | |
| ? Math.round(this.completed.amount * 100) + '%' | |
| : '0%'; | |
| return `<b>${this.name}</b><br/>Start: ${start}<br/>End: ${end}<br/>Progress: ${progress}`; | |
| } | |
| }, | |
| xAxis: [{ | |
| type: 'datetime', | |
| min: options.xAxisMin, | |
| max: options.xAxisMax, | |
| gridLineWidth: 1, | |
| gridLineColor: '#eff3f1', | |
| lineColor: '#dde5e2', | |
| tickColor: '#dde5e2', | |
| labels: { | |
| format: '{value:%b %Y}', | |
| style: { | |
| color: '#64706d', | |
| fontSize: '11px' | |
| } | |
| }, | |
| currentDateIndicator: { | |
| color: '#0f766e', | |
| dashStyle: 'ShortDash', | |
| width: 2, | |
| label: { | |
| format: 'Today', | |
| style: { | |
| color: '#0f766e', | |
| fontWeight: 'bold' | |
| } | |
| } | |
| } | |
| }], | |
| yAxis: { | |
| gridLineWidth: 1, | |
| gridLineColor: '#eff3f1', | |
| labels: { | |
| style: { | |
| color: '#34413d', | |
| fontSize: '12px', | |
| fontWeight: '600' | |
| } | |
| } | |
| }, | |
| plotOptions: { | |
| gantt: { | |
| borderWidth: 0, | |
| borderColor: 'transparent', | |
| dataLabels: { | |
| enabled: true, | |
| formatter: function() { | |
| if (this.point.completed && this.point.completed.amount !== undefined) { | |
| return Math.round(this.point.completed.amount * 100) + '%'; | |
| } | |
| return '0%'; | |
| }, | |
| style: { | |
| cursor: 'default', | |
| pointerEvents: 'none', | |
| textOutline: 'none', | |
| color: '#ffffff', | |
| fontSize: '11px', | |
| fontWeight: 'bold' | |
| } | |
| } | |
| } | |
| }, | |
| series: [{ | |
| name: 'Projects', | |
| borderWidth: 0, | |
| borderColor: 'transparent', | |
| data: data.map(item => ({ | |
| id: item.id, | |
| name: item.name, | |
| start: item.start, | |
| end: item.end, | |
| completed: { | |
| amount: item.completed, | |
| fill: item.color | |
| }, | |
| color: '#dde5e2' | |
| })) | |
| }] | |
| }); | |
| }; | |
| function initIcons() { | |
| if (window.lucide) { | |
| window.lucide.createIcons(); | |
| } | |
| if (typeof initFlatpickr === 'function') { | |
| initFlatpickr(); | |
| } | |
| } | |
| function downloadFile(filename, contentType, content) { | |
| const file = new Blob([content], {type: contentType}); | |
| const exportUrl = URL.createObjectURL(file); | |
| const a = document.createElement("a"); | |
| a.href = exportUrl; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(exportUrl); | |
| } | |
| function downloadFileFromBase64(filename, contentType, base64Content) { | |
| const byteCharacters = atob(base64Content); | |
| const byteNumbers = new Array(byteCharacters.length); | |
| for (let i = 0; i < byteCharacters.length; i++) { | |
| byteNumbers[i] = byteCharacters.charCodeAt(i); | |
| } | |
| const byteArray = new Uint8Array(byteNumbers); | |
| const file = new Blob([byteArray], {type: contentType}); | |
| const exportUrl = URL.createObjectURL(file); | |
| const a = document.createElement("a"); | |
| a.href = exportUrl; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(exportUrl); | |
| } | |
| function initFlatpickr() { | |
| if (window.flatpickr) { | |
| flatpickr("input[type='date']", { | |
| altInput: true, | |
| altFormat: "d-m-Y", | |
| dateFormat: "Y-m-d", | |
| allowInput: true | |
| }); | |
| } | |
| } | |
| function refreshClientEnhancements() { | |
| if (window.updateTaskifyThemeControls) { | |
| window.updateTaskifyThemeControls(); | |
| } | |
| initIcons(); | |
| initFlatpickr(); | |
| } | |
| // Run once loaded | |
| window.addEventListener('DOMContentLoaded', () => { | |
| refreshClientEnhancements(); | |
| }); | |
| // Blazor enhanced page updates trigger | |
| Blazor.addEventListener('enhancedload', () => { | |
| refreshClientEnhancements(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |