This commit is contained in:
Flatlogic Bot 2026-02-03 17:24:51 +00:00
parent 5bf8837f1c
commit bc608d7d1e
18 changed files with 397 additions and 26 deletions

Binary file not shown.

View File

@ -1,23 +1,27 @@
from django.contrib import admin
from .models import Worker, Project, Team, WorkLog
from .models import Worker, Project, Team, WorkLog, UserProfile
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'pin', 'is_admin')
@admin.register(Worker)
class WorkerAdmin(admin.ModelAdmin):
list_display = ('name', 'id_no', 'phone_no', 'monthly_salary', 'day_rate')
search_fields = ('name', 'id_no', 'phone_no')
list_display = ('name', 'id_no', 'phone_no', 'monthly_salary')
search_fields = ('name', 'id_no')
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
list_display = ('name', 'created_at')
search_fields = ('name',)
filter_horizontal = ('supervisors',)
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'created_at')
list_display = ('name', 'supervisor', 'created_at')
filter_horizontal = ('workers',)
@admin.register(WorkLog)
class WorkLogAdmin(admin.ModelAdmin):
list_display = ('date', 'project')
list_filter = ('date', 'project')
filter_horizontal = ('workers',)
list_display = ('date', 'project', 'supervisor')
list_filter = ('date', 'project', 'supervisor')
filter_horizontal = ('workers',)

42
core/forms.py Normal file
View File

@ -0,0 +1,42 @@
from django import forms
from .models import WorkLog, Project, Worker, Team
class WorkLogForm(forms.ModelForm):
team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False, empty_label="Select Team", widget=forms.Select(attrs={'class': 'form-control'}))
class Meta:
model = WorkLog
fields = ['date', 'project', 'workers', 'notes']
widgets = {
'date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'project': forms.Select(attrs={'class': 'form-control'}),
'workers': forms.CheckboxSelectMultiple(),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
# Base querysets with active filter
projects_qs = Project.objects.filter(is_active=True)
workers_qs = Worker.objects.filter(is_active=True)
teams_qs = Team.objects.filter(is_active=True)
if user and not user.is_superuser:
# Filter projects and workers based on user assignment
self.fields['project'].queryset = projects_qs.filter(supervisors=user)
# For workers, we might want to show workers from teams supervised by the user
# OR just all active workers if that's the business rule.
# The previous code filtered workers by managed teams. Let's keep that logic but respecting is_active.
managed_teams = user.managed_teams.all()
worker_ids = managed_teams.values_list('workers__id', flat=True).distinct()
self.fields['workers'].queryset = workers_qs.filter(id__in=worker_ids)
# Filter teams
self.fields['team'].queryset = teams_qs.filter(supervisor=user)
else:
self.fields['project'].queryset = projects_qs
self.fields['workers'].queryset = workers_qs
self.fields['team'].queryset = teams_qs

View File

@ -0,0 +1,40 @@
# Generated by Django 5.2.7 on 2026-02-03 15:59
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='project',
name='supervisors',
field=models.ManyToManyField(blank=True, related_name='assigned_projects', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='team',
name='supervisor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_teams', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='worklog',
name='supervisor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('pin', models.CharField(help_text='4-digit PIN for login', max_length=4)),
('is_admin', models.BooleanField(default=False)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-03 16:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_project_supervisors_team_supervisor_and_more'),
]
operations = [
migrations.AddField(
model_name='project',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='team',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='worker',
name='is_active',
field=models.BooleanField(default=True),
),
]

View File

@ -1,11 +1,22 @@
from django.db import models
from django.core.validators import MinValueValidator
from decimal import Decimal
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
pin = models.CharField(max_length=4, help_text="4-digit PIN for login")
is_admin = models.BooleanField(default=False)
def __str__(self):
return f"{self.user.username}'s profile"
class Project(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
supervisors = models.ManyToManyField(User, related_name='assigned_projects', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
@ -16,6 +27,7 @@ class Worker(models.Model):
phone_no = models.CharField(max_length=20, verbose_name="Phone Number")
monthly_salary = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))])
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
@property
def day_rate(self):
@ -27,7 +39,9 @@ class Worker(models.Model):
class Team(models.Model):
name = models.CharField(max_length=200)
workers = models.ManyToManyField(Worker, related_name='teams')
supervisor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_teams')
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
@ -36,6 +50,7 @@ class WorkLog(models.Model):
date = models.DateField()
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='logs')
workers = models.ManyToManyField(Worker, related_name='work_logs')
supervisor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
notes = models.TextField(blank=True)
def __str__(self):

View File

@ -10,28 +10,45 @@
{% load static %}
<!-- Bootstrap 5 CDN -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #1e293b;">
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #0f172a;">
<div class="container">
<a class="navbar-brand heading-font" href="/">LabourFlow</a>
<a class="navbar-brand heading-font fw-bold" href="{% url 'home' %}">
<span style="color: #10b981;">Labour</span>Flow
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link" href="/">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/">Admin</a></li>
<ul class="navbar-nav ms-auto align-items-center">
<li class="nav-item"><a class="nav-link" href="{% url 'home' %}">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'log_attendance' %}">Log Work</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'work_log_list' %}">History</a></li>
<li class="nav-item ms-lg-3"><a class="btn btn-sm btn-outline-light" href="/admin/">Admin Panel</a></li>
</ul>
</div>
</div>
</nav>
{% block content %}{% endblock %}
<main>
{% block content %}{% endblock %}
</main>
<footer class="py-4 mt-5 border-top bg-light">
<div class="container text-center text-muted small">
&copy; 2026 LabourFlow Management System. All rights reserved.
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
</html>

View File

@ -8,11 +8,11 @@
<div class="container">
<div class="row align-items-center">
<div class="col-md-8">
<h1 class="display-5 mb-2">Welcome Back, Admin</h1>
<h1 class="display-5 mb-2">Welcome Back, {% if user.is_authenticated %}{{ user.username }}{% else %}Admin{% endif %}</h1>
<p class="lead opacity-75">Track your projects, workers, and payroll in one place.</p>
</div>
<div class="col-md-4 text-md-end">
<a href="/admin/core/worklog/add/" class="btn btn-accent shadow-sm">
<a href="{% url 'log_attendance' %}" class="btn btn-accent shadow-sm">
+ Log Daily Work
</a>
</div>
@ -67,7 +67,10 @@
<div class="row">
<div class="col-lg-8">
<div class="card p-4 mb-4">
<h3 class="mb-4">Recent Daily Logs</h3>
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="mb-0">Recent Daily Logs</h3>
<a href="{% url 'work_log_list' %}" class="btn btn-sm btn-link text-decoration-none">View All</a>
</div>
{% if recent_logs %}
<div class="table-responsive">
<table class="table align-middle">
@ -97,7 +100,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
</div>
<p class="text-muted">No recent work logs found.</p>
<a href="/admin/core/worklog/add/" class="btn btn-sm btn-outline-primary">Create First Log</a>
<a href="{% url 'log_attendance' %}" class="btn btn-sm btn-outline-primary">Create First Log</a>
</div>
{% endif %}
</div>
@ -115,7 +118,7 @@
<a class="sidebar-link" href="/admin/core/team/">
<span class="me-2">👥</span> Manage Teams
</a>
<a class="sidebar-link" href="/admin/core/worklog/">
<a class="sidebar-link" href="{% url 'work_log_list' %}">
<span class="me-2">📅</span> View All Logs
</a>
<hr>
@ -127,4 +130,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,119 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Log Daily Attendance | LabourFlow{% endblock %}
{% block content %}
<div class="dashboard-header">
<div class="container">
<h1 class="display-5 mb-2">Log Daily Attendance</h1>
<p class="lead opacity-75">Record work for projects and labourers.</p>
</div>
</div>
<div class="container mb-5 mt-n4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card p-4 shadow-sm">
<form method="post">
{% csrf_token %}
<div class="row mb-4">
<div class="col-md-4">
<label class="form-label fw-bold">Date</label>
{{ form.date }}
{% if form.date.errors %}
<div class="text-danger mt-1 small">{{ form.date.errors }}</div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label fw-bold">Project</label>
{{ form.project }}
{% if form.project.errors %}
<div class="text-danger mt-1 small">{{ form.project.errors }}</div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label fw-bold">Team (Optional)</label>
{{ form.team }}
{% if form.team.errors %}
<div class="text-danger mt-1 small">{{ form.team.errors }}</div>
{% endif %}
</div>
</div>
<div class="mb-4">
<label class="form-label fw-bold d-block mb-3">Select Labourers</label>
<div class="row">
{% for checkbox in form.workers %}
<div class="col-md-6 col-lg-4 mb-2">
<div class="form-check p-3 border rounded-3 hover-shadow transition-all">
{{ checkbox.tag }}
<label class="form-check-label ms-2" for="{{ checkbox.id_for_label }}">
{{ checkbox.choice_label }}
</label>
</div>
</div>
{% endfor %}
</div>
{% if form.workers.errors %}
<div class="text-danger mt-1 small">{{ form.workers.errors }}</div>
{% endif %}
</div>
<div class="mb-4">
<label class="form-label fw-bold">Notes / Comments</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="text-danger mt-1 small">{{ form.notes.errors }}</div>
{% endif %}
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'home' %}" class="btn btn-light px-4">Cancel</a>
<button type="submit" class="btn btn-primary px-5">Save Work Log</button>
</div>
</form>
</div>
</div>
</div>
</div>
<style>
.hover-shadow:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
border-color: #0d6efd !important;
}
.transition-all {
transition: all 0.2s ease-in-out;
}
.mt-n4 {
margin-top: -3rem !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const teamSelect = document.getElementById('{{ form.team.id_for_label }}');
const teamWorkersMap = {{ team_workers_json|safe }};
if (teamSelect) {
teamSelect.addEventListener('change', function() {
const teamId = this.value;
if (teamId && teamWorkersMap[teamId]) {
const workerIds = teamWorkersMap[teamId];
// Select workers belonging to the team
workerIds.forEach(function(id) {
// Find the checkbox for this worker ID
// Django form checkboxes usually have name 'workers' and value equal to ID
const checkbox = document.querySelector(`input[name="workers"][value="${id}"]`);
if (checkbox) {
checkbox.checked = true;
}
});
}
});
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Work Log History | LabourFlow{% endblock %}
{% block content %}
<div class="dashboard-header">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="display-5 mb-2">Work Log History</h1>
<p class="lead opacity-75">View and filter historical daily work logs.</p>
</div>
<a href="{% url 'log_attendance' %}" class="btn btn-accent shadow-sm">
+ New Entry
</a>
</div>
</div>
</div>
<div class="container mb-5 mt-n4">
<div class="card p-4 shadow-sm">
{% if logs %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Project</th>
<th>Supervisor</th>
<th>Labourers</th>
<th>Notes</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.date }}</td>
<td><strong>{{ log.project.name }}</strong></td>
<td>{{ log.supervisor.username|default:"System" }}</td>
<td>
<span class="badge bg-primary bg-opacity-10 text-primary">
{{ log.workers.count }} Workers
</span>
</td>
<td><small class="text-muted">{{ log.notes|truncatechars:30 }}</small></td>
<td>
<a href="/admin/core/worklog/{{ log.id }}/change/" class="btn btn-sm btn-outline-secondary">Edit</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<p class="text-muted">No work logs recorded yet.</p>
<a href="{% url 'log_attendance' %}" class="btn btn-primary">Log First Attendance</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,7 +1,8 @@
from django.urls import path
from .views import home
from .views import home, log_attendance, work_log_list
urlpatterns = [
path("", home, name="home"),
]
path("log-attendance/", log_attendance, name="log_attendance"),
path("work-logs/", work_log_list, name="work_log_list"),
]

View File

@ -1,8 +1,11 @@
import os
import platform
from django.shortcuts import render
import json
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from django.contrib.auth.decorators import login_required
from .models import Worker, Project, Team, WorkLog
from .forms import WorkLogForm
def home(request):
"""Render the landing screen with dashboard stats."""
@ -18,4 +21,39 @@ def home(request):
"recent_logs": recent_logs,
"current_time": timezone.now(),
}
return render(request, "core/index.html", context)
return render(request, "core/index.html", context)
def log_attendance(request):
if request.method == 'POST':
form = WorkLogForm(request.POST, user=request.user)
if form.is_valid():
work_log = form.save(commit=False)
if request.user.is_authenticated:
work_log.supervisor = request.user
work_log.save()
form.save_m2m()
return redirect('home')
else:
form = WorkLogForm(user=request.user if request.user.is_authenticated else None)
# Build team workers map for frontend JS
teams_qs = Team.objects.filter(is_active=True)
if request.user.is_authenticated and not request.user.is_superuser:
teams_qs = teams_qs.filter(supervisor=request.user)
team_workers_map = {}
for team in teams_qs:
# Get active workers for the team
active_workers = team.workers.filter(is_active=True).values_list('id', flat=True)
team_workers_map[team.id] = list(active_workers)
context = {
'form': form,
'team_workers_json': json.dumps(team_workers_map)
}
return render(request, 'core/log_attendance.html', context)
def work_log_list(request):
logs = WorkLog.objects.all().order_by('-date')
return render(request, 'core/work_log_list.html', {'logs': logs})