Implemented financial tracking

This commit is contained in:
2026-03-20 13:07:28 -05:00
parent 6fa4fc1f7c
commit c88e344198
22 changed files with 1058 additions and 270 deletions

4
.antigravityrules Normal file
View 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
View 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.

View File

@@ -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

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View 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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@@ -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"),
]

View File

@@ -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})

View File

@@ -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;
}