This commit is contained in:
Flatlogic Bot 2026-02-22 13:31:37 +00:00
parent d513f6ec09
commit 306fb0e95d
6 changed files with 470 additions and 60 deletions

View File

@ -4,64 +4,97 @@
{% block title %}Dashboard | FoxFitt{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">Welcome back, {{ user.first_name|default:user.username }}!</h1>
<a href="{% url 'attendance_log' %}" class="btn text-white shadow-sm" style="background-color: #10b981;">
<i class="fas fa-plus fa-sm text-white-50 me-1"></i> Log Work
</a>
<!-- Gradient Header -->
<div class="dashboard-header mb-5 rounded shadow-sm p-4 d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0 text-white" style="font-family: 'Poppins', sans-serif;">Dashboard</h1>
<p class="text-white-50 mb-0">Welcome back, {{ user.first_name|default:user.username }}!</p>
</div>
<a href="{% url 'attendance_log' %}" class="btn btn-accent shadow-sm">
<i class="fas fa-plus fa-sm me-1"></i> Log Daily Work
</a>
</div>
<!-- Stats Row -->
<div class="row g-4 mb-4">
<!-- Active Workers Card -->
<div class="col-xl-4 col-md-6">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #10b981 !important;">
<div class="container py-2" style="margin-top: -3rem;">
{% if is_admin %}
<!-- Admin View -->
<div class="row g-4 mb-4 position-relative">
<!-- Outstanding Payments Card -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #ef4444;">
Outstanding Payments</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">${{ outstanding_payments|floatformat:2 }}</div>
</div>
<div class="col-auto">
<i class="fas fa-exclamation-circle fa-2x text-danger opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Paid This Month Card -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
Active Workers</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ total_workers|default:"0" }}</div>
Paid This Month</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">${{ paid_this_month|floatformat:2 }}</div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x text-secondary opacity-50"></i>
<i class="fas fa-check-circle fa-2x text-success opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Projects Card -->
<div class="col-xl-4 col-md-6">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #3b82f6 !important;">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #3b82f6;">
Active Projects</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ total_projects|default:"0" }}</div>
</div>
<div class="col-auto">
<i class="fas fa-hard-hat fa-2x text-secondary opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Today's Attendance Card -->
<div class="col-xl-4 col-md-6">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #f59e0b !important;">
<!-- Active Loans Card -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;">
Today's Logs</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ today_attendance|default:"0" }}</div>
Active Loans ({{ active_loans_count }})</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">${{ active_loans_balance|floatformat:2 }}</div>
</div>
<div class="col-auto">
<i class="fas fa-clipboard-check fa-2x text-secondary opacity-50"></i>
<i class="fas fa-hand-holding-usd fa-2x text-warning opacity-50"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Outstanding by Project -->
<div class="col-xl-3 col-md-6">
<div class="card stat-card h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #3b82f6;">
Outstanding by Project</div>
<div class="mb-0 text-gray-800" style="font-size: 0.85rem; max-height: 60px; overflow-y: auto;">
{% if outstanding_by_project %}
<ul class="list-unstyled mb-0">
{% for proj, amount in outstanding_by_project.items %}
<li><strong>{{ proj }}:</strong> ${{ amount|floatformat:2 }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
<div class="col-auto">
<i class="fas fa-chart-pie fa-2x text-primary opacity-50"></i>
</div>
</div>
</div>
@ -69,18 +102,223 @@
</div>
</div>
<!-- Content Row -->
<div class="row">
<div class="col-lg-12 mb-4">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header py-3 bg-white d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Recent Activity</h6>
<!-- Quick Actions and This Week -->
<div class="row mb-4">
<!-- This Week -->
<div class="col-lg-4 mb-4 mb-lg-0">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">This Week Summary</h6>
</div>
<div class="card-body">
<p class="text-muted mb-0">No recent activity to show yet. Start by logging today's attendance!</p>
<div class="card-body text-center d-flex flex-column justify-content-center">
<div class="h1 mb-0 font-weight-bold text-primary">{{ this_week_logs }}</div>
<div class="text-muted">Work Logs Created This Week</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-lg-8">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Quick Actions</h6>
</div>
<div class="card-body d-flex align-items-center justify-content-around flex-wrap">
<a href="{% url 'attendance_log' %}" class="btn btn-lg btn-outline-primary mb-2">
<i class="fas fa-clipboard-list mb-2 d-block fa-2x"></i> Log Work
</a>
<a href="#" class="btn btn-lg btn-outline-success mb-2">
<i class="fas fa-money-check-alt mb-2 d-block fa-2x"></i> Run Payroll
</a>
<a href="{% url 'work_history' %}" class="btn btn-lg btn-outline-secondary mb-2">
<i class="fas fa-history mb-2 d-block fa-2x"></i> View History
</a>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Activity -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Recent Activity</h6>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for log in recent_activity %}
<div class="list-group-item px-4 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">{{ log.project.name }}</h6>
<small class="text-muted">{{ log.date }} &middot; {{ log.workers.count }} workers</small>
</div>
<span class="badge bg-light text-dark border">{{ log.supervisor.username }}</span>
</div>
</div>
{% empty %}
<div class="p-4 text-center text-muted">
No recent activity.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Manage Resources -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Manage Resources</h6>
</div>
<div class="card-body p-0">
<ul class="nav nav-tabs px-3 pt-3" id="resourceTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="workers-tab" data-bs-toggle="tab" data-bs-target="#workers" type="button" role="tab" aria-selected="true">Workers</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="projects-tab" data-bs-toggle="tab" data-bs-target="#projects" type="button" role="tab" aria-selected="false">Projects</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="teams-tab" data-bs-toggle="tab" data-bs-target="#teams" type="button" role="tab" aria-selected="false">Teams</button>
</li>
</ul>
<div class="tab-content" id="resourceTabsContent">
<!-- Workers Tab -->
<div class="tab-pane fade show active" id="workers" role="tabpanel">
<ul class="list-group list-group-flush" style="max-height: 300px; overflow-y: auto;">
{% for item in workers %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ item.name }}
<div class="form-check form-switch">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="worker" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</li>
{% empty %}
<li class="list-group-item text-muted">No workers found.</li>
{% endfor %}
</ul>
</div>
<!-- Projects Tab -->
<div class="tab-pane fade" id="projects" role="tabpanel">
<ul class="list-group list-group-flush" style="max-height: 300px; overflow-y: auto;">
{% for item in projects %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ item.name }}
<div class="form-check form-switch">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="project" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</li>
{% empty %}
<li class="list-group-item text-muted">No projects found.</li>
{% endfor %}
</ul>
</div>
<!-- Teams Tab -->
<div class="tab-pane fade" id="teams" role="tabpanel">
<ul class="list-group list-group-flush" style="max-height: 300px; overflow-y: auto;">
{% for item in teams %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ item.name }}
<div class="form-check form-switch">
<input class="form-check-input toggle-active" type="checkbox" role="switch" data-type="team" data-id="{{ item.id }}" {% if item.active %}checked{% endif %}>
</div>
</li>
{% empty %}
<li class="list-group-item text-muted">No teams found.</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<!-- Supervisor View -->
<div class="row mb-4 position-relative">
<div class="col-md-6 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">This Week Summary</h6>
</div>
<div class="card-body text-center d-flex flex-column justify-content-center">
<div class="h1 mb-0 font-weight-bold text-primary">{{ this_week_logs }}</div>
<div class="text-muted">Work Logs Created This Week</div>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Recent Activity</h6>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for log in recent_activity %}
<div class="list-group-item px-4 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">{{ log.project.name }}</h6>
<small class="text-muted">{{ log.date }} &middot; {{ log.workers.count }} workers</small>
</div>
</div>
</div>
{% empty %}
<div class="p-4 text-center text-muted">
No recent activity.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const toggleSwitches = document.querySelectorAll('.toggle-active');
toggleSwitches.forEach(switchEl => {
switchEl.addEventListener('change', function() {
const type = this.getAttribute('data-type');
const id = this.getAttribute('data-id');
const isChecked = this.checked;
fetch(`/toggle/${type}/${id}/`, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.status !== 'success') {
// Revert if failed
this.checked = !isChecked;
alert('Error updating status.');
}
})
.catch(error => {
// Revert on error
this.checked = !isChecked;
alert('Error updating status.');
});
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Work History | FoxFitt{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0" style="color: #0f172a; font-family: 'Poppins', sans-serif;">Work History</h1>
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm shadow-sm">
<i class="fas fa-arrow-left fa-sm me-1"></i> Back
</a>
</div>
<div class="card shadow-sm border-0">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th scope="col" class="ps-4">Date</th>
<th scope="col">Project</th>
<th scope="col">Team</th>
<th scope="col">Workers</th>
<th scope="col">Supervisor</th>
<th scope="col">Overtime</th>
<th scope="col" class="pe-4">Notes</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td class="ps-4 align-middle">{{ log.date }}</td>
<td class="align-middle"><strong>{{ log.project.name }}</strong></td>
<td class="align-middle">{{ log.team.name|default:"-" }}</td>
<td class="align-middle">
<span class="badge bg-secondary">{{ log.workers.count }}</span>
</td>
<td class="align-middle">{{ log.supervisor.username|default:"-" }}</td>
<td class="align-middle">
{% if log.overtime_amount > 0 %}
<span class="badge bg-warning text-dark">{{ log.get_overtime_amount_display }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="pe-4 align-middle text-muted small">
{{ log.notes|truncatechars:30 }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">No work history found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -4,4 +4,6 @@ from . import views
urlpatterns = [
path('', views.index, name='home'),
path('attendance/log/', views.attendance_log, name='attendance_log'),
]
path('history/', views.work_history, name='work_history'),
path('toggle/<str:model_name>/<int:item_id>/', views.toggle_active, name='toggle_active'),
]

View File

@ -1,23 +1,95 @@
from django.shortcuts import render, redirect
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from .models import Worker, Project, WorkLog, Team
from django.db.models import Sum
from decimal import Decimal
from .models import Worker, Project, WorkLog, Team, PayrollRecord, Loan, PayrollAdjustment
from .forms import AttendanceLogForm
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpResponseForbidden
def is_admin(user):
return user.is_staff or user.is_superuser
def is_supervisor(user):
return user.supervised_teams.exists() or user.assigned_projects.exists() or user.groups.filter(name='Work Logger').exists()
def is_staff_or_supervisor(user):
return is_admin(user) or is_supervisor(user)
# Home view for the dashboard
@login_required
def index(request):
total_workers = Worker.objects.filter(active=True).count()
total_projects = Project.objects.filter(active=True).count()
today_attendance = WorkLog.objects.filter(date=timezone.now().date()).count()
user = request.user
context = {
'total_workers': total_workers,
'total_projects': total_projects,
'today_attendance': today_attendance,
}
return render(request, 'core/index.html', context)
if is_admin(user):
# Calculate total value of unpaid work and break it down by project
unpaid_worklogs = WorkLog.objects.filter(payroll_records__isnull=True).prefetch_related('workers', 'project')
outstanding_payments = Decimal('0.00')
outstanding_by_project = {}
for wl in unpaid_worklogs:
project_name = wl.project.name
if project_name not in outstanding_by_project:
outstanding_by_project[project_name] = Decimal('0.00')
for worker in wl.workers.all():
cost = worker.daily_rate
outstanding_payments += cost
outstanding_by_project[project_name] += cost
# Include unpaid payroll adjustments in the outstanding calculations
unpaid_adjustments = PayrollAdjustment.objects.filter(payroll_record__isnull=True)
for adj in unpaid_adjustments:
outstanding_payments += adj.amount
project_name = adj.project.name if adj.project else 'General'
if project_name not in outstanding_by_project:
outstanding_by_project[project_name] = Decimal('0.00')
outstanding_by_project[project_name] += adj.amount
# Sum the total amount paid out over the last 60 days
sixty_days_ago = timezone.now().date() - timezone.timedelta(days=60)
paid_this_month = PayrollRecord.objects.filter(date__gte=sixty_days_ago).aggregate(total=Sum('amount_paid'))['total'] or Decimal('0.00')
# Tally the count and total balance of active loans
active_loans_qs = Loan.objects.filter(active=True)
active_loans_count = active_loans_qs.count()
active_loans_balance = active_loans_qs.aggregate(total=Sum('remaining_balance'))['total'] or Decimal('0.00')
start_of_week = timezone.now().date() - timezone.timedelta(days=timezone.now().date().weekday())
this_week_logs = WorkLog.objects.filter(date__gte=start_of_week).count()
recent_activity = WorkLog.objects.all().order_by('-date', '-id')[:5]
# Get all workers, projects, and teams for the Manage Resources tab
workers = Worker.objects.all().order_by('name')
projects = Project.objects.all().order_by('name')
teams = Team.objects.all().order_by('name')
context = {
'is_admin': True,
'outstanding_payments': outstanding_payments,
'paid_this_month': paid_this_month,
'active_loans_count': active_loans_count,
'active_loans_balance': active_loans_balance,
'outstanding_by_project': outstanding_by_project,
'this_week_logs': this_week_logs,
'recent_activity': recent_activity,
'workers': workers,
'projects': projects,
'teams': teams,
}
return render(request, 'core/index.html', context)
else:
start_of_week = timezone.now().date() - timezone.timedelta(days=timezone.now().date().weekday())
this_week_logs = WorkLog.objects.filter(date__gte=start_of_week, supervisor=user).count()
recent_activity = WorkLog.objects.filter(supervisor=user).order_by('-date', '-id')[:5]
context = {
'is_admin': False,
'this_week_logs': this_week_logs,
'recent_activity': recent_activity,
}
return render(request, 'core/index.html', context)
# View for logging attendance
@login_required
@ -29,6 +101,42 @@ def attendance_log(request):
messages.success(request, 'Attendance logged successfully!')
return redirect('home')
else:
form = AttendanceLogForm(initial={'date': timezone.now().date()})
form = AttendanceLogForm(initial={'date': timezone.now().date(), 'supervisor': request.user})
return render(request, 'core/attendance_log.html', {'form': form})
# Work history view
@login_required
def work_history(request):
if is_admin(request.user):
logs = WorkLog.objects.all().order_by('-date', '-id')
else:
logs = WorkLog.objects.filter(supervisor=request.user).order_by('-date', '-id')
return render(request, 'core/work_history.html', {'logs': logs})
# API view to toggle resource active status
@login_required
def toggle_active(request, model_name, item_id):
if request.method != 'POST':
return HttpResponseForbidden("Only POST requests are allowed.")
if not is_admin(request.user):
return HttpResponseForbidden("Not authorized.")
model_map = {
'worker': Worker,
'project': Project,
'team': Team
}
if model_name not in model_map:
return JsonResponse({'error': 'Invalid model'}, status=400)
model = model_map[model_name]
try:
item = model.objects.get(id=item_id)
item.active = not item.active
item.save()
return JsonResponse({'status': 'success', 'active': item.active})
except model.DoesNotExist:
return JsonResponse({'error': 'Item not found'}, status=404)