Skip to content

Commit ab044b2

Browse files
committed
feat(weekly-booking): show holiday columns and forecast
1 parent 34fdb8f commit ab044b2

2 files changed

Lines changed: 122 additions & 5 deletions

File tree

time_tracking/fixtures/custom_html_block.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"name": "Time Tracking Summary",
55
"module": "Time Tracking",
66
"private": 0,
7-
"html": "<div class=\"tt-summary\">\n <div class=\"tt-summary-grid\">\n <div class=\"card tt-summary-card\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"week-total\">Week Total</div>\n <div class=\"tt-summary-value\" data-field=\"week-total\">0:00</div>\n <div class=\"tt-summary-status\" data-field=\"week-status\"></div>\n </div>\n </div>\n <div class=\"card tt-summary-card\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"month-total\">Month Total</div>\n <div class=\"tt-summary-value\" data-field=\"month-total\">0:00</div>\n <div class=\"tt-summary-status\" data-field=\"month-status\"></div>\n </div>\n </div>\n <div class=\"card tt-summary-card\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"hours-balance\">Hours Balance</div>\n <div class=\"tt-summary-value\" data-field=\"hours-balance\">0:00</div>\n </div>\n </div>\n <div class=\"card tt-summary-card\" data-card=\"vacation\" style=\"display: none;\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"vacation-remaining\">Vacation Remaining</div>\n <div class=\"tt-summary-value\" data-field=\"vacation-remaining\">0</div>\n <div class=\"tt-summary-status\" data-field=\"vacation-status\"></div>\n </div>\n </div>\n </div>\n</div>",
8-
"script": "const weekTitle = root_element.querySelector('[data-label=\"week-total\"]');\nconst monthTitle = root_element.querySelector('[data-label=\"month-total\"]');\nconst hoursBalanceTitle = root_element.querySelector('[data-label=\"hours-balance\"]');\nconst vacationTitle = root_element.querySelector('[data-label=\"vacation-remaining\"]');\n\nconst weekTotal = root_element.querySelector('[data-field=\"week-total\"]');\nconst weekStatus = root_element.querySelector('[data-field=\"week-status\"]');\nconst monthTotal = root_element.querySelector('[data-field=\"month-total\"]');\nconst monthStatus = root_element.querySelector('[data-field=\"month-status\"]');\nconst hoursBalance = root_element.querySelector('[data-field=\"hours-balance\"]');\nconst vacationCard = root_element.querySelector('[data-card=\"vacation\"]');\nconst vacationRemaining = root_element.querySelector('[data-field=\"vacation-remaining\"]');\nconst vacationStatus = root_element.querySelector('[data-field=\"vacation-status\"]');\n\nif (weekTitle) {\n weekTitle.textContent = __('Week Total');\n}\nif (monthTitle) {\n monthTitle.textContent = __('Month Total');\n}\nif (hoursBalanceTitle) {\n hoursBalanceTitle.textContent = __('Hours Balance');\n}\nif (vacationTitle) {\n vacationTitle.textContent = __('Vacation Remaining');\n}\n\nfunction formatMinutes(totalMinutes) {\n const minutes = Math.max(0, Math.round(totalMinutes || 0));\n const hours = Math.floor(minutes / 60);\n const remainder = minutes % 60;\n return `${hours}:${String(remainder).padStart(2, '0')}`;\n}\n\nfunction formatSignedMinutes(totalMinutes) {\n const value = Math.round(totalMinutes || 0);\n const sign = value < 0 ? '-' : '';\n return `${sign}${formatMinutes(Math.abs(value))}`;\n}\n\nfunction formatVacationDays(value) {\n const rounded = Math.round(Number(value || 0) * 100) / 100;\n return Number.isFinite(rounded) ? rounded.toString() : '0';\n}\n\nfunction updateWeeklyStatus(totalMinutes, targetHours, targetPeriod) {\n if (targetPeriod === 'Monthly') {\n weekStatus.textContent = '';\n weekStatus.style.display = 'none';\n return;\n }\n\n weekStatus.style.display = '';\n if (!targetHours) {\n weekStatus.textContent = __('Weekly target not set.');\n weekStatus.style.color = '#6c757d';\n return;\n }\n const targetMinutes = Math.round(Number(targetHours) * 60);\n const diff = totalMinutes - targetMinutes;\n if (diff >= 0) {\n weekStatus.textContent = `${__('Over by')}: ${formatMinutes(diff)}`;\n weekStatus.style.color = '#28a745';\n } else {\n weekStatus.textContent = `${__('Remaining')}: ${formatMinutes(Math.abs(diff))}`;\n weekStatus.style.color = '#dc3545';\n }\n}\n\nfunction updateMonthlyStatus(totalMinutes, targetHours, targetPeriod) {\n if (targetPeriod === 'Weekly') {\n monthStatus.textContent = '';\n monthStatus.style.display = 'none';\n return;\n }\n\n monthStatus.style.display = '';\n if (!targetHours) {\n monthStatus.textContent = __('Monthly target not set.');\n monthStatus.style.color = '#6c757d';\n return;\n }\n const targetMinutes = Math.round(Number(targetHours) * 60);\n const diff = totalMinutes - targetMinutes;\n if (diff >= 0) {\n monthStatus.textContent = `${__('Over by')}: ${formatMinutes(diff)}`;\n monthStatus.style.color = '#28a745';\n } else {\n monthStatus.textContent = `${__('Remaining')}: ${formatMinutes(Math.abs(diff))}`;\n monthStatus.style.color = '#dc3545';\n }\n}\n\nfunction updateVacationSummary(vacation) {\n if (!vacation || !vacation.enabled) {\n vacationCard.style.display = 'none';\n return;\n }\n\n vacationCard.style.display = '';\n if (!vacation.valid) {\n vacationRemaining.textContent = '--';\n vacationStatus.textContent = vacation.error || __('Vacation balance is unavailable.');\n vacationStatus.style.color = '#dc3545';\n return;\n }\n\n const remaining = Number(vacation.remaining_days || 0);\n const used = Number(vacation.used_days || 0);\n const allowance = Number(vacation.allowance_days || 0);\n\n vacationRemaining.textContent = formatVacationDays(remaining);\n vacationStatus.textContent = __('Used {0} of {1} days.', [\n formatVacationDays(used),\n formatVacationDays(allowance),\n ]);\n vacationStatus.style.color = remaining < 0 ? '#dc3545' : '#6c757d';\n}\n\nfunction showProfileMissing(message) {\n const text = message || __('Time Tracking Profile is required.');\n weekStatus.textContent = text;\n weekStatus.style.display = '';\n weekStatus.style.color = '#dc3545';\n monthStatus.textContent = '';\n monthStatus.style.display = 'none';\n vacationCard.style.display = 'none';\n}\n\nfunction updateSummary(data) {\n const weeklyMinutes = Number(data.weekly_total_minutes || 0);\n const monthlyMinutes = Number(data.monthly_total_minutes || 0);\n const hoursBalanceMinutes = Number(data.hours_balance_minutes || 0);\n const targetPeriod = data.target_period || null;\n\n weekTotal.textContent = formatMinutes(weeklyMinutes);\n monthTotal.textContent = formatMinutes(monthlyMinutes);\n hoursBalance.textContent = formatSignedMinutes(hoursBalanceMinutes);\n\n updateWeeklyStatus(weeklyMinutes, data.weekly_target_hours, targetPeriod);\n updateMonthlyStatus(monthlyMinutes, data.monthly_target_hours, targetPeriod);\n updateVacationSummary(data.vacation);\n}\n\nfrappe.call({\n method: 'time_tracking.time_tracking.workspace_summary.get_workspace_summary',\n callback: function (r) {\n const message = r.message || {};\n if (message.profile_missing) {\n showProfileMissing(message.message);\n return;\n }\n updateSummary(message);\n },\n});\n",
7+
"html": "<div class=\"tt-summary\">\n <div class=\"tt-summary-grid\">\n <div class=\"card tt-summary-card\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"week-total\">Week Total</div>\n <div class=\"tt-summary-value\" data-field=\"week-total\">0:00</div>\n <div class=\"tt-summary-status\" data-field=\"week-status\"></div>\n </div>\n </div>\n <div class=\"card tt-summary-card\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"month-total\">Month Total</div>\n <div class=\"tt-summary-value\" data-field=\"month-total\">0:00</div>\n <div class=\"tt-summary-status\" data-field=\"month-status\"></div>\n </div>\n </div>\n <div class=\"card tt-summary-card\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"week-forecast\">Forecast</div>\n <div class=\"tt-summary-value\" data-field=\"week-forecast\">0:00</div>\n </div>\n </div>\n <div class=\"card tt-summary-card\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"hours-balance\">Hours Balance</div>\n <div class=\"tt-summary-value\" data-field=\"hours-balance\">0:00</div>\n </div>\n </div>\n <div class=\"card tt-summary-card\" data-card=\"vacation\" style=\"display: none;\">\n <div class=\"card-body\">\n <div class=\"card-title text-muted\" data-label=\"vacation-remaining\">Vacation Remaining</div>\n <div class=\"tt-summary-value\" data-field=\"vacation-remaining\">0</div>\n <div class=\"tt-summary-status\" data-field=\"vacation-status\"></div>\n </div>\n </div>\n </div>\n</div>",
8+
"script": "const weekTitle = root_element.querySelector('[data-label=\"week-total\"]');\nconst monthTitle = root_element.querySelector('[data-label=\"month-total\"]');\nconst forecastTitle = root_element.querySelector('[data-label=\"week-forecast\"]');\nconst hoursBalanceTitle = root_element.querySelector('[data-label=\"hours-balance\"]');\nconst vacationTitle = root_element.querySelector('[data-label=\"vacation-remaining\"]');\n\nconst weekTotal = root_element.querySelector('[data-field=\"week-total\"]');\nconst weekStatus = root_element.querySelector('[data-field=\"week-status\"]');\nconst monthTotal = root_element.querySelector('[data-field=\"month-total\"]');\nconst monthStatus = root_element.querySelector('[data-field=\"month-status\"]');\nconst weekForecast = root_element.querySelector('[data-field=\"week-forecast\"]');\nconst hoursBalance = root_element.querySelector('[data-field=\"hours-balance\"]');\nconst vacationCard = root_element.querySelector('[data-card=\"vacation\"]');\nconst vacationRemaining = root_element.querySelector('[data-field=\"vacation-remaining\"]');\nconst vacationStatus = root_element.querySelector('[data-field=\"vacation-status\"]');\n\nif (weekTitle) {\n weekTitle.textContent = __('Week Total');\n}\nif (monthTitle) {\n monthTitle.textContent = __('Month Total');\n}\nif (forecastTitle) {\n forecastTitle.textContent = __('Forecast');\n}\nif (hoursBalanceTitle) {\n hoursBalanceTitle.textContent = __('Hours Balance');\n}\nif (vacationTitle) {\n vacationTitle.textContent = __('Vacation Remaining');\n}\n\nfunction formatMinutes(totalMinutes) {\n const minutes = Math.max(0, Math.round(totalMinutes || 0));\n const hours = Math.floor(minutes / 60);\n const remainder = minutes % 60;\n return `${hours}:${String(remainder).padStart(2, '0')}`;\n}\n\nfunction formatSignedMinutes(totalMinutes) {\n const value = Math.round(totalMinutes || 0);\n const sign = value < 0 ? '-' : '';\n return `${sign}${formatMinutes(Math.abs(value))}`;\n}\n\nfunction formatVacationDays(value) {\n const rounded = Math.round(Number(value || 0) * 100) / 100;\n return Number.isFinite(rounded) ? rounded.toString() : '0';\n}\n\nfunction updateWeeklyStatus(totalMinutes, targetHours, targetPeriod) {\n if (targetPeriod === 'Monthly') {\n weekStatus.textContent = '';\n weekStatus.style.display = 'none';\n return;\n }\n\n weekStatus.style.display = '';\n if (!targetHours) {\n weekStatus.textContent = __('Weekly target not set.');\n weekStatus.style.color = '#6c757d';\n return;\n }\n const targetMinutes = Math.round(Number(targetHours) * 60);\n const diff = totalMinutes - targetMinutes;\n if (diff >= 0) {\n weekStatus.textContent = `${__('Over by')}: ${formatMinutes(diff)}`;\n weekStatus.style.color = '#28a745';\n } else {\n weekStatus.textContent = `${__('Remaining')}: ${formatMinutes(Math.abs(diff))}`;\n weekStatus.style.color = '#dc3545';\n }\n}\n\nfunction updateMonthlyStatus(totalMinutes, targetHours, targetPeriod) {\n if (targetPeriod === 'Weekly') {\n monthStatus.textContent = '';\n monthStatus.style.display = 'none';\n return;\n }\n\n monthStatus.style.display = '';\n if (!targetHours) {\n monthStatus.textContent = __('Monthly target not set.');\n monthStatus.style.color = '#6c757d';\n return;\n }\n const targetMinutes = Math.round(Number(targetHours) * 60);\n const diff = totalMinutes - targetMinutes;\n if (diff >= 0) {\n monthStatus.textContent = `${__('Over by')}: ${formatMinutes(diff)}`;\n monthStatus.style.color = '#28a745';\n } else {\n monthStatus.textContent = `${__('Remaining')}: ${formatMinutes(Math.abs(diff))}`;\n monthStatus.style.color = '#dc3545';\n }\n}\n\nfunction updateVacationSummary(vacation) {\n if (!vacation || !vacation.enabled) {\n vacationCard.style.display = 'none';\n return;\n }\n\n vacationCard.style.display = '';\n if (!vacation.valid) {\n vacationRemaining.textContent = '--';\n vacationStatus.textContent = vacation.error || __('Vacation balance is unavailable.');\n vacationStatus.style.color = '#dc3545';\n return;\n }\n\n const remaining = Number(vacation.remaining_days || 0);\n const used = Number(vacation.used_days || 0);\n const allowance = Number(vacation.allowance_days || 0);\n\n vacationRemaining.textContent = formatVacationDays(remaining);\n vacationStatus.textContent = __('Used {0} of {1} days.', [\n formatVacationDays(used),\n formatVacationDays(allowance),\n ]);\n vacationStatus.style.color = remaining < 0 ? '#dc3545' : '#6c757d';\n}\n\nfunction showProfileMissing(message) {\n const text = message || __('Time Tracking Profile is required.');\n weekStatus.textContent = text;\n weekStatus.style.display = '';\n weekStatus.style.color = '#dc3545';\n monthStatus.textContent = '';\n monthStatus.style.display = 'none';\n if (weekForecast) {\n weekForecast.textContent = '0:00';\n }\n vacationCard.style.display = 'none';\n}\n\nfunction updateSummary(data) {\n const weeklyMinutes = Number(data.weekly_total_minutes || 0);\n const monthlyMinutes = Number(data.monthly_total_minutes || 0);\n const weeklyForecastMinutes = Number(data.weekly_forecast_minutes || 0);\n const hoursBalanceMinutes = Number(data.hours_balance_minutes || 0);\n const targetPeriod = data.target_period || null;\n\n weekTotal.textContent = formatMinutes(weeklyMinutes);\n monthTotal.textContent = formatMinutes(monthlyMinutes);\n if (weekForecast) {\n weekForecast.textContent = formatMinutes(weeklyForecastMinutes);\n }\n hoursBalance.textContent = formatSignedMinutes(hoursBalanceMinutes);\n\n updateWeeklyStatus(weeklyMinutes, data.weekly_target_hours, targetPeriod);\n updateMonthlyStatus(monthlyMinutes, data.monthly_target_hours, targetPeriod);\n updateVacationSummary(data.vacation);\n}\n\nfrappe.call({\n method: 'time_tracking.time_tracking.workspace_summary.get_workspace_summary',\n callback: function (r) {\n const message = r.message || {};\n if (message.profile_missing) {\n showProfileMissing(message.message);\n return;\n }\n updateSummary(message);\n },\n});\n",
99
"style": ".tt-summary {\n display: flex;\n justify-content: center;\n margin: 6px 0 4px;\n}\n\n.tt-summary-grid {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n width: 100%;\n justify-content: center;\n}\n\n.tt-summary-card {\n min-width: 260px;\n flex: 1 1 260px;\n}\n\n.tt-summary-value {\n font-size: 26px;\n font-weight: 600;\n}\n\n.tt-summary-status {\n font-size: 12px;\n color: #6c757d;\n}\n"
1010
}
11-
]
11+
]

0 commit comments

Comments
 (0)