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 title %}Dashboard | FoxFitt{% endblock %}
{% block content %} {% block content %}
<div class="container py-4"> <!-- Gradient Header -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="dashboard-header mb-5 rounded shadow-sm p-4 d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0 text-gray-800">Welcome back, {{ user.first_name|default:user.username }}!</h1> <div>
<a href="{% url 'attendance_log' %}" class="btn text-white shadow-sm" style="background-color: #10b981;"> <h1 class="h3 mb-0 text-white" style="font-family: 'Poppins', sans-serif;">Dashboard</h1>
<i class="fas fa-plus fa-sm text-white-50 me-1"></i> Log Work <p class="text-white-50 mb-0">Welcome back, {{ user.first_name|default:user.username }}!</p>
</a>
</div> </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="container py-2" style="margin-top: -3rem;">
<div class="row g-4 mb-4"> {% if is_admin %}
<!-- Active Workers Card --> <!-- Admin View -->
<div class="col-xl-4 col-md-6"> <div class="row g-4 mb-4 position-relative">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #10b981 !important;"> <!-- 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="card-body">
<div class="row no-gutters align-items-center"> <div class="row no-gutters align-items-center">
<div class="col me-2"> <div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;"> <div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #10b981;">
Active Workers</div> Paid This Month</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ total_workers|default:"0" }}</div> <div class="h5 mb-0 font-weight-bold text-gray-800">${{ paid_this_month|floatformat:2 }}</div>
</div> </div>
<div class="col-auto"> <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> </div>
</div> </div>
</div> </div>
<!-- Projects Card --> <!-- Active Loans Card -->
<div class="col-xl-4 col-md-6"> <div class="col-xl-3 col-md-6">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #3b82f6 !important;"> <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;">
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;">
<div class="card-body"> <div class="card-body">
<div class="row no-gutters align-items-center"> <div class="row no-gutters align-items-center">
<div class="col me-2"> <div class="col me-2">
<div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;"> <div class="text-xs font-weight-bold text-uppercase mb-1" style="color: #f59e0b;">
Today's Logs</div> Active Loans ({{ active_loans_count }})</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ today_attendance|default:"0" }}</div> <div class="h5 mb-0 font-weight-bold text-gray-800">${{ active_loans_balance|floatformat:2 }}</div>
</div> </div>
<div class="col-auto"> <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> </div>
</div> </div>
@ -69,18 +102,223 @@
</div> </div>
</div> </div>
<!-- Content Row --> <!-- Quick Actions and This Week -->
<div class="row"> <div class="row mb-4">
<div class="col-lg-12 mb-4"> <!-- This Week -->
<div class="card shadow-sm border-0 mb-4"> <div class="col-lg-4 mb-4 mb-lg-0">
<div class="card-header py-3 bg-white d-flex flex-row align-items-center justify-content-between"> <div class="card shadow-sm border-0 h-100">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">Recent Activity</h6> <div class="card-header py-3 bg-white">
<h6 class="m-0 font-weight-bold" style="color: #0f172a;">This Week Summary</h6>
</div> </div>
<div class="card-body"> <div class="card-body text-center d-flex flex-column justify-content-center">
<p class="text-muted mb-0">No recent activity to show yet. Start by logging today's attendance!</p> <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> </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> </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 = [ urlpatterns = [
path('', views.index, name='home'), path('', views.index, name='home'),
path('attendance/log/', views.attendance_log, name='attendance_log'), 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 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 .forms import AttendanceLogForm
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required 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 # Home view for the dashboard
@login_required @login_required
def index(request): def index(request):
total_workers = Worker.objects.filter(active=True).count() user = request.user
total_projects = Project.objects.filter(active=True).count()
today_attendance = WorkLog.objects.filter(date=timezone.now().date()).count()
context = { if is_admin(user):
'total_workers': total_workers, # Calculate total value of unpaid work and break it down by project
'total_projects': total_projects, unpaid_worklogs = WorkLog.objects.filter(payroll_records__isnull=True).prefetch_related('workers', 'project')
'today_attendance': today_attendance, outstanding_payments = Decimal('0.00')
} outstanding_by_project = {}
return render(request, 'core/index.html', context)
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 # View for logging attendance
@login_required @login_required
@ -29,6 +101,42 @@ def attendance_log(request):
messages.success(request, 'Attendance logged successfully!') messages.success(request, 'Attendance logged successfully!')
return redirect('home') return redirect('home')
else: 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}) 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)