diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index ac2da0c..3e95dc8 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index afbb350..a9c72b5 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index 3238eec..17cd42a 100644 --- a/core/forms.py +++ b/core/forms.py @@ -2,6 +2,23 @@ from django import forms from .models import WorkLog, Project, Worker, Team class WorkLogForm(forms.ModelForm): + end_date = forms.DateField( + required=False, + widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + label="End Date (Optional)" + ) + include_saturday = forms.BooleanField( + required=False, + label="Include Saturday", + initial=False, + widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) + ) + include_sunday = forms.BooleanField( + required=False, + label="Include Sunday", + initial=False, + widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) + ) team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False, empty_label="Select Team", widget=forms.Select(attrs={'class': 'form-control'})) class Meta: @@ -39,4 +56,4 @@ class WorkLogForm(forms.ModelForm): else: self.fields['project'].queryset = projects_qs self.fields['workers'].queryset = workers_qs - self.fields['team'].queryset = teams_qs + self.fields['team'].queryset = teams_qs \ No newline at end of file diff --git a/core/templates/core/log_attendance.html b/core/templates/core/log_attendance.html index 71cae69..3dd1563 100644 --- a/core/templates/core/log_attendance.html +++ b/core/templates/core/log_attendance.html @@ -13,27 +13,41 @@
-
+
{% csrf_token %}
-
- +
+ {{ form.date }} {% if form.date.errors %}
{{ form.date.errors }}
{% endif %}
-
+
+ + {{ form.end_date }} +
+
+ {{ form.include_saturday }} + +
+
+ {{ form.include_sunday }} + +
+
+
+
{{ form.project }} {% if form.project.errors %}
{{ form.project.errors }}
{% endif %}
-
+
{{ form.team }} {% if form.team.errors %} @@ -93,22 +107,22 @@
@@ -139,9 +153,14 @@ const teamId = this.value; if (teamId && teamWorkersMap[teamId]) { const workerIds = teamWorkersMap[teamId]; - // Select workers belonging to the team + // Uncheck all first? No, maybe append. Let's append as per common expectations unless explicit clear needed. + // Actually, if I change team, I probably expect to select THAT team's workers. + // Let's clear and select. + // But maybe I want to mix teams. + // User didn't specify. Previous logic was: select workers belonging to team. + // Let's stick to "select", don't uncheck others. + workerIds.forEach(function(id) { - // Find the checkbox for this worker ID const checkbox = document.querySelector(`input[name="workers"][value="${id}"]`); if (checkbox) { checkbox.checked = true; diff --git a/core/views.py b/core/views.py index 7f3606f..05ee53e 100644 --- a/core/views.py +++ b/core/views.py @@ -79,65 +79,108 @@ def log_attendance(request): if request.method == 'POST': form = WorkLogForm(request.POST, user=request.user) if form.is_valid(): - date = form.cleaned_data['date'] + start_date = form.cleaned_data['date'] + end_date = form.cleaned_data.get('end_date') + include_sat = form.cleaned_data.get('include_saturday') + include_sun = form.cleaned_data.get('include_sunday') selected_workers = form.cleaned_data['workers'] + project = form.cleaned_data['project'] + notes = form.cleaned_data['notes'] conflict_action = request.POST.get('conflict_action') - # Check for existing logs for these workers on this date - # We want to find workers who ARE in selected_workers AND have a WorkLog on 'date' - conflicting_workers = Worker.objects.filter( - work_logs__date=date, - id__in=selected_workers.values_list('id', flat=True) - ).distinct() - - if conflicting_workers.exists() and not conflict_action: - context = { - 'form': form, - 'team_workers_json': json.dumps(team_workers_map), - 'conflicting_workers': conflicting_workers, - 'is_conflict': True, - 'conflict_date': date, - } - return render(request, 'core/log_attendance.html', context) - - # If we are here, either no conflicts or action is chosen - workers_to_save = list(selected_workers) - - if conflict_action == 'skip': - # Exclude conflicting workers - conflicting_ids = conflicting_workers.values_list('id', flat=True) - workers_to_save = [w for w in selected_workers if w.id not in conflicting_ids] - - if not workers_to_save: - messages.warning(request, "No new workers to log (all skipped).") - return redirect('home') - - messages.success(request, f"Logged {len(workers_to_save)} workers (skipped {conflicting_workers.count()} duplicates).") - - elif conflict_action == 'overwrite': - # Remove conflicting workers from their OLD logs - for worker in conflicting_workers: - old_logs = WorkLog.objects.filter(date=date, workers=worker) - for log in old_logs: - log.workers.remove(worker) - # Cleanup empty logs - if log.workers.count() == 0: - log.delete() - messages.success(request, f"Logged {len(workers_to_save)} workers (overwrote {conflicting_workers.count()} previous entries).") - + # Generate Target Dates + target_dates = [] + if end_date and end_date >= start_date: + curr = start_date + while curr <= end_date: + # 5 = Saturday, 6 = Sunday + if (curr.weekday() == 5 and not include_sat) or (curr.weekday() == 6 and not include_sun): + curr += timedelta(days=1) + continue + target_dates.append(curr) + curr += timedelta(days=1) else: - # No conflicts initially - messages.success(request, "Work log saved successfully.") + target_dates = [start_date] - # Save the new log - work_log = form.save(commit=False) - if request.user.is_authenticated: - work_log.supervisor = request.user - work_log.save() + if not target_dates: + messages.warning(request, "No valid dates selected (check weekends).") + return render(request, 'core/log_attendance.html', { + 'form': form, 'team_workers_json': json.dumps(team_workers_map) + }) + + # Check Conflicts - Scan all target dates + if not conflict_action: + conflicts = [] + for d in target_dates: + # Find workers who already have a log on this date + existing_logs = WorkLog.objects.filter(date=d, workers__in=selected_workers).distinct() + for log in existing_logs: + # Which of the selected workers are in this log? + for w in log.workers.all(): + if w in selected_workers: + # Avoid adding duplicates if multiple logs exist for same worker/day (rare but possible) + conflict_entry = {'name': f"{w.name} ({d.strftime('%Y-%m-%d')})"} + if conflict_entry not in conflicts: + conflicts.append(conflict_entry) + + if conflicts: + context = { + 'form': form, + 'team_workers_json': json.dumps(team_workers_map), + 'conflicting_workers': conflicts, + 'is_conflict': True, + } + return render(request, 'core/log_attendance.html', context) - # Manually set workers - work_log.workers.set(workers_to_save) + # Execution Phase + created_count = 0 + skipped_count = 0 + overwritten_count = 0 + for d in target_dates: + # Find conflicts for this specific day + day_conflicts = Worker.objects.filter( + work_logs__date=d, + id__in=selected_workers.values_list('id', flat=True) + ).distinct() + + workers_to_save = list(selected_workers) + + if day_conflicts.exists(): + if conflict_action == 'skip': + conflicting_ids = day_conflicts.values_list('id', flat=True) + workers_to_save = [w for w in selected_workers if w.id not in conflicting_ids] + skipped_count += day_conflicts.count() + + elif conflict_action == 'overwrite': + # Remove conflicting workers from their OLD logs + for worker in day_conflicts: + old_logs = WorkLog.objects.filter(date=d, workers=worker) + for log in old_logs: + log.workers.remove(worker) + if log.workers.count() == 0: + log.delete() + overwritten_count += day_conflicts.count() + # workers_to_save remains full list + + if workers_to_save: + # Create Log + log = WorkLog.objects.create( + date=d, + project=project, + notes=notes, + supervisor=request.user if request.user.is_authenticated else None + ) + log.workers.set(workers_to_save) + created_count += len(workers_to_save) + + msg = f"Logged {created_count} entries." + if skipped_count: + msg += f" Skipped {skipped_count} duplicates." + if overwritten_count: + msg += f" Overwrote {overwritten_count} previous entries." + + messages.success(request, msg) return redirect('home') else: form = WorkLogForm(user=request.user if request.user.is_authenticated else None) @@ -567,4 +610,4 @@ def add_adjustment(request): ) messages.success(request, f"{adj_type} of R{amount} added for {worker.name}.") - return redirect('payroll_dashboard') \ No newline at end of file + return redirect('payroll_dashboard') diff --git a/static/css/custom.css b/static/css/custom.css index 13343f9..8767672 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -23,6 +23,12 @@ h1, h2, h3, .heading-font { font-weight: 700; } +.navbar { + position: sticky; + top: 0; + z-index: 1000; +} + .dashboard-header { background: linear-gradient(135deg, var(--primary-color) 0%, #334155 100%); color: var(--white);