v2
This commit is contained in:
parent
5bf8837f1c
commit
bc608d7d1e
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,23 +1,27 @@
|
|||||||
from django.contrib import admin
|
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)
|
@admin.register(Worker)
|
||||||
class WorkerAdmin(admin.ModelAdmin):
|
class WorkerAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'id_no', 'phone_no', 'monthly_salary', 'day_rate')
|
list_display = ('name', 'id_no', 'phone_no', 'monthly_salary')
|
||||||
search_fields = ('name', 'id_no', 'phone_no')
|
search_fields = ('name', 'id_no')
|
||||||
|
|
||||||
@admin.register(Project)
|
@admin.register(Project)
|
||||||
class ProjectAdmin(admin.ModelAdmin):
|
class ProjectAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'created_at')
|
list_display = ('name', 'created_at')
|
||||||
search_fields = ('name',)
|
filter_horizontal = ('supervisors',)
|
||||||
|
|
||||||
@admin.register(Team)
|
@admin.register(Team)
|
||||||
class TeamAdmin(admin.ModelAdmin):
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'created_at')
|
list_display = ('name', 'supervisor', 'created_at')
|
||||||
filter_horizontal = ('workers',)
|
filter_horizontal = ('workers',)
|
||||||
|
|
||||||
@admin.register(WorkLog)
|
@admin.register(WorkLog)
|
||||||
class WorkLogAdmin(admin.ModelAdmin):
|
class WorkLogAdmin(admin.ModelAdmin):
|
||||||
list_display = ('date', 'project')
|
list_display = ('date', 'project', 'supervisor')
|
||||||
list_filter = ('date', 'project')
|
list_filter = ('date', 'project', 'supervisor')
|
||||||
filter_horizontal = ('workers',)
|
filter_horizontal = ('workers',)
|
||||||
42
core/forms.py
Normal file
42
core/forms.py
Normal 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
|
||||||
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
@ -1,11 +1,22 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from decimal import Decimal
|
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):
|
class Project(models.Model):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
|
supervisors = models.ManyToManyField(User, related_name='assigned_projects', blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -16,6 +27,7 @@ class Worker(models.Model):
|
|||||||
phone_no = models.CharField(max_length=20, verbose_name="Phone Number")
|
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'))])
|
monthly_salary = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))])
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def day_rate(self):
|
def day_rate(self):
|
||||||
@ -27,7 +39,9 @@ class Worker(models.Model):
|
|||||||
class Team(models.Model):
|
class Team(models.Model):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
workers = models.ManyToManyField(Worker, related_name='teams')
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -36,6 +50,7 @@ class WorkLog(models.Model):
|
|||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='logs')
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='logs')
|
||||||
workers = models.ManyToManyField(Worker, related_name='work_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)
|
notes = models.TextField(blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -10,27 +10,44 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<!-- Bootstrap 5 CDN -->
|
<!-- Bootstrap 5 CDN -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<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 -->
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Inter', sans-serif; }
|
||||||
|
</style>
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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">
|
<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">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto align-items-center">
|
||||||
<li class="nav-item"><a class="nav-link" href="/">Dashboard</a></li>
|
<li class="nav-item"><a class="nav-link" href="{% url 'home' %}">Dashboard</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/admin/">Admin</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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">
|
||||||
|
© 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>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -8,11 +8,11 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-md-8">
|
<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>
|
<p class="lead opacity-75">Track your projects, workers, and payroll in one place.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-md-end">
|
<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
|
+ Log Daily Work
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -67,7 +67,10 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card p-4 mb-4">
|
<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 %}
|
{% if recent_logs %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table align-middle">
|
<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>
|
<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>
|
</div>
|
||||||
<p class="text-muted">No recent work logs found.</p>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -115,7 +118,7 @@
|
|||||||
<a class="sidebar-link" href="/admin/core/team/">
|
<a class="sidebar-link" href="/admin/core/team/">
|
||||||
<span class="me-2">👥</span> Manage Teams
|
<span class="me-2">👥</span> Manage Teams
|
||||||
</a>
|
</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
|
<span class="me-2">📅</span> View All Logs
|
||||||
</a>
|
</a>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
119
core/templates/core/log_attendance.html
Normal file
119
core/templates/core/log_attendance.html
Normal 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 %}
|
||||||
64
core/templates/core/work_log_list.html
Normal file
64
core/templates/core/work_log_list.html
Normal 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 %}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from .views import home, log_attendance, work_log_list
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
path("log-attendance/", log_attendance, name="log_attendance"),
|
||||||
|
path("work-logs/", work_log_list, name="work_log_list"),
|
||||||
]
|
]
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import platform
|
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.utils import timezone
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from .models import Worker, Project, Team, WorkLog
|
from .models import Worker, Project, Team, WorkLog
|
||||||
|
from .forms import WorkLogForm
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with dashboard stats."""
|
"""Render the landing screen with dashboard stats."""
|
||||||
@ -19,3 +22,38 @@ def home(request):
|
|||||||
"current_time": timezone.now(),
|
"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})
|
||||||
Loading…
x
Reference in New Issue
Block a user