Implemented financial tracking
This commit is contained in:
4
.antigravityrules
Normal file
4
.antigravityrules
Normal file
@@ -0,0 +1,4 @@
|
||||
# Django Virtual Environment Rule
|
||||
|
||||
Always use the virtual environment located at `.venv` when running any Python scripts, pip commands, or Django management commands.
|
||||
You can run commands using the virtual environment's Python executable directly (e.g., `.venv/bin/python manage.py ...`) or activate it first.
|
||||
4
.cursorrules
Normal file
4
.cursorrules
Normal file
@@ -0,0 +1,4 @@
|
||||
# Django Virtual Environment Rule
|
||||
|
||||
Always use the virtual environment located at `.venv` when running any Python scripts, pip commands, or Django management commands.
|
||||
You can run commands using the virtual environment's Python executable directly (e.g., `.venv/bin/python manage.py ...`) or activate it first.
|
||||
@@ -1,20 +1,90 @@
|
||||
import datetime
|
||||
from django import forms
|
||||
|
||||
from django.forms import ModelForm
|
||||
from .models import Employee, Contract, ChargeNumber
|
||||
from .models import Employee, Contract, ChargeNumber, TimeCardCell, AddressModel
|
||||
|
||||
class NewEmployeeForm(ModelForm):
|
||||
first_name = forms.CharField(max_length=30, required=False, label="First Name")
|
||||
last_name = forms.CharField(max_length=30, required=False, label="Last Name")
|
||||
address_1 = forms.CharField(max_length=128, label="Address Line 1")
|
||||
address_2 = forms.CharField(max_length=128, required=False, label="Address Line 2")
|
||||
city = forms.CharField(max_length=64, label="City")
|
||||
state = forms.CharField(max_length=2, label="State (e.g. NY)")
|
||||
zip_code = forms.CharField(max_length=5, label="ZIP Code")
|
||||
|
||||
class Meta:
|
||||
model = Employee
|
||||
fields = ["user", "manager", "phoneNumber", "slary"]
|
||||
|
||||
def save(self, commit=True):
|
||||
employee = super().save(commit=False)
|
||||
|
||||
if self.cleaned_data.get('first_name') or self.cleaned_data.get('last_name'):
|
||||
if self.cleaned_data.get('first_name'):
|
||||
employee.user.first_name = self.cleaned_data.get('first_name')
|
||||
if self.cleaned_data.get('last_name'):
|
||||
employee.user.last_name = self.cleaned_data.get('last_name')
|
||||
employee.user.save()
|
||||
|
||||
address = AddressModel.objects.create(
|
||||
address_1=self.cleaned_data['address_1'],
|
||||
address_2=self.cleaned_data['address_2'],
|
||||
city=self.cleaned_data['city'],
|
||||
state=self.cleaned_data['state'],
|
||||
zip_code=self.cleaned_data['zip_code'],
|
||||
)
|
||||
employee.primaryAddress = address
|
||||
employee.workAddress = address
|
||||
if commit:
|
||||
employee.save()
|
||||
return employee
|
||||
|
||||
class EmployeeForm(ModelForm):
|
||||
class Meta:
|
||||
model = Employee
|
||||
# TODO: fix slary to be salary
|
||||
fields = ["primaryAddress","workAddress", "slary"]
|
||||
fields = ["user", "manager", "primaryAddress", "workAddress", "phoneNumber", "slary"]
|
||||
|
||||
class ContractForm(ModelForm):
|
||||
class Meta:
|
||||
model = Contract
|
||||
fields = ["contract_type","name","proposed_amount","baseline_amount","funded_amount","baseline_start","baseline_end"]
|
||||
fields = ["contract_type","name","proposed_amount","baseline_amount","funded_amount","budget_hours","baseline_start","baseline_end"]
|
||||
|
||||
class ChargeNumberForm(ModelForm):
|
||||
class Meta:
|
||||
model = ChargeNumber
|
||||
fields = ["charge_number_type","amount", "start_date","end_date"]
|
||||
fields = ["charge_number_type","amount", "start_date","end_date"]
|
||||
|
||||
class TimeLogForm(ModelForm):
|
||||
start_time = forms.TimeField(required=False, widget=forms.TimeInput(attrs={'type': 'time'}))
|
||||
end_time = forms.TimeField(required=False, widget=forms.TimeInput(attrs={'type': 'time'}))
|
||||
hour = forms.FloatField(required=False, label="Duration (hours)")
|
||||
date = forms.DateField(initial=datetime.date.today, widget=forms.DateInput(attrs={'type': 'date'}))
|
||||
|
||||
class Meta:
|
||||
model = TimeCardCell
|
||||
fields = ["contract", "date", "start_time", "end_time", "hour"]
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
start = cleaned_data.get('start_time')
|
||||
end = cleaned_data.get('end_time')
|
||||
duration = cleaned_data.get('hour')
|
||||
|
||||
if start and end:
|
||||
dt_start = datetime.datetime.combine(datetime.date.today(), start)
|
||||
dt_end = datetime.datetime.combine(datetime.date.today(), end)
|
||||
diff = (dt_end - dt_start).total_seconds() / 3600.0
|
||||
if diff < 0:
|
||||
diff += 24.0
|
||||
cleaned_data['hour'] = round(diff, 2)
|
||||
elif start and duration:
|
||||
dt_start = datetime.datetime.combine(datetime.date.today(), start)
|
||||
dt_end = dt_start + datetime.timedelta(hours=duration)
|
||||
cleaned_data['end_time'] = (dt_end).time()
|
||||
elif not duration and not (start and end):
|
||||
raise forms.ValidationError("You must provide either (Start Time and End Time) OR (Start Time and Duration) OR (Duration).")
|
||||
|
||||
if not cleaned_data.get('hour') and duration:
|
||||
cleaned_data['hour'] = duration
|
||||
|
||||
return cleaned_data
|
||||
@@ -0,0 +1,76 @@
|
||||
# Generated by Django 5.0 on 2026-03-20 11:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('financial', '0005_chargenumber_financial_chargenumber_charge_number_type_chargenumbertypeenum_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='chargenumber',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chargenumber',
|
||||
name='last_modified_BY',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='budget_hours',
|
||||
field=models.FloatField(default=0.0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='last_modified_BY',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employee',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='employee',
|
||||
name='last_modified_BY',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timecard',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timecard',
|
||||
name='last_modified_BY',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timecardcell',
|
||||
name='contract',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='financial.contract'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timecardcell',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timecardcell',
|
||||
name='last_modified_BY',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0 on 2026-03-20 14:35
|
||||
|
||||
import django_enum.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('financial', '0006_chargenumber_created_by_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='contract',
|
||||
name='financial_Contract_contract_type_ContractTypeEnum',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='award_amount',
|
||||
field=models.FloatField(default=0.0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='contract_type',
|
||||
field=django_enum.fields.EnumCharField(choices=[('FFP', 'FIRM_FIX_PRICED'), ('CPFF', 'COST_PLUS_FIXED_FEE'), ('MAX', 'MAX_NUM_CONTRACT_TYPES')], max_length=4),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='contract',
|
||||
constraint=models.CheckConstraint(check=models.Q(('contract_type__in', ['FFP', 'CPFF', 'MAX'])), name='financial_Contract_contract_type_ContractTypeEnum'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.0 on 2026-03-20 14:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('financial', '0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='chargenumber',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contract',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='employee',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timecard',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timecardcell',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0 on 2026-03-20 15:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('financial', '0008_alter_chargenumber_slug_alter_contract_slug_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='timecardcell',
|
||||
name='end_time',
|
||||
field=models.TimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timecardcell',
|
||||
name='start_time',
|
||||
field=models.TimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.0 on 2026-03-20 15:30
|
||||
|
||||
import django.db.models.deletion
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('financial', '0009_timecardcell_end_time_timecardcell_start_time'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='employee',
|
||||
name='manager',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='manager_employee', to='financial.employee'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='employee',
|
||||
name='phoneNumber',
|
||||
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -6,25 +6,36 @@ from django_enum import EnumField
|
||||
from django.utils.text import slugify
|
||||
import datetime
|
||||
|
||||
def user_str(self):
|
||||
if self.first_name and self.last_name:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
if self.username:
|
||||
return self.username
|
||||
if self.email:
|
||||
return self.email
|
||||
return str(self.id)
|
||||
|
||||
User.__str__ = user_str
|
||||
|
||||
# Abstract Model Classes
|
||||
class TimeMixin(models.Model):
|
||||
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
last_modified = models.DateTimeField(default=timezone.now)
|
||||
created_by = models.ForeignKey
|
||||
last_modified_BY = models.ForeignKey
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='+')
|
||||
last_modified_BY = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='+')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class IdMixin(models.Model):
|
||||
slug = models.SlugField()
|
||||
slug = models.SlugField(blank=True, unique=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.slug is None:
|
||||
if not self.slug:
|
||||
self.slug = slugify(datetime.datetime.now().time())
|
||||
super(IdMixin, self).save(*args, **kwargs)
|
||||
|
||||
@@ -32,6 +43,7 @@ class IdMixin(models.Model):
|
||||
class Contract(IdMixin, TimeMixin):
|
||||
class ContractTypeEnum(models.TextChoices):
|
||||
FIRM_FIX_PRICED = "FFP", "FIRM_FIX_PRICED"
|
||||
COST_PLUS_FIXED_FEE = "CPFF", "COST_PLUS_FIXED_FEE"
|
||||
MAX_NUM_CONRACT_TYPES = "MAX", "MAX_NUM_CONTRACT_TYPES"
|
||||
|
||||
contract_type = EnumField(ContractTypeEnum)
|
||||
@@ -40,11 +52,16 @@ class Contract(IdMixin, TimeMixin):
|
||||
proposed_amount = models.FloatField(default=0.0)
|
||||
baseline_amount = models.FloatField(default=0.0)
|
||||
funded_amount = models.FloatField(default=0.0)
|
||||
award_amount = models.FloatField(default=0.0)
|
||||
budget_hours = models.FloatField(default=0.0)
|
||||
|
||||
baseline_start = models.DateField(null=True, blank=True, default=None)
|
||||
baseline_end = models.DateField(null=True, blank=True, default=None)
|
||||
linkedContracts = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} - {self.contract_type}"
|
||||
|
||||
# TODO: make calc ev func
|
||||
|
||||
class ChargeNumber(IdMixin, TimeMixin):
|
||||
@@ -73,14 +90,19 @@ class AddressModel(models.Model):
|
||||
zip_code = models.CharField(max_length=5)
|
||||
|
||||
class Employee(IdMixin, TimeMixin):
|
||||
manager = models.ForeignKey("self", on_delete=models.CASCADE, related_name="manager_employee")
|
||||
manager = models.ForeignKey("self", on_delete=models.CASCADE, related_name="manager_employee", null=True, blank=True)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
primaryAddress = models.ForeignKey(AddressModel, on_delete=models.CASCADE, related_name="primary_address_employee")
|
||||
workAddress = models.ForeignKey(AddressModel, on_delete=models.CASCADE, related_name="work_address_employee")
|
||||
phoneNumber = PhoneNumberField(null=False, blank=False, unique=True)
|
||||
phoneNumber = PhoneNumberField(null=True, blank=True, unique=True)
|
||||
slary= models.FloatField(default=0.0)
|
||||
|
||||
# TODO: get hourly salary
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
@property
|
||||
def hourly_salary(self):
|
||||
return (self.slary or 0.0) / 2080.0
|
||||
# TODO: roles, jpbTitles
|
||||
|
||||
|
||||
@@ -97,7 +119,10 @@ class TimeCard(IdMixin, TimeMixin):
|
||||
class TimeCardCell(IdMixin, TimeMixin):
|
||||
timeCard = models.ForeignKey(TimeCard, on_delete=models.CASCADE)
|
||||
date = models.DateField(null=True, default = None)
|
||||
start_time = models.TimeField(null=True, blank=True)
|
||||
end_time = models.TimeField(null=True, blank=True)
|
||||
hour = models.FloatField(default = 0.0)
|
||||
contract = models.ForeignKey(Contract, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,56 +1,65 @@
|
||||
<!-- a lot of stuff-->
|
||||
|
||||
<!doctype html>
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
<html lang="en"">
|
||||
<head>
|
||||
<title>Contract Detail</title>
|
||||
<link href=" {% static 'financial/css/material-dashboard.css' %}" rel="stylesheet">
|
||||
<link rel="icon" type="image/xicon" href="{% static 'public/img/favicon.jpg' %}">
|
||||
|
||||
<body class="g-sidenav-show bg-gray-200">
|
||||
{% block title %}Contract Detail - AI ML Operations{% endblock %}
|
||||
|
||||
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg">
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<a href="{% url 'contracts' %}" class="btn" style="padding: 0.5rem 1.5rem; font-size: 0.9rem;">Back to
|
||||
Contracts</a>
|
||||
</div>
|
||||
|
||||
{% if is_new %}
|
||||
<h1>New Contract</h1>
|
||||
<form method="post" action='{% url "new_contract" %}'>
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<button type="" button" value="submit">Create</button>
|
||||
</form>
|
||||
<h1 class="section-title" style="text-align: left;">New Contract</h1>
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="post" action='{% url "new_contract" %}'>
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn" style="margin-top: 1rem;">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<h1> {{ contract.name }}</h1>
|
||||
<form method="post" action='{% url "contract_detail" contract.slug %}'>
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<button type="" button" value="submit">Update</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<h1 class="section-title" style="text-align: left;">{{ contract.name }}</h1>
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="post" action='{% url "contract_detail" contract.slug %}'>
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn" style="margin-top: 1rem;">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr style="margin: 40px 0; border: 0; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% if is_new %}
|
||||
{% else %}
|
||||
<h2> Charge Numbers</h2>
|
||||
<h2 class="section-title" style="text-align: left; font-size: 2rem;">Charge Numbers</h2>
|
||||
{% if charge_numbes %}
|
||||
<p> put charge number table here</p>
|
||||
<p> Create a new charge number</p>
|
||||
{{ charge_number_form }}
|
||||
<div class="card" style="margin-bottom: 2rem;">
|
||||
<p style="color: var(--text-muted);">put charge number table here</p>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<h3 style="margin-bottom: 1rem;">Create a new charge number</h3>
|
||||
<form method="post" action='{% url "new_charge_number" %}'>
|
||||
{% csrf_token %}
|
||||
{{ charge_number_form.as_p }}
|
||||
<button type="submit" class="btn" style="margin-top: 1rem;">Add Charge Number</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<p> There are no charge numbers for this contract</p>
|
||||
<form method="post" action='{% url "new_charge_number" %}'>
|
||||
{% csrf_token %}
|
||||
{{ charge_number_form }}
|
||||
<button type="" button" value="submit">Update</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">There are no charge numbers for this contract.
|
||||
</p>
|
||||
<form method="post" action='{% url "new_charge_number" %}'>
|
||||
{% csrf_token %}
|
||||
{{ charge_number_form.as_p }}
|
||||
<button type="submit" class="btn" style="margin-top: 1rem;">Add Charge Number</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</body>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,50 +1,151 @@
|
||||
<!-- a lot of stuff-->
|
||||
|
||||
<!doctype html>
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
<html lang="en"">
|
||||
<head>
|
||||
<title>Profile</title>
|
||||
<link href=" {% static 'financial/css/material-dashboard.css' %}" rel="stylesheet">
|
||||
<link rel="icon" type="image/xicon" href="{% static 'public/img/favicon.jpg' %}">
|
||||
|
||||
<body class="g-sidenav-show bg-gray-200">
|
||||
{% block title %}Contracts - AI ML Operations{% endblock %}
|
||||
|
||||
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg">
|
||||
|
||||
|
||||
<h1>Contracts</h1>
|
||||
{% if contracts %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Identifier</th>
|
||||
<th>Type</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Proposed Amount</th>
|
||||
<th>Baseline Amount</th>
|
||||
<th>Funded Amount</th>
|
||||
</tr>
|
||||
|
||||
{% for contract in contracts %}
|
||||
<tr>
|
||||
<td> <a href="{% url 'contract_detail' contract.slug %}">{{ contract.name }}</a></td>
|
||||
<td> {{ contract.slug }}</td>
|
||||
<td> {{ contract.contract_type }}</td>
|
||||
<td> {{ contract.baseline_start }}</td>
|
||||
<td> {{ contract.baseline_end }}</td>
|
||||
<td> {{ contract.proposed_amount }}</td>
|
||||
<td> {{ contract.baseline_amount }}</td>
|
||||
<td> {{ contract.funded_amount }}</td>
|
||||
</tr>
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
||||
<h1 class="section-title" style="margin-bottom: 0;">Contracts</h1>
|
||||
<a href="{% url 'financial_index' %}" class="btn" style="padding: 0.5rem 1.5rem; font-size: 0.9rem;">Back to
|
||||
Dashboard</a>
|
||||
</div>
|
||||
|
||||
{% if chart_data_json %}
|
||||
<h2 class="section-title" style="font-size: 2rem; margin-bottom: 2rem;">Burn Rate Charts</h2>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 3rem;">
|
||||
{% for c in contracts %}
|
||||
<div
|
||||
style="flex: 1; min-width: 300px; max-width: 400px; background: var(--surface-color); border: 1px solid rgba(255,255,255,0.1); padding: 15px; border-radius: 8px;">
|
||||
<canvas id="chart_{{ forloop.counter }}"></canvas>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<hr>
|
||||
<p> Create <a href="{% url 'new_contract' %}">new</a> contract.</p>
|
||||
{% else %}
|
||||
<p> There are no contracts. <a href="{% url 'new_contract' %}">Please make one</a></p>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</body>
|
||||
|
||||
<div class="table-responsive">
|
||||
{% if contracts %}
|
||||
<table class="table" style="margin-bottom: 2rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Identifier</th>
|
||||
<th>Type</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Proposed Amount</th>
|
||||
<th>Budget Hours</th>
|
||||
<th>Hours Charged</th>
|
||||
<th>Money Spent</th>
|
||||
<th>Money Remaining</th>
|
||||
<th>Projected End Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contract in contracts %}
|
||||
<tr>
|
||||
<td><a href="{% url 'contract_detail' contract.slug %}">{{ contract.name }}</a></td>
|
||||
<td>{{ contract.slug }}</td>
|
||||
<td>{{ contract.contract_type }}</td>
|
||||
<td>{{ contract.baseline_start }}</td>
|
||||
<td>{{ contract.baseline_end }}</td>
|
||||
<td>{{ contract.proposed_amount }}</td>
|
||||
<td>{{ contract.budget_hours }}</td>
|
||||
<td>{{ contract.total_hours_spent }}</td>
|
||||
<td>${{ contract.total_money_spent|floatformat:2 }}</td>
|
||||
<td>${{ contract.remaining_money|floatformat:2 }}</td>
|
||||
<td>{{ contract.projected_end_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Create a <a href="{% url 'new_contract' %}" class="text-cyber-cyan"
|
||||
style="text-decoration: underline;">new contract</a>.</p>
|
||||
{% else %}
|
||||
<p style="color: var(--text-muted);">There are no contracts. <a href="{% url 'new_contract' %}"
|
||||
class="text-cyber-cyan" style="text-decoration: underline;">Please make one</a>.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.1/dist/chartjs-adapter-moment.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const chartDataRaw = '{{ chart_data_json|escapejs }}';
|
||||
if (chartDataRaw) {
|
||||
Chart.defaults.color = '#e0e0e0';
|
||||
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||||
|
||||
const chartData = JSON.parse(chartDataRaw);
|
||||
chartData.forEach((data, index) => {
|
||||
const canvas = document.getElementById('chart_' + (index + 1));
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Historical Burn',
|
||||
data: [
|
||||
{ x: data.start_date, y: data.budget },
|
||||
{ x: data.today, y: data.remaining }
|
||||
],
|
||||
borderColor: '#00f3ff', /* Cyber cyan */
|
||||
backgroundColor: '#00f3ff',
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
},
|
||||
{
|
||||
label: 'Projected Burn',
|
||||
data: [
|
||||
{ x: data.today, y: data.remaining },
|
||||
{ x: data.projected_end, y: 0 }
|
||||
],
|
||||
borderColor: '#bc13fe', /* Neon purple */
|
||||
borderDash: [5, 5],
|
||||
backgroundColor: '#bc13fe',
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: data.name + ' Burn Rate ($)',
|
||||
color: '#ffffff'
|
||||
},
|
||||
legend: {
|
||||
labels: { color: '#e0e0e0' }
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: { unit: 'month' },
|
||||
title: { display: true, text: 'Date', color: '#a0a0a0' },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#a0a0a0' }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Remaining Budget ($)', color: '#a0a0a0' },
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#a0a0a0' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Edit Time Log - AI ML Operations{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<h1 class="section-title" style="text-align: left;">Edit Time Log</h1>
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div style="margin-top: 1.5rem; display: flex; gap: 1rem; align-items: center;">
|
||||
<button type="submit" class="btn">Update Record</button>
|
||||
<a href="{% url 'time_logs' %}" class="btn"
|
||||
style="background: var(--surface-color); color: var(--text-color); border: 1px solid rgba(255,255,255,0.1);">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,118 +1,95 @@
|
||||
<!doctype html>
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
<html lang="en"">
|
||||
<head>
|
||||
<title>Accounting</title>
|
||||
<link href=" {% static 'financial/css/material-dashboard.css' %}" rel="stylesheet">
|
||||
<link rel="icon" type="image/xicon" href="{% static 'public/img/favicon.jpg' %}">
|
||||
|
||||
<body class="g-sidenav-show bg-gray-200">
|
||||
<div class="sidenav-header">
|
||||
<ul class="navbar-item">
|
||||
<li class="nav-item">
|
||||
Dashboard
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
Settings
|
||||
</li>
|
||||
{% block title %}Accounting Dashboard{% endblock %}
|
||||
|
||||
</ul>
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<h1 class="section-title">Dashboard</h1>
|
||||
|
||||
<div class="card-grid" style="margin-bottom: 3rem;">
|
||||
<a href="{% url 'contracts' %}" class="card">
|
||||
<span class="card-title">View Contracts</span>
|
||||
<p class="card-text">Overview of all active and past contracts and their scopes.</p>
|
||||
</a>
|
||||
<a href="{% url 'new_contract' %}" class="card">
|
||||
<span class="card-title">New Contract</span>
|
||||
<p class="card-text">Establish a new project contract to track budgets.</p>
|
||||
</a>
|
||||
<a href="{% url 'new_employee' %}" class="card">
|
||||
<span class="card-title">New Employee</span>
|
||||
<p class="card-text">Add a new personnel member to your organization.</p>
|
||||
</a>
|
||||
<a href="{% url 'Timekeeping' %}" class="card">
|
||||
<span class="card-title">Log Time</span>
|
||||
<p class="card-text">Record work hours against specific contracts.</p>
|
||||
</a>
|
||||
<a href="{% url 'time_logs' %}" class="card">
|
||||
<span class="card-title">Manage Time Logs</span>
|
||||
<p class="card-text">Review and edit submitted time entries.</p>
|
||||
</a>
|
||||
<a href="{% url 'client_reports' %}" class="card">
|
||||
<span class="card-title">Client Reports</span>
|
||||
<p class="card-text">Generate comprehensive reports for billing.</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title" style="font-size: 2rem; margin-bottom: 2rem;">Contracts Overview</h2>
|
||||
<div class="table-responsive">
|
||||
{% if contracts %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Contract Name</th>
|
||||
<th>Budget Hours</th>
|
||||
<th>Logged Hours</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in contracts %}
|
||||
<tr>
|
||||
<td><a href="{% url 'contract_detail' c.slug %}">{{ c.name }}</a></td>
|
||||
<td>{{ c.budget_hours }}</td>
|
||||
<td>{{ c.total_logged }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color: var(--text-muted);">No contracts available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr style="margin: 40px 0; border: 0; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
|
||||
<h2 class="section-title" style="font-size: 2rem; margin-bottom: 2rem;">Employee Hours per Contract</h2>
|
||||
<div class="table-responsive">
|
||||
{% if employee_data %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Employee Name</th>
|
||||
{% for c in contracts %}
|
||||
<th>{{ c.name }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in employee_data %}
|
||||
<tr>
|
||||
<td>{{ row.employee }}</td>
|
||||
{% for ch in row.contract_hours %}
|
||||
<td>{{ ch.hours }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color: var(--text-muted);">No employees available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg">
|
||||
<nav class="navbar navbar-main navbar-expand-lg px-0 mx-4 shadow-none border-radius-xl" id="navbarBlur"
|
||||
data-scroll="true">
|
||||
<div class="container-fluid py-1 px-3">
|
||||
<nav aria-label="breadcrumb">
|
||||
<h6>Dashboard</h6>
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-sm-4 mb-xl-o mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header p-3 pt-2">
|
||||
<a href="{% url 'contracts' %}">
|
||||
Contracts
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div class="card-body">
|
||||
<p> put picture here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if is_worker %}
|
||||
<div class="col-xl-3 col-sm-4 mb-xl-o mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header p-3 pt-2">
|
||||
<a href="{% url 'profile' %}">
|
||||
Profile
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div class="card-body">
|
||||
<p> put picture here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_worker %}
|
||||
<div class="col-xl-3 col-sm-4 mb-xl-o mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header p-3 pt-2">
|
||||
<a href="{% url 'Timekeeping' %}">
|
||||
Timekeeping
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div class="card-body">
|
||||
<p> put picture here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_manager %}
|
||||
<div class="col-xl-3 col-sm-4 mb-xl-o mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header p-3 pt-2">
|
||||
<a href="{% url 'Timeapproval' %}">
|
||||
Time Approval
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p> put picture here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_procurment_officer %}
|
||||
<div class="col-xl-3 col-sm-4 mb-xl-o mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header p-3 pt-2">
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p> put picture here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_finance %}
|
||||
<div class="col-xl-3 col-sm-4 mb-xl-o mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header p-3 pt-2">
|
||||
<a href="{% url 'contracts' %}">
|
||||
Contracts
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p> put picture here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<h1>Accouting</h1>
|
||||
</body>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
company_site/financial/templates/financial/new_employee.html
Normal file
25
company_site/financial/templates/financial/new_employee.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}New Employee - AI ML Operations{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<a href="{% url 'financial_index' %}" class="btn" style="padding: 0.5rem 1.5rem; font-size: 0.9rem;">Back to
|
||||
Dashboard</a>
|
||||
</div>
|
||||
|
||||
<h1 class="section-title" style="text-align: left;">New Employee</h1>
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="post" action="{% url 'new_employee' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn" style="margin-top: 1rem;">Create Employee</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1 +1,13 @@
|
||||
<p> this page has not been created yet</p>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Page Not Found{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container" style="text-align: center; padding: 4rem 0;">
|
||||
<h1 class="section-title" style="margin-bottom: 1.5rem;">Coming Soon</h1>
|
||||
<p style="color: var(--text-muted); font-size: 1.2rem;">This page has not been created yet.</p>
|
||||
<a href="{% url 'financial_index' %}" class="btn" style="margin-top: 2rem;">Return to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,18 +1,19 @@
|
||||
<!-- a lot of stuff-->
|
||||
|
||||
<!doctype html>
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
<html lang="en"">
|
||||
<head>
|
||||
<title>Profile</title>
|
||||
<link href=" {% static 'financial/css/material-dashboard.css' %}" rel="stylesheet">
|
||||
<link rel="icon" type="image/xicon" href="{% static 'public/img/favicon.jpg' %}">
|
||||
|
||||
<body class="g-sidenav-show bg-gray-200">
|
||||
{% block title %}Profile - AI ML Operations{% endblock %}
|
||||
|
||||
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg">
|
||||
|
||||
|
||||
<h1>Profile</h1>
|
||||
{{ form }}
|
||||
</body>
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<h1 class="section-title" style="text-align: left;">Profile</h1>
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn" style="margin-top: 1rem;">Save Profile</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
44
company_site/financial/templates/financial/reports.html
Normal file
44
company_site/financial/templates/financial/reports.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Client Reports - AI ML Operations{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
||||
<h1 class="section-title" style="margin-bottom: 0;">Client Reports</h1>
|
||||
<a href="{% url 'financial_index' %}" class="btn" style="padding: 0.5rem 1.5rem; font-size: 0.9rem;">Back to
|
||||
Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
{% if contracts %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Contract Name</th>
|
||||
<th>Total Budget (Hours)</th>
|
||||
<th>Total Logged (Hours)</th>
|
||||
<th>Remaining Budget (Hours)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in contracts %}
|
||||
<tr>
|
||||
<td>{{ c.name }}</td>
|
||||
<td>{{ c.budget_hours }}</td>
|
||||
<td>{{ c.total_logged }}</td>
|
||||
<td style="{% if c.remaining_budget < 0 %}color: #ff4444; font-weight: bold;{% endif %}">{{
|
||||
c.remaining_budget }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color: var(--text-muted); text-align: center; padding: 2rem;">No data available to report.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
64
company_site/financial/templates/financial/time_logs.html
Normal file
64
company_site/financial/templates/financial/time_logs.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Time Logs - AI ML Operations{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
||||
<h1 class="section-title" style="margin-bottom: 0;">All Time Logs</h1>
|
||||
<div>
|
||||
<a href="{% url 'financial_index' %}" class="btn"
|
||||
style="padding: 0.5rem 1.5rem; font-size: 0.9rem; margin-right: 1rem; background: var(--surface-color); color: var(--text-color); border: 1px solid rgba(255,255,255,0.1);">Back
|
||||
to Dashboard</a>
|
||||
<a href="{% url 'Timekeeping' %}" class="btn" style="padding: 0.5rem 1.5rem; font-size: 0.9rem;">Log New
|
||||
Time</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Employee</th>
|
||||
<th>Contract</th>
|
||||
<th>Date</th>
|
||||
<th>Start Time</th>
|
||||
<th>End Time</th>
|
||||
<th>Duration (hrs)</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log.timeCard.employee }}</td>
|
||||
<td>{{ log.contract }}</td>
|
||||
<td>{{ log.date }}</td>
|
||||
<td>{{ log.start_time|default_if_none:"" }}</td>
|
||||
<td>{{ log.end_time|default_if_none:"" }}</td>
|
||||
<td>{{ log.hour }}</td>
|
||||
<td>
|
||||
<a href="{% url 'edit_time_log' log.id %}" class="text-cyber-cyan"
|
||||
style="margin-right: 10px;">Edit</a>
|
||||
<form action="{% url 'delete_time_log' log.id %}" method="POST" style="display:inline;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this time log?');">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
style="background:none; border:none; color: #ff4444; cursor:pointer; font-size: 0.95rem; font-family: var(--font-main);">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" style="color: var(--text-muted); text-align: center; padding: 2rem;">No time
|
||||
logs found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
31
company_site/financial/templates/financial/timekeeping.html
Normal file
31
company_site/financial/templates/financial/timekeeping.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Timekeeping - AI ML Operations{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<a href="{% url 'financial_index' %}" class="btn" style="padding: 0.5rem 1.5rem; font-size: 0.9rem;">Back to
|
||||
Dashboard</a>
|
||||
</div>
|
||||
|
||||
<h1 class="section-title" style="text-align: left;">Log Time</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" style="max-width: 600px; margin-bottom: 1.5rem;">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card" style="max-width: 600px;">
|
||||
<form method="post" action="{% url 'Timekeeping' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" class="btn" style="margin-top: 1rem;">Log Time</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,13 +5,18 @@ from . import views
|
||||
urlpatterns = [
|
||||
path("", views.index, name="financial_index"),
|
||||
path("timekeeping", views.timekeeping, name="Timekeeping"),
|
||||
path("time_logs", views.time_logs, name="time_logs"),
|
||||
path("time_logs/<int:log_id>/edit", views.edit_time_log, name="edit_time_log"),
|
||||
path("time_logs/<int:log_id>/delete", views.delete_time_log, name="delete_time_log"),
|
||||
path("timeapproval", views.timeapproval, name="Timeapproval"),
|
||||
path("contracts", views.contracts, name="contracts"),
|
||||
path("<str:contract_slug>/contract_detail", views.contract_detail, name="contract_detail"),
|
||||
path("new_contract", views.new_contract, name="new_contract"),
|
||||
path("new_employee", views.new_employee, name="new_employee"),
|
||||
path("new_charge_number", views.new_charge_number, name="new_charge_number"),
|
||||
path("<str:charge_number_slug>/update_charge_number", views.update_charge_number, name="update_charge_number"),
|
||||
#path("contracts/<int:contract_id>/", views.contract_detail, name="contract"),
|
||||
path("procurements", views.procurement, name="procurements"),
|
||||
path("profile", views.profile, name="profile"),
|
||||
path("client_reports", views.client_reports, name="client_reports"),
|
||||
]
|
||||
@@ -1,79 +1,210 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from .forms import EmployeeForm, ContractForm, ChargeNumberForm
|
||||
from .models import Contract
|
||||
# Create your views here.
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from .forms import EmployeeForm, ContractForm, ChargeNumberForm, TimeLogForm, NewEmployeeForm
|
||||
from .models import Contract, TimeCard, TimeCardCell, Employee
|
||||
from django.utils import timezone
|
||||
from django.db.models import Sum
|
||||
from datetime import timedelta
|
||||
import json
|
||||
|
||||
# PAGES TO CREATE
|
||||
# dashboard
|
||||
# log in
|
||||
# log out
|
||||
# password reset
|
||||
# employee timecard
|
||||
# time card approval
|
||||
# contract
|
||||
# charge number
|
||||
# user management (?)
|
||||
def is_admin(user):
|
||||
return user.is_active and user.is_superuser
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def index(request):
|
||||
permissions = []
|
||||
context = {
|
||||
'is_procurment_officer':True,
|
||||
'is_worker':True,
|
||||
'is_manager':True,
|
||||
'is_finance': True
|
||||
|
||||
}
|
||||
return render(request, "financial/index.html", context)
|
||||
|
||||
def contracts(request):
|
||||
contracts = Contract.objects.all()
|
||||
return render(request, 'financial/contracts.html', {'contracts':contracts})
|
||||
for c in contracts:
|
||||
total = TimeCardCell.objects.filter(contract=c).aggregate(Sum('hour'))['hour__sum']
|
||||
c.total_logged = total if total else 0.0
|
||||
|
||||
employees = Employee.objects.all()
|
||||
employee_data = []
|
||||
for e in employees:
|
||||
contract_hours = []
|
||||
for c in contracts:
|
||||
total_e_c = TimeCardCell.objects.filter(contract=c, timeCard__employee=e).aggregate(Sum('hour'))['hour__sum']
|
||||
contract_hours.append({'contract': c, 'hours': total_e_c if total_e_c else 0.0})
|
||||
employee_data.append({'employee': e, 'contract_hours': contract_hours})
|
||||
|
||||
return render(request, "financial/index.html", {
|
||||
'contracts': contracts,
|
||||
'employee_data': employee_data
|
||||
})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def new_employee(request):
|
||||
if request.method == "POST":
|
||||
form = NewEmployeeForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('financial_index')
|
||||
else:
|
||||
form = NewEmployeeForm()
|
||||
return render(request, 'financial/new_employee.html', {"form": form})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def contracts(request):
|
||||
contracts_list = Contract.objects.all()
|
||||
today = timezone.now().date()
|
||||
|
||||
chart_data_list = []
|
||||
|
||||
for c in contracts_list:
|
||||
cells = TimeCardCell.objects.filter(contract=c).select_related('timeCard__employee').order_by('date')
|
||||
|
||||
total_hours = sum((cell.hour or 0.0) for cell in cells)
|
||||
total_money = sum((cell.hour or 0.0) * cell.timeCard.employee.hourly_salary for cell in cells)
|
||||
|
||||
c.total_hours_spent = total_hours
|
||||
c.total_money_spent = total_money
|
||||
c.remaining_hours = max(0, c.budget_hours - total_hours)
|
||||
|
||||
budget_amt = c.funded_amount if c.funded_amount > 0 else c.proposed_amount
|
||||
c.remaining_money = max(0, budget_amt - total_money)
|
||||
|
||||
proj_end = None
|
||||
|
||||
if cells.exists():
|
||||
first_date = cells.first().date or c.baseline_start or today
|
||||
last_date = cells.last().date or today
|
||||
days_elapsed = (last_date - first_date).days
|
||||
if days_elapsed <= 0:
|
||||
days_elapsed = 1
|
||||
|
||||
daily_hour_burn = total_hours / days_elapsed
|
||||
daily_money_burn = total_money / days_elapsed
|
||||
|
||||
days_out_hours = (c.remaining_hours / daily_hour_burn) if daily_hour_burn > 0 else 9999
|
||||
days_out_money = (c.remaining_money / daily_money_burn) if daily_money_burn > 0 else 9999
|
||||
|
||||
days_out = max(days_out_hours, days_out_money)
|
||||
|
||||
if days_out < 9999:
|
||||
proj_end = last_date + timedelta(days=int(days_out))
|
||||
|
||||
c.projected_end_date = proj_end if proj_end else "N/A"
|
||||
|
||||
start_dt = str(c.baseline_start or today)
|
||||
end_dt = str(proj_end or (today + timedelta(days=30)))
|
||||
|
||||
chart_data_list.append({
|
||||
'name': c.name,
|
||||
'start_date': start_dt,
|
||||
'today': str(today),
|
||||
'projected_end': end_dt,
|
||||
'budget': float(budget_amt),
|
||||
'spent': float(total_money),
|
||||
'remaining': float(c.remaining_money)
|
||||
})
|
||||
|
||||
return render(request, 'financial/contracts.html', {
|
||||
'contracts': contracts_list,
|
||||
'chart_data_json': json.dumps(chart_data_list)
|
||||
})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def contract_detail(request, contract_slug):
|
||||
contract = Contract.objects.filter(slug=contract_slug)
|
||||
contract = Contract.objects.filter(slug=contract_slug).first()
|
||||
if request.method == 'POST':
|
||||
form = ContractForm(request.POST, instance=contract[0])
|
||||
form = ContractForm(request.POST, instance=contract)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('contracts')
|
||||
else:
|
||||
form = ContractForm(instance = contract[0])
|
||||
form = ContractForm(instance=contract)
|
||||
charge_number_form = ChargeNumberForm()
|
||||
# TODO: handle multiple better but we can assume there is only one
|
||||
|
||||
return render(request, 'financial/contract_detail.html', {'is_new': False, 'form': form, 'charge_number_form':charge_number_form, 'contract': contract[0]})
|
||||
return render(request, 'financial/contract_detail.html', {'is_new': False, 'form': form, 'charge_number_form':charge_number_form, 'contract': contract})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def new_contract(request):
|
||||
if request.method == "POST":
|
||||
form = ContractForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('contracts')
|
||||
else:
|
||||
return render(request, 'financial/contract_detail.html', {"form": ContractForm(), 'is_new': True})
|
||||
|
||||
else:
|
||||
return render(request, 'financial/contract_detail.html', {"form": ContractForm(), 'is_new': True})
|
||||
form = ContractForm()
|
||||
return render(request, 'financial/contract_detail.html', {"form": form, 'is_new': True})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def timekeeping(request):
|
||||
if request.method == "POST":
|
||||
form = TimeLogForm(request.POST)
|
||||
if form.is_valid():
|
||||
employee = Employee.objects.filter(user=request.user).first()
|
||||
if not employee:
|
||||
employee = Employee.objects.first()
|
||||
if not employee:
|
||||
return render(request, 'financial/timekeeping.html', {"form": form, "error": "No Employee exists. Cannot auto-create TimeCard. Please create an Employee first."})
|
||||
|
||||
time_card, _ = TimeCard.objects.get_or_create(employee=employee, startDate=timezone.now().date(), endDate=timezone.now().date())
|
||||
cell = form.save(commit=False)
|
||||
cell.timeCard = time_card
|
||||
cell.save()
|
||||
return redirect('financial_index')
|
||||
else:
|
||||
form = TimeLogForm()
|
||||
return render(request, 'financial/timekeeping.html', {'form': form})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def time_logs(request):
|
||||
logs = TimeCardCell.objects.all().order_by('-date', '-created')
|
||||
return render(request, 'financial/time_logs.html', {'logs': logs})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def edit_time_log(request, log_id):
|
||||
log_entry = TimeCardCell.objects.filter(id=log_id).first()
|
||||
if not log_entry:
|
||||
return redirect('time_logs')
|
||||
|
||||
if request.method == "POST":
|
||||
form = TimeLogForm(request.POST, instance=log_entry)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('time_logs')
|
||||
else:
|
||||
form = TimeLogForm(instance=log_entry)
|
||||
|
||||
return render(request, 'financial/edit_time_log.html', {'form': form, 'log': log_entry})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def delete_time_log(request, log_id):
|
||||
if request.method == "POST":
|
||||
log_entry = TimeCardCell.objects.filter(id=log_id).first()
|
||||
if log_entry:
|
||||
log_entry.delete()
|
||||
return redirect('time_logs')
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def client_reports(request):
|
||||
contracts = Contract.objects.all()
|
||||
for c in contracts:
|
||||
total = TimeCardCell.objects.filter(contract=c).aggregate(Sum('hour'))['hour__sum']
|
||||
c.total_logged = total if total else 0.0
|
||||
c.remaining_budget = c.budget_hours - c.total_logged
|
||||
return render(request, 'financial/reports.html', {'contracts': contracts})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def update_charge_number(request, charge_number_slug):
|
||||
return render(request, 'financial/not_created.html', {})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def new_charge_number(request, charge_number_slug):
|
||||
return render(request, 'financial/not_created.html', {})
|
||||
|
||||
def timekeeping(request):
|
||||
return render(request, 'financial/not_created.html', {})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def timeapproval(request):
|
||||
return render(request, 'financial/not_created.html', {})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def chargenumber(request):
|
||||
return render(request, 'financial/not_created.html', {})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def procurement(request):
|
||||
return render(request, 'financial/procurement.html', {})
|
||||
|
||||
@user_passes_test(is_admin)
|
||||
def profile(request):
|
||||
form = EmployeeForm()
|
||||
return render(request, 'financial/profile.html', {'form': form})
|
||||
# def contract_detail(request, contract_id):
|
||||
return render(request, 'financial/profile.html', {'form': form})
|
||||
@@ -458,4 +458,69 @@ nav {
|
||||
.hero-image {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 1rem;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
||||
color: var(--primary-color);
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Django Form Styling defaults */
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-family: var(--font-main);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
transition: border-color var(--transition-speed);
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.helptext {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
Reference in New Issue
Block a user