Ver 9 Bulk add

This commit is contained in:
Flatlogic Bot 2026-02-04 00:24:27 +00:00
parent dbb09ad772
commit af006bef5f
6 changed files with 153 additions and 68 deletions

View File

@ -2,6 +2,23 @@ from django import forms
from .models import WorkLog, Project, Worker, Team from .models import WorkLog, Project, Worker, Team
class WorkLogForm(forms.ModelForm): 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'})) team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False, empty_label="Select Team", widget=forms.Select(attrs={'class': 'form-control'}))
class Meta: class Meta:

View File

@ -13,27 +13,41 @@
<div class="container mb-5 mt-n4"> <div class="container mb-5 mt-n4">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-8"> <div class="col-lg-10">
<div class="card p-4 shadow-sm"> <div class="card p-4 shadow-sm">
<form method="post" id="workLogForm"> <form method="post" id="workLogForm">
{% csrf_token %} {% csrf_token %}
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-4"> <div class="col-md-3">
<label class="form-label fw-bold">Date</label> <label class="form-label fw-bold">Start Date</label>
{{ form.date }} {{ form.date }}
{% if form.date.errors %} {% if form.date.errors %}
<div class="text-danger mt-1 small">{{ form.date.errors }}</div> <div class="text-danger mt-1 small">{{ form.date.errors }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<label class="form-label fw-bold">End Date (Optional)</label>
{{ form.end_date }}
<div class="mt-2">
<div class="form-check form-check-inline">
{{ form.include_saturday }}
<label class="form-check-label small" for="{{ form.include_saturday.id_for_label }}">Sat</label>
</div>
<div class="form-check form-check-inline">
{{ form.include_sunday }}
<label class="form-check-label small" for="{{ form.include_sunday.id_for_label }}">Sun</label>
</div>
</div>
</div>
<div class="col-md-3">
<label class="form-label fw-bold">Project</label> <label class="form-label fw-bold">Project</label>
{{ form.project }} {{ form.project }}
{% if form.project.errors %} {% if form.project.errors %}
<div class="text-danger mt-1 small">{{ form.project.errors }}</div> <div class="text-danger mt-1 small">{{ form.project.errors }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<label class="form-label fw-bold">Team (Optional)</label> <label class="form-label fw-bold">Team (Optional)</label>
{{ form.team }} {{ form.team }}
{% if form.team.errors %} {% if form.team.errors %}
@ -93,22 +107,22 @@
</h5> </h5>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p class="fw-bold">The following workers have already been logged for {{ conflict_date }}:</p> <p class="fw-bold">The following duplicate entries were found:</p>
<div class="card bg-light mb-3"> <div class="card bg-light mb-3" style="max-height: 200px; overflow-y: auto;">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% for worker in conflicting_workers %} {% for conflict in conflicting_workers %}
<li class="list-group-item bg-transparent">{{ worker.name }}</li> <li class="list-group-item bg-transparent">{{ conflict.name }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<p>How would you like to proceed?</p> <p>How would you like to proceed?</p>
<div class="alert alert-info small mb-0"> <div class="alert alert-info small mb-0">
<strong>Skip:</strong> Log only the new workers. Keep existing logs as they are.<br> <strong>Skip:</strong> Log only the new entries. Existing logs remain unchanged.<br>
<strong>Overwrite:</strong> Remove these workers from previous logs for this date and add them here. <strong>Overwrite:</strong> Update existing logs for these dates with the new project/team selection.
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="submitConflict('skip')">Skip Duplicate Workers</button> <button type="button" class="btn btn-secondary" onclick="submitConflict('skip')">Skip Duplicates</button>
<button type="button" class="btn btn-primary" onclick="submitConflict('overwrite')">Overwrite Existing</button> <button type="button" class="btn btn-primary" onclick="submitConflict('overwrite')">Overwrite Existing</button>
</div> </div>
</div> </div>
@ -139,9 +153,14 @@
const teamId = this.value; const teamId = this.value;
if (teamId && teamWorkersMap[teamId]) { if (teamId && teamWorkersMap[teamId]) {
const workerIds = 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) { workerIds.forEach(function(id) {
// Find the checkbox for this worker ID
const checkbox = document.querySelector(`input[name="workers"][value="${id}"]`); const checkbox = document.querySelector(`input[name="workers"][value="${id}"]`);
if (checkbox) { if (checkbox) {
checkbox.checked = true; checkbox.checked = true;

View File

@ -79,65 +79,108 @@ def log_attendance(request):
if request.method == 'POST': if request.method == 'POST':
form = WorkLogForm(request.POST, user=request.user) form = WorkLogForm(request.POST, user=request.user)
if form.is_valid(): 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'] selected_workers = form.cleaned_data['workers']
project = form.cleaned_data['project']
notes = form.cleaned_data['notes']
conflict_action = request.POST.get('conflict_action') conflict_action = request.POST.get('conflict_action')
# Check for existing logs for these workers on this date # Generate Target Dates
# We want to find workers who ARE in selected_workers AND have a WorkLog on 'date' target_dates = []
conflicting_workers = Worker.objects.filter( if end_date and end_date >= start_date:
work_logs__date=date, curr = start_date
id__in=selected_workers.values_list('id', flat=True) while curr <= end_date:
).distinct() # 5 = Saturday, 6 = Sunday
if (curr.weekday() == 5 and not include_sat) or (curr.weekday() == 6 and not include_sun):
if conflicting_workers.exists() and not conflict_action: curr += timedelta(days=1)
context = { continue
'form': form, target_dates.append(curr)
'team_workers_json': json.dumps(team_workers_map), curr += timedelta(days=1)
'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).")
else: else:
# No conflicts initially target_dates = [start_date]
messages.success(request, "Work log saved successfully.")
# Save the new log if not target_dates:
work_log = form.save(commit=False) messages.warning(request, "No valid dates selected (check weekends).")
if request.user.is_authenticated: return render(request, 'core/log_attendance.html', {
work_log.supervisor = request.user 'form': form, 'team_workers_json': json.dumps(team_workers_map)
work_log.save() })
# Manually set workers # Check Conflicts - Scan all target dates
work_log.workers.set(workers_to_save) 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)
# 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') return redirect('home')
else: else:
form = WorkLogForm(user=request.user if request.user.is_authenticated else None) form = WorkLogForm(user=request.user if request.user.is_authenticated else None)

View File

@ -23,6 +23,12 @@ h1, h2, h3, .heading-font {
font-weight: 700; font-weight: 700;
} }
.navbar {
position: sticky;
top: 0;
z-index: 1000;
}
.dashboard-header { .dashboard-header {
background: linear-gradient(135deg, var(--primary-color) 0%, #334155 100%); background: linear-gradient(135deg, var(--primary-color) 0%, #334155 100%);
color: var(--white); color: var(--white);