Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
561398a333 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
39
core/migrations/0001_initial.py
Normal file
39
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-23 09:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Job',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('job_number', models.CharField(editable=False, max_length=20, unique=True)),
|
||||||
|
('client_name', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField(blank=True)),
|
||||||
|
('status', models.CharField(choices=[('draft', 'Draft'), ('active', 'Active'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='draft', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Agreement',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('agreement_date', models.DateField(default=django.utils.timezone.now)),
|
||||||
|
('terms', models.TextField()),
|
||||||
|
('scope_of_work', models.TextField()),
|
||||||
|
('total_value', models.DecimalField(decimal_places=2, default=0.0, max_digits=12)),
|
||||||
|
('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='agreement', to='core.job')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -1,3 +1,44 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Create your models here.
|
class Job(models.Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('active', 'Active'),
|
||||||
|
('completed', 'Completed'),
|
||||||
|
('cancelled', 'Cancelled'),
|
||||||
|
]
|
||||||
|
|
||||||
|
job_number = models.CharField(max_length=20, unique=True, editable=False)
|
||||||
|
client_name = models.CharField(max_length=255)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.job_number:
|
||||||
|
last_job = Job.objects.order_by('-id').first()
|
||||||
|
if last_job and last_job.job_number.startswith('AIS'):
|
||||||
|
try:
|
||||||
|
last_num = int(last_job.job_number[3:])
|
||||||
|
new_num = last_num + 1
|
||||||
|
except ValueError:
|
||||||
|
new_num = 1
|
||||||
|
else:
|
||||||
|
new_num = 1
|
||||||
|
self.job_number = f"AIS{new_num:04d}"
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.job_number} - {self.client_name}"
|
||||||
|
|
||||||
|
class Agreement(models.Model):
|
||||||
|
job = models.OneToOneField(Job, on_delete=models.CASCADE, related_name='agreement')
|
||||||
|
agreement_date = models.DateField(default=timezone.now)
|
||||||
|
terms = models.TextField()
|
||||||
|
scope_of_work = models.TextField()
|
||||||
|
total_value = models.DecimalField(max_digits=12, decimal_places=2, default=0.00)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Agreement for {self.job.job_number}"
|
||||||
@ -1,25 +1,54 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% if project_description %}
|
<title>{% block title %}Aitken Spence Industrial Solutions{% endblock %}</title>
|
||||||
<meta name="description" content="{{ project_description }}">
|
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<!-- Google Fonts -->
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
{% endif %}
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
{% if project_image_url %}
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=Outfit:wght@500;700&display=swap" rel="stylesheet">
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<!-- Bootstrap 5 -->
|
||||||
{% endif %}
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v=1.0">
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<nav class="navbar navbar-expand-lg navbar-dark mb-4 no-print">
|
||||||
</body>
|
<div class="container">
|
||||||
|
<a class="navbar-brand fw-bold" href="{% url 'home' %}">AIS PORTAL</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'home' %}">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'job_list' %}">Jobs</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link btn btn-accent ms-lg-2 px-3" href="{% url 'job_create' %}">+ New Job</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="container mt-5 py-4 border-top no-print">
|
||||||
|
<p class="text-center text-muted">© 2026 Aitken Spence Industrial Solutions. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
110
core/templates/core/agreement_print.html
Normal file
110
core/templates/core/agreement_print.html
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
.print-agreement {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 40px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.agreement-brand {
|
||||||
|
color: #003366;
|
||||||
|
border-bottom: 3px solid #003366;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.agreement-meta {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-top: 25px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.signature-box {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.print-agreement {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="no-print text-center mb-4">
|
||||||
|
<div class="alert alert-info d-inline-block">
|
||||||
|
<p class="mb-2"><strong>Print Preview:</strong> This layout is optimized for A4 paper printing.</p>
|
||||||
|
<button onclick="window.print()" class="btn btn-primary">Print Now</button>
|
||||||
|
<a href="{% url 'job_detail' agreement.job.pk %}" class="btn btn-link">Back to Job</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="print-agreement bg-white">
|
||||||
|
<div class="agreement-brand d-flex justify-content-between align-items-end">
|
||||||
|
<div>
|
||||||
|
<h1 class="h2 mb-0">Aitken Spence Industrial Solutions</h1>
|
||||||
|
<p class="small text-muted mb-0">Tower 1, Colombo 02, Sri Lanka</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<h2 class="h5 text-primary mb-0">COMMERCIAL AGREEMENT</h2>
|
||||||
|
<p class="fw-bold mb-0">REF: {{ agreement.job.job_number }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agreement-meta row">
|
||||||
|
<div class="col-6">
|
||||||
|
<p class="small text-muted mb-0">CLIENT</p>
|
||||||
|
<p class="fw-bold fs-5">{{ agreement.job.client_name }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<p class="small text-muted mb-0">DATE</p>
|
||||||
|
<p class="fw-bold fs-5">{{ agreement.agreement_date }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agreement-body">
|
||||||
|
<h3 class="section-title h6">1. Scope of Work</h3>
|
||||||
|
<p>{{ agreement.scope_of_work|linebreaksbr }}</p>
|
||||||
|
|
||||||
|
<h3 class="section-title h6">2. Terms and Conditions</h3>
|
||||||
|
<p>{{ agreement.terms|linebreaksbr }}</p>
|
||||||
|
|
||||||
|
<h3 class="section-title h6">3. Commercial Value</h3>
|
||||||
|
<p class="fs-4 fw-bold">LKR {{ agreement.total_value|floatformat:2 }}</p>
|
||||||
|
<p class="small text-muted">Inclusive of all applicable taxes and industrial surcharges.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="signature-box row mt-5 pt-5">
|
||||||
|
<div class="col-6">
|
||||||
|
<div style="border-top: 1px solid #000; padding-top: 10px;">
|
||||||
|
<p class="small mb-0">On behalf of Aitken Spence</p>
|
||||||
|
<p class="small fw-bold">Authorized Signatory</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<div style="border-top: 1px solid #000; padding-top: 10px;">
|
||||||
|
<p class="small mb-0">On behalf of {{ agreement.job.client_name }}</p>
|
||||||
|
<p class="small fw-bold">Authorized Signatory</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 text-center">
|
||||||
|
<p class="small text-muted italic" style="font-size: 10px;">This is a computer-generated document. {{ agreement.job.job_number }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -1,145 +1,89 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-color-start: #6a11cb;
|
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<div class="row align-items-center mb-5">
|
||||||
<div class="card">
|
<div class="col-lg-6">
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<h1 class="display-4 fw-bold">Industrial Operations Control</h1>
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<p class="lead text-muted">Manage jobs, agreements, and industrial solutions for Aitken Spence with precision and speed.</p>
|
||||||
<span class="sr-only">Loading…</span>
|
<div class="d-flex gap-3">
|
||||||
|
<a href="{% url 'job_create' %}" class="btn btn-primary btn-lg px-4">Create New Job</a>
|
||||||
|
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary btn-lg px-4">View All Jobs</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 d-none d-lg-block">
|
||||||
|
<div class="p-5 bg-white rounded-4 shadow-sm text-center">
|
||||||
|
<!-- Simple SVG Placeholder for Industrial Visual -->
|
||||||
|
<svg width="200" height="200" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="text-primary opacity-50">
|
||||||
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-5">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<h3 class="display-6 fw-bold text-primary">{{ jobs_count }}</h3>
|
||||||
|
<p class="mb-0 text-muted">Total Jobs</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card p-4 text-center border-start border-accent border-4">
|
||||||
|
<h3 class="display-6 fw-bold text-accent">{{ active_jobs }}</h3>
|
||||||
|
<p class="mb-0 text-muted">Active Agreements</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card p-4 text-center">
|
||||||
|
<h3 class="display-6 fw-bold text-primary">100%</h3>
|
||||||
|
<p class="mb-0 text-muted">SLA Compliance</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="h4 mb-0">Recent Job Orders</h2>
|
||||||
|
<a href="{% url 'job_list' %}" class="btn btn-sm btn-link text-decoration-none">View All</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Job ID</th>
|
||||||
|
<th>Client</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for job in recent_jobs %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold text-primary">{{ job.job_number }}</td>
|
||||||
|
<td>{{ job.client_name }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge rounded-pill {% if job.status == 'active' %}bg-success{% elif job.status == 'draft' %}bg-warning text-dark{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ job.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ job.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'job_detail' job.pk %}" class="btn btn-sm btn-outline-primary">Manage</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-4">No jobs found. Start by creating one.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
|
||||||
<p class="runtime">
|
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
|
||||||
</footer>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
71
core/templates/core/job_detail.html
Normal file
71
core/templates/core/job_detail.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Job {{ job.job_number }} - AIS Portal{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card p-4 mb-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="text-uppercase text-muted small fw-bold">Job Profile</span>
|
||||||
|
<h1 class="h2 mb-1 text-primary">{{ job.job_number }}</h1>
|
||||||
|
<p class="h5">{{ job.client_name }}</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-primary px-3 py-2 fs-6">{{ job.get_status_display }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<p class="text-muted small mb-1">Creation Date</p>
|
||||||
|
<p class="fw-bold">{{ job.created_at|date:"d F Y, H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 text-sm-end">
|
||||||
|
<p class="text-muted small mb-1">Last Update</p>
|
||||||
|
<p class="fw-bold">{{ job.updated_at|date:"d F Y, H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3 class="h5 mb-3 mt-4">Job Description</h3>
|
||||||
|
<div class="p-3 bg-light rounded">
|
||||||
|
{{ job.description|linebreaks|default:"No description provided." }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card p-4">
|
||||||
|
<h3 class="h5 mb-4">Agreement Status</h3>
|
||||||
|
{% if job.agreement %}
|
||||||
|
<div class="d-flex align-items-center justify-content-between p-3 border rounded">
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 fw-bold">Commercial Agreement Generated</p>
|
||||||
|
<p class="small text-muted mb-0">Dated: {{ job.agreement.agreement_date }}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'agreement_print' job.pk %}" class="btn btn-outline-primary">Preview & Print</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<p class="text-muted">No agreement linked to this job yet.</p>
|
||||||
|
<button class="btn btn-sm btn-primary">Initialize Agreement</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card p-4 sticky-top" style="top: 20px;">
|
||||||
|
<h3 class="h5 mb-3">Workflow Actions</h3>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-primary" disabled>Update Status</button>
|
||||||
|
<button class="btn btn-outline-secondary" disabled>Edit Details</button>
|
||||||
|
<hr>
|
||||||
|
<button class="btn btn-danger btn-sm opacity-50" disabled>Cancel Job</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 p-3 border-start border-primary border-4 bg-light">
|
||||||
|
<p class="small mb-0 italic">"Ensure all client data matches the physical purchase order before printing the agreement."</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
31
core/templates/core/job_form.html
Normal file
31
core/templates/core/job_form.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}New Job - AIS Portal{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card p-5">
|
||||||
|
<h1 class="h3 mb-4">Register New Industrial Job</h1>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="client_name" class="form-label">Client / Organization Name</label>
|
||||||
|
<input type="text" class="form-control form-control-lg" id="client_name" name="client_name" required placeholder="e.g. Colombo Port Authority">
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="description" class="form-label">Scope Description</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="4" placeholder="Briefly describe the industrial services required..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Create Job Entry</button>
|
||||||
|
<a href="{% url 'job_list' %}" class="btn btn-link text-muted">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-4 p-3 bg-light rounded small">
|
||||||
|
<p class="mb-0 text-muted">Note: Job Number (AISXXXX) will be automatically generated upon submission.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
50
core/templates/core/job_list.html
Normal file
50
core/templates/core/job_list.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}All Jobs - AIS Portal{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h2">Job Inventory</h1>
|
||||||
|
<a href="{% url 'job_create' %}" class="btn btn-primary">+ New Job</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card p-4">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Job ID</th>
|
||||||
|
<th>Client Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Date Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for job in jobs %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold text-primary">{{ job.job_number }}</td>
|
||||||
|
<td>{{ job.client_name }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge rounded-pill {% if job.status == 'active' %}bg-success{% elif job.status == 'draft' %}bg-warning text-dark{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ job.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ job.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{% url 'job_detail' job.pk %}" class="btn btn-sm btn-outline-primary">View</a>
|
||||||
|
<a href="{% url 'agreement_print' job.pk %}" class="btn btn-sm btn-outline-secondary">Print</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-5 text-muted">No jobs available.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from .views import home, job_list, job_create, job_detail, agreement_print
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
path("jobs/", job_list, name="job_list"),
|
||||||
|
path("jobs/new/", job_create, name="job_create"),
|
||||||
|
path("jobs/<int:pk>/", job_detail, name="job_detail"),
|
||||||
|
path("jobs/<int:pk>/print/", agreement_print, name="agreement_print"),
|
||||||
]
|
]
|
||||||
@ -1,25 +1,45 @@
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
from django import get_version as django_version
|
from django import get_version as django_version
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from .models import Job, Agreement
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
"""Modern Dashboard Landing Page"""
|
||||||
host_name = request.get_host().lower()
|
jobs_count = Job.objects.count()
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
active_jobs = Job.objects.filter(status='active').count()
|
||||||
now = timezone.now()
|
recent_jobs = Job.objects.order_by('-created_at')[:5]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "New Style",
|
"project_name": "Aitken Spence Industrial Solutions",
|
||||||
"agent_brand": agent_brand,
|
"jobs_count": jobs_count,
|
||||||
|
"active_jobs": active_jobs,
|
||||||
|
"recent_jobs": recent_jobs,
|
||||||
"django_version": django_version(),
|
"django_version": django_version(),
|
||||||
"python_version": platform.python_version(),
|
"python_version": platform.python_version(),
|
||||||
"current_time": now,
|
|
||||||
"host_name": host_name,
|
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
return render(request, "core/index.html", context)
|
||||||
|
|
||||||
|
def job_list(request):
|
||||||
|
jobs = Job.objects.all().order_by('-created_at')
|
||||||
|
return render(request, "core/job_list.html", {"jobs": jobs})
|
||||||
|
|
||||||
|
def job_create(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
client_name = request.POST.get("client_name")
|
||||||
|
description = request.POST.get("description")
|
||||||
|
job = Job.objects.create(client_name=client_name, description=description)
|
||||||
|
# Automatically create an empty agreement
|
||||||
|
Agreement.objects.create(job=job, terms="Standard Terms Apply", scope_of_work=description)
|
||||||
|
return redirect('job_detail', pk=job.pk)
|
||||||
|
return render(request, "core/job_form.html")
|
||||||
|
|
||||||
|
def job_detail(request, pk):
|
||||||
|
job = get_object_or_404(Job, pk=pk)
|
||||||
|
return render(request, "core/job_detail.html", {"job": job})
|
||||||
|
|
||||||
|
def agreement_print(request, pk):
|
||||||
|
agreement = get_object_or_404(Agreement, job__pk=pk)
|
||||||
|
return render(request, "core/agreement_print.html", {"agreement": agreement})
|
||||||
@ -1,4 +1,73 @@
|
|||||||
/* Custom styles for the application */
|
:root {
|
||||||
body {
|
--primary-color: #003366; /* Deep Industrial Blue */
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
--accent-color: #FF8C00; /* Safety Orange */
|
||||||
|
--bg-color: #F8F9FA;
|
||||||
|
--text-color: #212529;
|
||||||
|
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand, .nav-link {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accent {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-accent:hover {
|
||||||
|
background-color: #e67e00;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styling */
|
||||||
|
@media print {
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.print-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.agreement-header {
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user