diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index 038b418..1a20f2e 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -659,94 +659,104 @@ document.addEventListener('DOMContentLoaded', function() { // ================================================================= // CHART.JS — Monthly Totals (Line Chart) + // Wrapped in try-catch so a Chart.js failure doesn't prevent + // the rest of the page's JavaScript from running. // ================================================================= - const monthlyCtx = document.getElementById('monthlyChart'); - if (monthlyCtx) { - new Chart(monthlyCtx, { - type: 'line', - data: { - labels: chartLabels, - datasets: [{ - label: 'Total Paid', - data: chartTotals, - borderColor: '#10b981', - backgroundColor: 'rgba(16, 185, 129, 0.1)', - fill: true, - tension: 0.3, - pointRadius: 4, - pointHoverRadius: 6, - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { display: false }, - tooltip: { - callbacks: { - label: function(context) { - return fmt(context.parsed.y); + try { + const monthlyCtx = document.getElementById('monthlyChart'); + if (monthlyCtx) { + new Chart(monthlyCtx, { + type: 'line', + data: { + labels: chartLabels, + datasets: [{ + label: 'Total Paid', + data: chartTotals, + borderColor: '#10b981', + backgroundColor: 'rgba(16, 185, 129, 0.1)', + fill: true, + tension: 0.3, + pointRadius: 4, + pointHoverRadius: 6, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { + label: function(context) { + return fmt(context.parsed.y); + } + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + callback: function(val) { return 'R ' + val.toLocaleString(); } } } } - }, - scales: { - y: { - beginAtZero: true, - ticks: { - callback: function(val) { return 'R ' + val.toLocaleString(); } - } - } } - } - }); + }); + } + } catch (e) { + console.warn('Monthly chart failed to render:', e); } // ================================================================= // CHART.JS — Per-Project Costs (Stacked Bar Chart) // ================================================================= - const projectCtx = document.getElementById('projectChart'); - if (projectCtx) { - // Color palette for projects - const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']; - const datasets = projectChartData.map(function(proj, i) { - return { - label: proj.name, - data: proj.data, - backgroundColor: colors[i % colors.length], - }; - }); + try { + const projectCtx = document.getElementById('projectChart'); + if (projectCtx) { + // Color palette for projects + const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']; + const datasets = projectChartData.map(function(proj, i) { + return { + label: proj.name, + data: proj.data, + backgroundColor: colors[i % colors.length], + }; + }); - new Chart(projectCtx, { - type: 'bar', - data: { - labels: chartLabels, - datasets: datasets, - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - tooltip: { - callbacks: { - label: function(context) { - return context.dataset.label + ': ' + fmt(context.parsed.y); + new Chart(projectCtx, { + type: 'bar', + data: { + labels: chartLabels, + datasets: datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + tooltip: { + callbacks: { + label: function(context) { + return context.dataset.label + ': ' + fmt(context.parsed.y); + } + } + } + }, + scales: { + x: { stacked: true }, + y: { + stacked: true, + beginAtZero: true, + ticks: { + callback: function(val) { return 'R ' + val.toLocaleString(); } } } } - }, - scales: { - x: { stacked: true }, - y: { - stacked: true, - beginAtZero: true, - ticks: { - callback: function(val) { return 'R ' + val.toLocaleString(); } - } - } } - } - }); + }); + } + } catch (e) { + console.warn('Project chart failed to render:', e); } // ================================================================= @@ -971,12 +981,17 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('deleteAdjType').textContent = adjType; document.getElementById('deleteAdjWorker').textContent = adjWorker; // Close edit modal, open delete modal - bootstrap.Modal.getInstance(document.getElementById('editAdjustmentModal')).hide(); - new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show(); + var editModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('editAdjustmentModal')); + editModal.hide(); + // Wait for edit modal to finish hiding before showing delete modal + document.getElementById('editAdjustmentModal').addEventListener('hidden.bs.modal', function handler() { + document.getElementById('editAdjustmentModal').removeEventListener('hidden.bs.modal', handler); + bootstrap.Modal.getOrCreateInstance(document.getElementById('deleteConfirmModal')).show(); + }); }; // Show the modal - new bootstrap.Modal(document.getElementById('editAdjustmentModal')).show(); + bootstrap.Modal.getOrCreateInstance(document.getElementById('editAdjustmentModal')).show(); }); }); @@ -1004,7 +1019,7 @@ document.addEventListener('DOMContentLoaded', function() { modalBody.appendChild(loadingDiv); // Show modal - new bootstrap.Modal(document.getElementById('previewPayslipModal')).show(); + bootstrap.Modal.getOrCreateInstance(document.getElementById('previewPayslipModal')).show(); // Fetch preview data fetch('/payroll/preview/' + workerId + '/') diff --git a/core/templates/core/work_history.html b/core/templates/core/work_history.html index 4388ffb..8e4b194 100644 --- a/core/templates/core/work_history.html +++ b/core/templates/core/work_history.html @@ -4,10 +4,30 @@ {% block title %}Work History | Fox Fitt{% endblock %} {% block content %} + +
+ + {# === PAGE HEADER with view toggle and export === #}

Work History

+ {# View toggle — List vs Calendar #} + + {# CSV Export button — keeps the current filters in the export URL #} @@ -19,10 +39,18 @@
- {# --- Filter Bar --- #} + {# === FILTER BAR === #}
- {# --- Work Log Table --- #} + + {% if view_mode == 'calendar' %} + {# =============================================================== #} + {# === CALENDAR VIEW === #} + {# =============================================================== #} + + {# Month navigation header #} +
+
+
+ + + +
+ {{ month_name }} +
+ + + +
+
+
+ + {# Calendar grid #} +
+
+ {# Day-of-week header row #} +
+
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+
Sun
+
+ + {# Calendar weeks — each row is 7 day cells #} + {% for week in calendar_weeks %} +
+ {% for day in week %} +
0 %}data-date="{{ day.date|date:'Y-m-d' }}"{% endif %}> + {# Day number + badge count #} +
+ {{ day.day }} + {% if day.count > 0 %} + {{ day.count }} + {% endif %} +
+ {# Mini log indicators (show first 3 entries) #} + {% for log in day.records|slice:":3" %} +
+ + {% if log.payroll_records.exists %} + + {% else %} + + {% endif %} + {{ log.project.name }} + +
+ {% endfor %} + {# "and X more" indicator #} + {% if day.count > 3 %} +
+ +{{ day.count|add:"-3" }} more +
+ {% endif %} +
+ {% endfor %} +
+ {% endfor %} +
+
+ + {# === Day Detail Panel === #} + {# Hidden by default. When you click a day cell with logs, this panel + appears showing full details for all entries on that day. #} +
+
+
+ Details +
+ +
+
+ {# Content built by JavaScript #} +
+
+ + {# Pass calendar detail data to JavaScript safely using json_script #} + {{ calendar_detail|json_script:"calDetailJson" }} + + + + {# Calendar-specific CSS #} + + + + {% else %} + {# =============================================================== #} + {# === LIST VIEW (TABLE) === #} + {# =============================================================== #}
@@ -138,5 +465,7 @@
+ {% endif %} +
{% endblock %} diff --git a/core/views.py b/core/views.py index 0236a76..395b2e9 100644 --- a/core/views.py +++ b/core/views.py @@ -5,6 +5,7 @@ import csv import json import datetime +import calendar as cal_module from decimal import Decimal from django.shortcuts import render, redirect, get_object_or_404 @@ -357,8 +358,9 @@ def attendance_log(request): # === WORK LOG HISTORY === -# Shows a table of all work logs with filters. +# Shows work logs in two modes: a table list or a monthly calendar grid. # Supervisors only see their own projects. Admins see everything. +# The calendar view groups logs by day and lets you click a day to see details. @login_required def work_history(request): @@ -410,6 +412,20 @@ def work_history(request): active=True, supervisors=user ).order_by('name') + # --- View mode: list or calendar --- + view_mode = request.GET.get('view', 'list') + today = timezone.now().date() + + # Build a query string that preserves all current filters + # (used by the List/Calendar toggle links to keep filters when switching) + filter_params = '' + if worker_filter: + filter_params += '&worker=' + worker_filter + if project_filter: + filter_params += '&project=' + project_filter + if status_filter: + filter_params += '&status=' + status_filter + context = { 'logs': logs, 'filter_workers': filter_workers, @@ -418,7 +434,114 @@ def work_history(request): 'selected_project': project_filter, 'selected_status': status_filter, 'is_admin': is_admin(user), + 'view_mode': view_mode, + 'filter_params': filter_params, } + + # === CALENDAR MODE === + # Build a monthly grid of days, each containing the work logs for that day. + # Also build a JSON object keyed by date string for the JavaScript + # click-to-see-details panel. + if view_mode == 'calendar': + # Get target month from URL (default: current month) + try: + target_year = int(request.GET.get('year', today.year)) + target_month = int(request.GET.get('month', today.month)) + if not (1 <= target_month <= 12): + target_year, target_month = today.year, today.month + except (ValueError, TypeError): + target_year, target_month = today.year, today.month + + # Build the calendar grid using Python's calendar module. + # monthdatescalendar() returns a list of weeks, where each week is + # a list of 7 datetime.date objects (including overflow from prev/next month). + cal = cal_module.Calendar(firstweekday=0) # Week starts on Monday + month_dates = cal.monthdatescalendar(target_year, target_month) + + # Get the full date range for the calendar grid (includes overflow days) + first_display_date = month_dates[0][0] + last_display_date = month_dates[-1][-1] + + # Filter logs to only this date range (improves performance) + month_logs = logs.filter(date__range=[first_display_date, last_display_date]) + + # Group logs by date string for quick lookup + logs_by_date = {} + for log in month_logs: + date_key = log.date.isoformat() + if date_key not in logs_by_date: + logs_by_date[date_key] = [] + logs_by_date[date_key].append(log) + + # Build the calendar_weeks structure that the template iterates over. + # Each day cell has: date, day number, whether it's the current month, + # a list of log objects, and a count badge number. + calendar_weeks = [] + for week in month_dates: + week_data = [] + for day in week: + date_key = day.isoformat() + day_logs = logs_by_date.get(date_key, []) + week_data.append({ + 'date': day, + 'day': day.day, + 'is_current_month': day.month == target_month, + 'is_today': day == today, + 'records': day_logs, + 'count': len(day_logs), + }) + calendar_weeks.append(week_data) + + # Build detail data for JavaScript — when you click a day cell, + # the JS reads this JSON to populate the detail panel below the calendar. + # NOTE: Pass raw Python dict, not json.dumps() — the template's + # |json_script filter handles serialization. + calendar_detail = {} + for date_key, day_logs in logs_by_date.items(): + calendar_detail[date_key] = [] + for log in day_logs: + entry = { + 'project': log.project.name, + 'workers': [w.name for w in log.workers.all()], + 'supervisor': ( + log.supervisor.get_full_name() or log.supervisor.username + ) if log.supervisor else '-', + 'notes': log.notes or '', + 'is_paid': log.payroll_records.exists(), + 'overtime': log.get_overtime_amount_display() if log.overtime_amount > 0 else '', + } + # Only show cost data to admins + if is_admin(user): + entry['amount'] = float( + sum(w.daily_rate for w in log.workers.all()) + ) + calendar_detail[date_key].append(entry) + + # Calculate previous/next month for navigation arrows + if target_month == 1: + prev_year, prev_month = target_year - 1, 12 + else: + prev_year, prev_month = target_year, target_month - 1 + + if target_month == 12: + next_year, next_month = target_year + 1, 1 + else: + next_year, next_month = target_year, target_month + 1 + + month_name = datetime.date(target_year, target_month, 1).strftime('%B %Y') + + context.update({ + 'calendar_weeks': calendar_weeks, + 'calendar_detail': calendar_detail, + 'curr_year': target_year, + 'curr_month': target_month, + 'month_name': month_name, + 'prev_year': prev_year, + 'prev_month': prev_month, + 'next_year': next_year, + 'next_month': next_month, + }) + return render(request, 'core/work_history.html', context) @@ -722,6 +845,11 @@ def payroll_dashboard(request): team.workers.filter(active=True).values_list('id', flat=True) ) + # NOTE: Pass raw Python objects here, NOT json.dumps() strings. + # The template uses Django's |json_script filter which handles + # JSON serialization. If we pre-serialize with json.dumps(), the + # filter double-encodes the data and JavaScript receives strings + # instead of arrays/objects, which crashes the entire script. context = { 'workers_data': workers_data, 'paid_records': paid_records, @@ -731,15 +859,15 @@ def payroll_dashboard(request): 'active_tab': status_filter, 'all_workers': all_workers, 'all_teams': all_teams, - 'team_workers_map_json': json.dumps(team_workers_map), + 'team_workers_map_json': team_workers_map, 'adjustment_types': PayrollAdjustment.TYPE_CHOICES, 'active_projects': active_projects, 'loans': loans, 'loan_filter': loan_filter, - 'chart_labels_json': json.dumps(chart_labels), - 'chart_totals_json': json.dumps(chart_totals), - 'project_chart_json': json.dumps(project_chart_data), - 'overtime_data_json': json.dumps(all_ot_data), + 'chart_labels_json': chart_labels, + 'chart_totals_json': chart_totals, + 'project_chart_json': project_chart_data, + 'overtime_data_json': all_ot_data, 'today': today, # For pre-filling date fields in modals 'active_loans_count': active_loans_count, 'active_loans_balance': active_loans_balance,