From af006bef5fa9a4a720b866c80a585bbbd32a14f0 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Feb 2026 00:24:27 +0000 Subject: [PATCH] Ver 9 Bulk add --- core/__pycache__/forms.cpython-311.pyc | Bin 3210 -> 3830 bytes core/__pycache__/views.cpython-311.pyc | Bin 27923 -> 29531 bytes core/forms.py | 19 ++- core/templates/core/log_attendance.html | 47 +++++--- core/views.py | 149 +++++++++++++++--------- static/css/custom.css | 6 + 6 files changed, 153 insertions(+), 68 deletions(-) diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index ac2da0c5ee185c7064fec8dbf3b3debba5ac774d..3e95dc8a2ada58c79f6f8257b137acf2f101e125 100644 GIT binary patch delta 1244 zcma)5%}>-o6z}YIyJf#xK3u`i6(mqZJ*ZKmF-C=zMBxC&WDjo2&H#nduG2QU99&{J z5MvZm{|1!E&BTw3Coi5{l09rDTs(SP5@L)e=XF^Q$idgnFMaRL@Aqb2XP$NaI+}Wu zOvVImUtT^nvawgG&!HFS)wd%uMj?TOXCwZ>P^OHf_2_C?5Z>|8u$&a6qfH+p+@#36 zu@)RN(0XJQaV8%PTaT}b+~=cVxt4#F5>E@e^RkPD&CYPiP@w-Aq|%K>XJc9 zX{NYfc%QYU_N+|}30VOvO9EC`LwwbIG%VM;EKDKXx+Le?mW9VC*FG+2Z7hD@rgL}f z>0z7B4>J+#F8Py~Ds%62!M0p#n`pH^p6!? z!?tu&)51)qNlz|+KK5mTQe=7Kt66-*D4%((7)6ze4ln_Ez)f?_kIFn8Lp zik_>9^nM%9&<+kPYFLJA=q7E4$*Elp&*BOT@o{33f}5Ej`n)DG`JqQ>$ssNU0Tw6o zMYog>$_IS^2im+&q1`N6r1mVQvgjk;0YV)Xv22$(L6#=?6q*E30brp&;V5Y#s`yg) zYov=io&NXmb7=r348GUJi7-0ke~9*=!#;~Xka{4V_OHo9=#0N4_o1u)x;!AAg777O zQ|?0D?|;b#DuaO5L61Ws18@QWs<9Sm@328kOizL@6AS0zAF;#@+ct@A1xc2?3GJJ- zACEu-nQAmTop*LR%~HFOpl?jQYhz;GoUx5Ud8YuJU3wX+h?Z>;+6(>#fI)yukV)qA z!67IR)6C~JM6*2Vz)iVMgN?z>qyTVRoD$FcUKE;7O*yvgJulZ<(>q^pr#?NYlzT@j z{8h#_l(DKZR!jBNqoOKp2^_Wo7H`*MLg&DWS3Rf!HvQ;HpZ;5nrGKXz|ir7(42jYI|+O4?OtZ*h?ho@ z6hSOJ#?rznA~r$_v9d{HX(xk~ot=WHq;lTIfS_-gA2a{|+xO<<$M~ZI{tM4@kc^)b z4-?fI_-~chXz+f_Cwkie9pbskKj6t>ML|SuTRR#;u^MZ9fQRoJ8yZ5d)UJ#vKh3&Mm&$#O5DBo;2|bXh-4qdO!O#!3<| z5yr2opXP-YM~n!wOY^pDAv=b!C5p5cyjtacRSvl6S- zzfOK443jKjVYp|r;UstziUuAXgXD3*K>+N4pQ`M?eNIkCjbBd diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index afbb35076db991bdda7147f1a1cfae7898dd5fa0..a9c72b5e8bcb18fa47e851eea94dd2e582ba8e47 100644 GIT binary patch delta 5271 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 Tyq{&avl(XH--rsIN3s6_UeoUt 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);