diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 881731c..e7b86fc 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 42d995d..ac4b4d6 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..18cf908 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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", diff --git a/config/urls.py b/config/urls.py index bcfc074..1eee8cd 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 2964e11..4d4addd 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..2b6866d Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 18a063c..bd60181 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ebb8c6e..399aecc 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8d204fa..96f277a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..1fd949f 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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') \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..8e31894 --- /dev/null +++ b/core/forms.py @@ -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) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..72592a0 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..47d6fc4 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..e38921a 100644 --- a/core/models.py +++ b/core/models.py @@ -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 diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..45e4df7 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,68 @@ +{% load static %} - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Fox Fitt Construction{% endblock %} + + + + + + + + + + + {% block head %}{% endblock %} - - {% block content %}{% endblock %} - + + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+ {% block content %}{% endblock %} +
+ + + + + + {% block scripts %}{% endblock %} + diff --git a/core/templates/core/attendance_log.html b/core/templates/core/attendance_log.html new file mode 100644 index 0000000..6e354f8 --- /dev/null +++ b/core/templates/core/attendance_log.html @@ -0,0 +1,82 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+

Log Daily Attendance

+
+
+
+ {% csrf_token %} +
+ + {{ form.date }} +
+ +
+
+ + {{ form.project }} +
+
+ + {{ form.supervisor }} +
+
+ +
+ + {{ form.team }} +
+ +
+ +
+
+ {% for worker in form.workers %} +
+
+ {{ worker.tag }} + +
+
+ {% endfor %} +
+
+
+ +
+
+ + {{ form.overtime_amount }} +
+
+ +
+ + {{ form.notes }} +
+ +
+ +
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..5113fd6 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,66 @@ -{% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% extends 'base.html' %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

Construction Payroll & Attendance

+

Efficiently track workers, projects, and payroll for Fox Fitt Construction.

+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file + + +
+
+
+
+
+

{{ total_workers|default:"0" }}

+
Active Workers
+

Field workers registered in the system.

+
+
+
+
+
+
+

{{ total_projects|default:"0" }}

+
Projects
+

Ongoing solar farm foundation projects.

+
+
+
+
+
+
+

{{ today_attendance|default:"0" }}

+
Today's Attendance
+

Workers logged for today across all sites.

+
+
+
+
+
+ +
+
+
+

Streamlined Attendance Tracking

+

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.

+
    +
  • ✅ Real-time attendance logging
  • +
  • ✅ Integrated overtime calculations
  • +
  • ✅ Instant team selection
  • +
  • ✅ Offline-first field record management
  • +
+ Get Started +
+
+ Construction Worker +
+
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..ea76e98 100644 --- a/core/urls.py +++ b/core/urls.py @@ -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'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..9def89e 100644 --- a/core/views.py +++ b/core/views.py @@ -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}) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e22994c..7908add 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..0d3212e 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -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; +} \ No newline at end of file