This commit is contained in:
Flatlogic Bot 2026-02-22 12:26:15 +00:00
parent d3fb8046d5
commit d10151cf40
21 changed files with 712 additions and 209 deletions

View File

@ -135,7 +135,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Africa/Johannesburg'
USE_I18N = True
@ -151,10 +151,12 @@ STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
BASE_DIR / 'assets',
BASE_DIR / 'node_modules',
]
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Email
EMAIL_BACKEND = os.getenv(
"EMAIL_BACKEND",

View File

@ -1,19 +1,3 @@
"""
URL configuration for config project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
from django.conf import settings
@ -25,5 +9,5 @@ urlpatterns = [
]
if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Binary file not shown.

View File

@ -1,3 +1,73 @@
from django.contrib import admin
from .models import (
UserProfile, Project, Worker, Team, WorkLog,
PayrollRecord, Loan, PayrollAdjustment,
ExpenseReceipt, ExpenseLineItem
)
# Register your models here.
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ('user',)
search_fields = ('user__username', 'user__first_name', 'user__last_name')
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
list_display = ('name', 'active')
list_filter = ('active',)
search_fields = ('name', 'description')
filter_horizontal = ('supervisors',)
@admin.register(Worker)
class WorkerAdmin(admin.ModelAdmin):
list_display = ('name', 'id_number', 'monthly_salary', 'active')
list_filter = ('active',)
search_fields = ('name', 'id_number', 'phone_number')
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'supervisor', 'active')
list_filter = ('active', 'supervisor')
search_fields = ('name',)
filter_horizontal = ('workers',)
@admin.register(WorkLog)
class WorkLogAdmin(admin.ModelAdmin):
list_display = ('date', 'project', 'supervisor', 'overtime_amount')
list_filter = ('date', 'project', 'supervisor')
search_fields = ('project__name', 'notes')
filter_horizontal = ('workers', 'priced_workers')
@admin.register(PayrollRecord)
class PayrollRecordAdmin(admin.ModelAdmin):
list_display = ('worker', 'date', 'amount_paid')
list_filter = ('date', 'worker')
search_fields = ('worker__name',)
filter_horizontal = ('work_logs',)
@admin.register(Loan)
class LoanAdmin(admin.ModelAdmin):
list_display = ('worker', 'principal_amount', 'remaining_balance', 'date', 'active')
list_filter = ('active', 'date', 'worker')
search_fields = ('worker__name', 'reason')
@admin.register(PayrollAdjustment)
class PayrollAdjustmentAdmin(admin.ModelAdmin):
list_display = ('worker', 'type', 'amount', 'date')
list_filter = ('type', 'date', 'worker')
search_fields = ('worker__name', 'description')
class ExpenseLineItemInline(admin.TabularInline):
model = ExpenseLineItem
extra = 1
@admin.register(ExpenseReceipt)
class ExpenseReceiptAdmin(admin.ModelAdmin):
list_display = ('vendor_name', 'date', 'total_amount', 'user')
list_filter = ('date', 'payment_method', 'vat_type')
search_fields = ('vendor_name', 'description')
inlines = [ExpenseLineItemInline]
@admin.register(ExpenseLineItem)
class ExpenseLineItemAdmin(admin.ModelAdmin):
list_display = ('product_name', 'amount', 'receipt')
search_fields = ('product_name', 'receipt__vendor_name')

22
core/forms.py Normal file
View File

@ -0,0 +1,22 @@
from django import forms
from .models import WorkLog, Project, Team, Worker
class AttendanceLogForm(forms.ModelForm):
class Meta:
model = WorkLog
fields = ['date', 'project', 'team', 'workers', 'supervisor', 'overtime_amount', 'notes']
widgets = {
'date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'project': forms.Select(attrs={'class': 'form-select'}),
'team': forms.Select(attrs={'class': 'form-select'}),
'workers': forms.CheckboxSelectMultiple(attrs={'class': 'form-check-input'}),
'supervisor': forms.Select(attrs={'class': 'form-select'}),
'overtime_amount': forms.Select(attrs={'class': 'form-select'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['workers'].queryset = Worker.objects.filter(active=True)
self.fields['project'].queryset = Project.objects.filter(active=True)
self.fields['team'].queryset = Team.objects.filter(active=True)

View File

@ -0,0 +1,136 @@
# Generated by Django 5.2.7 on 2026-02-22 12:17
import django.db.models.deletion
import django.utils.timezone
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Worker',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('id_number', models.CharField(max_length=50, unique=True)),
('phone_number', models.CharField(blank=True, max_length=20)),
('monthly_salary', models.DecimalField(decimal_places=2, max_digits=10)),
('photo', models.ImageField(blank=True, null=True, upload_to='workers/photos/')),
('id_document', models.FileField(blank=True, null=True, upload_to='workers/documents/')),
('employment_date', models.DateField(default=django.utils.timezone.now)),
('notes', models.TextField(blank=True)),
('active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='ExpenseReceipt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now)),
('vendor_name', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('payment_method', models.CharField(choices=[('Cash', 'Cash'), ('Card', 'Card'), ('EFT', 'EFT'), ('Other', 'Other')], max_length=20)),
('vat_type', models.CharField(choices=[('Included', 'Included'), ('Excluded', 'Excluded'), ('None', 'None')], max_length=20)),
('subtotal', models.DecimalField(decimal_places=2, max_digits=12)),
('vat_amount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)),
('total_amount', models.DecimalField(decimal_places=2, max_digits=12)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expense_receipts', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ExpenseLineItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_name', models.CharField(max_length=200)),
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
('receipt', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='line_items', to='core.expensereceipt')),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('active', models.BooleanField(default=True)),
('supervisors', models.ManyToManyField(related_name='assigned_projects', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Team',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('active', models.BooleanField(default=True)),
('supervisor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supervised_teams', to=settings.AUTH_USER_MODEL)),
('workers', models.ManyToManyField(related_name='teams', to='core.worker')),
],
),
migrations.CreateModel(
name='Loan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('principal_amount', models.DecimalField(decimal_places=2, max_digits=10)),
('remaining_balance', models.DecimalField(decimal_places=2, max_digits=10)),
('date', models.DateField(default=django.utils.timezone.now)),
('reason', models.TextField(blank=True)),
('active', models.BooleanField(default=True)),
('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loans', to='core.worker')),
],
),
migrations.CreateModel(
name='WorkLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now)),
('notes', models.TextField(blank=True)),
('overtime_amount', models.DecimalField(choices=[(Decimal('0.00'), 'None'), (Decimal('0.25'), '1/4 Day'), (Decimal('0.50'), '1/2 Day'), (Decimal('0.75'), '3/4 Day'), (Decimal('1.00'), 'Full Day')], decimal_places=2, default=Decimal('0.00'), max_digits=3)),
('priced_workers', models.ManyToManyField(blank=True, related_name='priced_overtime_logs', to='core.worker')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='work_logs', to='core.project')),
('supervisor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='work_logs_created', to=settings.AUTH_USER_MODEL)),
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='work_logs', to='core.team')),
('workers', models.ManyToManyField(related_name='work_logs', to='core.worker')),
],
),
migrations.CreateModel(
name='PayrollRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now)),
('amount_paid', models.DecimalField(decimal_places=2, max_digits=10)),
('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payroll_records', to='core.worker')),
('work_logs', models.ManyToManyField(related_name='payroll_records', to='core.worklog')),
],
),
migrations.CreateModel(
name='PayrollAdjustment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('date', models.DateField(default=django.utils.timezone.now)),
('description', models.TextField(blank=True)),
('type', models.CharField(choices=[('Bonus', 'Bonus'), ('Overtime', 'Overtime'), ('Deduction', 'Deduction'), ('Loan Repayment', 'Loan Repayment'), ('New Loan', 'New Loan'), ('Advance Payment', 'Advance Payment')], max_length=50)),
('loan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='repayments', to='core.loan')),
('payroll_record', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments', to='core.payrollrecord')),
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments_by_project', to='core.project')),
('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='adjustments', to='core.worker')),
('work_log', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments_by_work_log', to='core.worklog')),
],
),
]

View File

@ -1,3 +1,163 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from decimal import Decimal
from django.db.models.signals import post_save
from django.dispatch import receiver
# Create your models here.
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
# Add any extra profile fields if needed in the future
def __str__(self):
return self.user.username
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.get_or_create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
if hasattr(instance, 'profile'):
instance.profile.save()
class Project(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
supervisors = models.ManyToManyField(User, related_name='assigned_projects')
active = models.BooleanField(default=True)
def __str__(self):
return self.name
class Worker(models.Model):
name = models.CharField(max_length=200)
id_number = models.CharField(max_length=50, unique=True)
phone_number = models.CharField(max_length=20, blank=True)
monthly_salary = models.DecimalField(max_digits=10, decimal_places=2)
photo = models.ImageField(upload_to='workers/photos/', blank=True, null=True)
id_document = models.FileField(upload_to='workers/documents/', blank=True, null=True)
employment_date = models.DateField(default=timezone.now)
notes = models.TextField(blank=True)
active = models.BooleanField(default=True)
@property
def daily_rate(self):
# monthly salary divided by 20 working days
return (self.monthly_salary / Decimal('20.00')).quantize(Decimal('0.01'))
def __str__(self):
return self.name
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, related_name='supervised_teams')
active = models.BooleanField(default=True)
def __str__(self):
return self.name
class WorkLog(models.Model):
OVERTIME_CHOICES = [
(Decimal('0.00'), 'None'),
(Decimal('0.25'), '1/4 Day'),
(Decimal('0.50'), '1/2 Day'),
(Decimal('0.75'), '3/4 Day'),
(Decimal('1.00'), 'Full Day'),
]
date = models.DateField(default=timezone.now)
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='work_logs')
team = models.ForeignKey(Team, on_delete=models.SET_NULL, null=True, blank=True, related_name='work_logs')
workers = models.ManyToManyField(Worker, related_name='work_logs')
supervisor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='work_logs_created')
notes = models.TextField(blank=True)
overtime_amount = models.DecimalField(max_digits=3, decimal_places=2, choices=OVERTIME_CHOICES, default=Decimal('0.00'))
priced_workers = models.ManyToManyField(Worker, related_name='priced_overtime_logs', blank=True)
def __str__(self):
return f"{self.date} - {self.project.name}"
class PayrollRecord(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='payroll_records')
date = models.DateField(default=timezone.now)
amount_paid = models.DecimalField(max_digits=10, decimal_places=2)
work_logs = models.ManyToManyField(WorkLog, related_name='payroll_records')
def __str__(self):
return f"{self.worker.name} - {self.date}"
class Loan(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='loans')
principal_amount = models.DecimalField(max_digits=10, decimal_places=2)
remaining_balance = models.DecimalField(max_digits=10, decimal_places=2)
date = models.DateField(default=timezone.now)
reason = models.TextField(blank=True)
active = models.BooleanField(default=True)
def save(self, *args, **kwargs):
if not self.pk:
self.remaining_balance = self.principal_amount
super().save(*args, **kwargs)
def __str__(self):
return f"{self.worker.name} - Loan - {self.date}"
class PayrollAdjustment(models.Model):
TYPE_CHOICES = [
('Bonus', 'Bonus'),
('Overtime', 'Overtime'),
('Deduction', 'Deduction'),
('Loan Repayment', 'Loan Repayment'),
('New Loan', 'New Loan'),
('Advance Payment', 'Advance Payment'),
]
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='adjustments')
payroll_record = models.ForeignKey(PayrollRecord, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments')
loan = models.ForeignKey(Loan, on_delete=models.SET_NULL, null=True, blank=True, related_name='repayments')
work_log = models.ForeignKey(WorkLog, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments_by_work_log')
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments_by_project')
amount = models.DecimalField(max_digits=10, decimal_places=2)
date = models.DateField(default=timezone.now)
description = models.TextField(blank=True)
type = models.CharField(max_length=50, choices=TYPE_CHOICES)
def __str__(self):
return f"{self.worker.name} - {self.type} - {self.amount}"
class ExpenseReceipt(models.Model):
METHOD_CHOICES = [
('Cash', 'Cash'),
('Card', 'Card'),
('EFT', 'EFT'),
('Other', 'Other'),
]
VAT_CHOICES = [
('Included', 'Included'),
('Excluded', 'Excluded'),
('None', 'None'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense_receipts')
date = models.DateField(default=timezone.now)
vendor_name = models.CharField(max_length=200)
description = models.TextField(blank=True)
payment_method = models.CharField(max_length=20, choices=METHOD_CHOICES)
vat_type = models.CharField(max_length=20, choices=VAT_CHOICES)
subtotal = models.DecimalField(max_digits=12, decimal_places=2)
vat_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
total_amount = models.DecimalField(max_digits=12, decimal_places=2)
def __str__(self):
return f"{self.vendor_name} - {self.date}"
class ExpenseLineItem(models.Model):
receipt = models.ForeignKey(ExpenseReceipt, on_delete=models.CASCADE, related_name='line_items')
product_name = models.CharField(max_length=200)
amount = models.DecimalField(max_digits=12, decimal_places=2)
def __str__(self):
return self.product_name

View File

@ -1,25 +1,68 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title>
{% if project_description %}
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
{% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
{% load static %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Fox Fitt Construction{% endblock %}</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Open+Sans:wght@400;600&display=swap" rel="stylesheet">
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container">
<a class="navbar-brand fw-bold" href="{% url 'index' %}">FOX FITT</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="{% url 'index' %}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'attendance_log' %}">Log Attendance</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/">Admin Portal</a>
</li>
</ul>
</div>
</div>
</nav>
{% if messages %}
<div class="container mt-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
<main>
{% block content %}{% endblock %}
</main>
<footer class="footer mt-auto">
<div class="container text-center">
<p>&copy; 2026 Fox Fitt Construction - Payroll Management</p>
</div>
</footer>
<!-- Bootstrap 5 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,82 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<section class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-primary text-white text-center py-3">
<h2 class="h4 mb-0">Log Daily Attendance</h2>
</div>
<div class="card-body p-4">
<form method="POST" id="attendanceForm">
{% csrf_token %}
<div class="mb-4">
<label class="form-label fw-bold">{{ form.date.label }}</label>
{{ form.date }}
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-bold">{{ form.project.label }}</label>
{{ form.project }}
</div>
<div class="col-md-6">
<label class="form-label fw-bold">{{ form.supervisor.label }}</label>
{{ form.supervisor }}
</div>
</div>
<div class="mb-4">
<label class="form-label fw-bold">{{ form.team.label }} (Optional - selection will auto-check workers)</label>
{{ form.team }}
</div>
<div class="mb-4">
<label class="form-label fw-bold d-block mb-3">Workers Present</label>
<div class="worker-selection border rounded p-3 bg-light" style="max-height: 300px; overflow-y: auto;">
<div class="row">
{% for worker in form.workers %}
<div class="col-md-6 mb-2">
<div class="form-check">
{{ worker.tag }}
<label class="form-check-label" for="{{ worker.id_for_label }}">
{{ worker.choice_label }}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-bold">{{ form.overtime_amount.label }}</label>
{{ form.overtime_amount }}
</div>
</div>
<div class="mb-4">
<label class="form-label fw-bold">{{ form.notes.label }}</label>
{{ form.notes }}
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg shadow">Save Attendance Log</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block scripts %}
<script>
// Simple team selection auto-checking logic could be added here
// For this MVP, we'll keep it as a standard multi-select for workers.
</script>
{% endblock %}

View File

@ -1,145 +1,66 @@
{% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% extends 'base.html' %}
{% load static %}
{% block content %}
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<section class="hero-section text-center">
<div class="container">
<h1 class="display-4 mb-4">Construction Payroll & Attendance</h1>
<p class="lead mb-5">Efficiently track workers, projects, and payroll for Fox Fitt Construction.</p>
<div class="d-flex justify-content-center gap-3">
<a href="{% url 'attendance_log' %}" class="btn btn-primary btn-lg px-5 py-3">Log Attendance</a>
<a href="/admin/core/worker/" class="btn btn-outline-light btn-lg px-5 py-3">Manage Workers</a>
</div>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}
</section>
<section class="container py-5">
<div class="row g-4 text-center">
<div class="col-md-4">
<div class="card h-100 p-4">
<div class="card-body">
<h3 class="h1 text-primary mb-3">{{ total_workers|default:"0" }}</h3>
<h5 class="card-title">Active Workers</h5>
<p class="card-text text-muted">Field workers registered in the system.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 p-4">
<div class="card-body">
<h3 class="h1 text-primary mb-3">{{ total_projects|default:"0" }}</h3>
<h5 class="card-title">Projects</h5>
<p class="card-text text-muted">Ongoing solar farm foundation projects.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 p-4">
<div class="card-body">
<h3 class="h1 text-primary mb-3">{{ today_attendance|default:"0" }}</h3>
<h5 class="card-title">Today's Attendance</h5>
<p class="card-text text-muted">Workers logged for today across all sites.</p>
</div>
</div>
</div>
</div>
</section>
<section class="container py-5 bg-white rounded-3 shadow-sm mb-5">
<div class="row align-items-center">
<div class="col-lg-6">
<h2 class="mb-4">Streamlined Attendance Tracking</h2>
<p class="text-muted mb-4">Supervisors can now quickly log attendance directly from their mobile devices while on-site. Select a whole team with one click or pick individual workers for each project.</p>
<ul class="list-unstyled mb-4">
<li class="mb-2">✅ Real-time attendance logging</li>
<li class="mb-2">✅ Integrated overtime calculations</li>
<li class="mb-2">✅ Instant team selection</li>
<li class="mb-2">✅ Offline-first field record management</li>
</ul>
<a href="{% url 'attendance_log' %}" class="btn btn-primary px-4 py-2">Get Started</a>
</div>
<div class="col-lg-6">
<img src="https://images.pexels.com/photos/159306/construction-site-build-construction-worker-159306.jpeg?auto=compress&cs=tinysrgb&w=800" alt="Construction Worker" class="img-fluid rounded-3 shadow">
</div>
</div>
</section>
{% endblock %}

View File

@ -1,7 +1,7 @@
from django.urls import path
from .views import home
from . import views
urlpatterns = [
path("", home, name="home"),
]
path('', views.index, name='index'),
path('attendance/log/', views.attendance_log, name='attendance_log'),
]

View File

@ -1,25 +1,29 @@
import os
import platform
from django import get_version as django_version
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.utils import timezone
from .models import Worker, Project, WorkLog, Team
from .forms import AttendanceLogForm
from django.contrib import messages
def home(request):
"""Render the landing screen with loader and environment details."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now()
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()
context = {
"project_name": "New Style",
"agent_brand": agent_brand,
"django_version": django_version(),
"python_version": platform.python_version(),
"current_time": now,
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
'total_workers': total_workers,
'total_projects': total_projects,
'today_attendance': today_attendance,
}
return render(request, "core/index.html", context)
return render(request, 'core/index.html', context)
def attendance_log(request):
if request.method == 'POST':
form = AttendanceLogForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Attendance logged successfully!')
return redirect('index')
else:
form = AttendanceLogForm(initial={'date': timezone.now().date()})
return render(request, 'core/attendance_log.html', {'form': form})

View File

@ -1,3 +1,36 @@
anyio==4.12.1
asgiref==3.10.0
certifi==2022.9.24
chardet==5.1.0
charset-normalizer==3.0.1
dbus-python==1.3.2
distro-info==1.5+deb12u1
Django==5.2.7
h11==0.16.0
httpcore==1.0.9
httplib2==0.20.4
httpx==0.28.1
idna==3.3
markdown-it-py==2.1.0
mdurl==0.1.2
mysqlclient==2.2.7
netifaces==0.11.0
pillow==12.1.1
pycurl==7.45.2
Pygments==2.14.0
PyGObject==3.42.2
pyparsing==3.0.9
PySimpleSOAP==1.16.2
python-apt==2.6.0
python-debian==0.1.49
python-debianbts==4.0.1
python-dotenv==1.1.1
PyYAML==6.0
reportbug==12.0.0
requests==2.28.1
rich==13.3.1
six==1.16.0
sqlparse==0.5.3
typing_extensions==4.15.0
unattended-upgrades==0.1
urllib3==1.26.12

View File

@ -1,4 +1,50 @@
/* Custom styles for the application */
body {
font-family: system-ui, -apple-system, sans-serif;
:root {
--primary: #2F3E46;
--secondary: #84A59D;
--accent: #FFD166;
--background: #F7F7F7;
--text: #354F52;
}
body {
font-family: 'Open Sans', sans-serif;
background-color: var(--background);
color: var(--text);
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Montserrat', sans-serif;
font-weight: 700;
}
.navbar {
background-color: var(--primary) !important;
}
.btn-primary {
background-color: var(--secondary);
border-color: var(--secondary);
}
.btn-primary:hover {
background-color: var(--primary);
border-color: var(--primary);
}
.card {
border: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.hero-section {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 100px 0;
}
.footer {
background-color: var(--primary);
color: white;
padding: 20px 0;
margin-top: 50px;
}