diff --git a/.antigravityrules b/.antigravityrules
new file mode 100644
index 0000000..3138ce4
--- /dev/null
+++ b/.antigravityrules
@@ -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.
diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 0000000..3138ce4
--- /dev/null
+++ b/.cursorrules
@@ -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.
diff --git a/company_site/financial/forms.py b/company_site/financial/forms.py
index 1d5a277..7d8968c 100644
--- a/company_site/financial/forms.py
+++ b/company_site/financial/forms.py
@@ -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"]
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/company_site/financial/migrations/0006_chargenumber_created_by_and_more.py b/company_site/financial/migrations/0006_chargenumber_created_by_and_more.py
new file mode 100644
index 0000000..eee5efe
--- /dev/null
+++ b/company_site/financial/migrations/0006_chargenumber_created_by_and_more.py
@@ -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),
+ ),
+ ]
diff --git a/company_site/financial/migrations/0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more.py b/company_site/financial/migrations/0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more.py
new file mode 100644
index 0000000..abb4c0c
--- /dev/null
+++ b/company_site/financial/migrations/0007_remove_contract_financial_contract_contract_type_contracttypeenum_and_more.py
@@ -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'),
+ ),
+ ]
diff --git a/company_site/financial/migrations/0008_alter_chargenumber_slug_alter_contract_slug_and_more.py b/company_site/financial/migrations/0008_alter_chargenumber_slug_alter_contract_slug_and_more.py
new file mode 100644
index 0000000..9cfe100
--- /dev/null
+++ b/company_site/financial/migrations/0008_alter_chargenumber_slug_alter_contract_slug_and_more.py
@@ -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),
+ ),
+ ]
diff --git a/company_site/financial/migrations/0009_timecardcell_end_time_timecardcell_start_time.py b/company_site/financial/migrations/0009_timecardcell_end_time_timecardcell_start_time.py
new file mode 100644
index 0000000..cc11e60
--- /dev/null
+++ b/company_site/financial/migrations/0009_timecardcell_end_time_timecardcell_start_time.py
@@ -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),
+ ),
+ ]
diff --git a/company_site/financial/migrations/0010_alter_employee_manager_alter_employee_phonenumber.py b/company_site/financial/migrations/0010_alter_employee_manager_alter_employee_phonenumber.py
new file mode 100644
index 0000000..2caa783
--- /dev/null
+++ b/company_site/financial/migrations/0010_alter_employee_manager_alter_employee_phonenumber.py
@@ -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),
+ ),
+ ]
diff --git a/company_site/financial/models.py b/company_site/financial/models.py
index 63477de..6c51cec 100644
--- a/company_site/financial/models.py
+++ b/company_site/financial/models.py
@@ -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)
diff --git a/company_site/financial/templates/financial/contract_detail.html b/company_site/financial/templates/financial/contract_detail.html
index d403277..f18b4c0 100644
--- a/company_site/financial/templates/financial/contract_detail.html
+++ b/company_site/financial/templates/financial/contract_detail.html
@@ -1,56 +1,65 @@
-
-
-
+{% extends "base.html" %}
{% load static %}
-
-
- Contract Detail
-
-
-
+{% block title %}Contract Detail - AI ML Operations{% endblock %}
-
+{% block content %}
+
+
+
{% if is_new %}
-
New Contract
-
+
New Contract
+
+
+
{% else %}
-
{{ contract.name }}
-
- {% endif %}
+
{{ contract.name }}
+
+
+
+
-
-
-
-
-
- {% if is_new %}
- {% else %}
-
Charge Numbers
+
Charge Numbers
{% if charge_numbes %}
-
put charge number table here
-
Create a new charge number
- {{ charge_number_form }}
+
+
put charge number table here
+
+
+
+
Create a new charge number
+
+
{% else %}
-
There are no charge numbers for this contract
-
- {% endif %}
+
+
There are no charge numbers for this contract.
+
+
+
{% endif %}
-
-
\ No newline at end of file
+ {% endif %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/contracts.html b/company_site/financial/templates/financial/contracts.html
index d4fcfa7..d0c151d 100644
--- a/company_site/financial/templates/financial/contracts.html
+++ b/company_site/financial/templates/financial/contracts.html
@@ -1,50 +1,151 @@
-
-
-
+{% extends "base.html" %}
{% load static %}
-
-
- Profile
-
-
-
+{% block title %}Contracts - AI ML Operations{% endblock %}
-
-
-
- Contracts
- {% if contracts %}
-
-
- Name
- Identifier
- Type
- Start Date
- End Date
- Proposed Amount
- Baseline Amount
- Funded Amount
-
-
- {% for contract in contracts %}
-
- {{ contract.name }}
- {{ contract.slug }}
- {{ contract.contract_type }}
- {{ contract.baseline_start }}
- {{ contract.baseline_end }}
- {{ contract.proposed_amount }}
- {{ contract.baseline_amount }}
- {{ contract.funded_amount }}
-
+{% block content %}
+
+
+
+ {% if chart_data_json %}
+
Burn Rate Charts
+
+ {% for c in contracts %}
+
+
+
{% endfor %}
-
-
- Create new contract.
- {% else %}
- There are no contracts. Please make one
-
+
{% endif %}
-
\ No newline at end of file
+
+
+ {% if contracts %}
+
+
+
+ Name
+ Identifier
+ Type
+ Start Date
+ End Date
+ Proposed Amount
+ Budget Hours
+ Hours Charged
+ Money Spent
+ Money Remaining
+ Projected End Date
+
+
+
+ {% for contract in contracts %}
+
+ {{ contract.name }}
+ {{ contract.slug }}
+ {{ contract.contract_type }}
+ {{ contract.baseline_start }}
+ {{ contract.baseline_end }}
+ {{ contract.proposed_amount }}
+ {{ contract.budget_hours }}
+ {{ contract.total_hours_spent }}
+ ${{ contract.total_money_spent|floatformat:2 }}
+ ${{ contract.remaining_money|floatformat:2 }}
+ {{ contract.projected_end_date }}
+
+ {% endfor %}
+
+
+
+
Create a new contract .
+ {% else %}
+
There are no contracts. Please make one .
+ {% endif %}
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/edit_time_log.html b/company_site/financial/templates/financial/edit_time_log.html
new file mode 100644
index 0000000..cdfe4ca
--- /dev/null
+++ b/company_site/financial/templates/financial/edit_time_log.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}Edit Time Log - AI ML Operations{% endblock %}
+
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/index.html b/company_site/financial/templates/financial/index.html
index 39fbef7..15467f3 100644
--- a/company_site/financial/templates/financial/index.html
+++ b/company_site/financial/templates/financial/index.html
@@ -1,118 +1,95 @@
-
+{% extends "base.html" %}
{% load static %}
-
-
- Accounting
-
-
-
-
- {% endif %}
- {% if is_worker %}
-
-
-
-
-
-
put picture here
-
-
-
- {% endif %}
- {% if is_manager %}
-
- {% endif %}
- {% if is_procurment_officer %}
-
- {% endif %}
- {% if is_finance %}
-
- {% endif %}
-
-
-
- Accouting
-
\ No newline at end of file
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/new_employee.html b/company_site/financial/templates/financial/new_employee.html
new file mode 100644
index 0000000..1176803
--- /dev/null
+++ b/company_site/financial/templates/financial/new_employee.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}New Employee - AI ML Operations{% endblock %}
+
+{% block content %}
+
+
+
+
+
New Employee
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/not_created.html b/company_site/financial/templates/financial/not_created.html
index fad4fcc..68c883b 100644
--- a/company_site/financial/templates/financial/not_created.html
+++ b/company_site/financial/templates/financial/not_created.html
@@ -1 +1,13 @@
- this page has not been created yet
\ No newline at end of file
+{% extends "base.html" %}
+
+{% block title %}Page Not Found{% endblock %}
+
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/profile.html b/company_site/financial/templates/financial/profile.html
index 3960525..2eb37a5 100644
--- a/company_site/financial/templates/financial/profile.html
+++ b/company_site/financial/templates/financial/profile.html
@@ -1,18 +1,19 @@
-
-
-
+{% extends "base.html" %}
{% load static %}
-
-
- Profile
-
-
-
+{% block title %}Profile - AI ML Operations{% endblock %}
-
-
-
- Profile
- {{ form }}
-
\ No newline at end of file
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/reports.html b/company_site/financial/templates/financial/reports.html
new file mode 100644
index 0000000..1213f98
--- /dev/null
+++ b/company_site/financial/templates/financial/reports.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}Client Reports - AI ML Operations{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ {% if contracts %}
+
+
+
+ Contract Name
+ Total Budget (Hours)
+ Total Logged (Hours)
+ Remaining Budget (Hours)
+
+
+
+ {% for c in contracts %}
+
+ {{ c.name }}
+ {{ c.budget_hours }}
+ {{ c.total_logged }}
+ {{
+ c.remaining_budget }}
+
+ {% endfor %}
+
+
+ {% else %}
+
No data available to report.
+ {% endif %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/time_logs.html b/company_site/financial/templates/financial/time_logs.html
new file mode 100644
index 0000000..0386475
--- /dev/null
+++ b/company_site/financial/templates/financial/time_logs.html
@@ -0,0 +1,64 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}Time Logs - AI ML Operations{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ Employee
+ Contract
+ Date
+ Start Time
+ End Time
+ Duration (hrs)
+ Actions
+
+
+
+ {% for log in logs %}
+
+ {{ log.timeCard.employee }}
+ {{ log.contract }}
+ {{ log.date }}
+ {{ log.start_time|default_if_none:"" }}
+ {{ log.end_time|default_if_none:"" }}
+ {{ log.hour }}
+
+ Edit
+
+
+
+ {% empty %}
+
+ No time
+ logs found.
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/templates/financial/timekeeping.html b/company_site/financial/templates/financial/timekeeping.html
new file mode 100644
index 0000000..08159a8
--- /dev/null
+++ b/company_site/financial/templates/financial/timekeeping.html
@@ -0,0 +1,31 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}Timekeeping - AI ML Operations{% endblock %}
+
+{% block content %}
+
+
+
+
+
Log Time
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/company_site/financial/urls.py b/company_site/financial/urls.py
index 069db3f..c09a196 100644
--- a/company_site/financial/urls.py
+++ b/company_site/financial/urls.py
@@ -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//edit", views.edit_time_log, name="edit_time_log"),
+ path("time_logs//delete", views.delete_time_log, name="delete_time_log"),
path("timeapproval", views.timeapproval, name="Timeapproval"),
path("contracts", views.contracts, name="contracts"),
path("/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("/update_charge_number", views.update_charge_number, name="update_charge_number"),
#path("contracts//", 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"),
]
\ No newline at end of file
diff --git a/company_site/financial/views.py b/company_site/financial/views.py
index edc7631..69ae737 100644
--- a/company_site/financial/views.py
+++ b/company_site/financial/views.py
@@ -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):
\ No newline at end of file
+ return render(request, 'financial/profile.html', {'form': form})
\ No newline at end of file
diff --git a/company_site/public/static/public/css/style.css b/company_site/public/static/public/css/style.css
index a170904..764b028 100644
--- a/company_site/public/static/public/css/style.css
+++ b/company_site/public/static/public/css/style.css
@@ -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;
}
\ No newline at end of file