diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..37a7b09 --- /dev/null +++ b/ai/__init__.py @@ -0,0 +1,3 @@ +"""Helpers for interacting with the Flatlogic AI proxy from Django code.""" + +from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401 diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py new file mode 100644 index 0000000..bc1af27 --- /dev/null +++ b/ai/local_ai_api.py @@ -0,0 +1,282 @@ +""" +LocalAIApi — lightweight Python client for the Flatlogic AI proxy. + +Usage (inside the Django workspace): + + from ai.local_ai_api import LocalAIApi + + response = LocalAIApi.create_response({ + "input": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Summarise this text in two sentences."}, + ], + "text": {"format": {"type": "json_object"}}, + }) + + if response.get("success"): + data = LocalAIApi.decode_json_from_response(response) + # ... + +The helper automatically injects the project UUID header and falls back to +reading executor/.env if environment variables are missing. +""" + +from __future__ import annotations + +import json +import os +import ssl +from typing import Any, Dict, Iterable, Optional +from urllib import error as urlerror +from urllib import request as urlrequest + +__all__ = [ + "LocalAIApi", + "create_response", + "request", + "decode_json_from_response", +] + + +_CONFIG_CACHE: Optional[Dict[str, Any]] = None + + +class LocalAIApi: + """Static helpers mirroring the PHP implementation.""" + + @staticmethod + def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return create_response(params, options or {}) + + @staticmethod + def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, + options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return request(path, payload or {}, options or {}) + + @staticmethod + def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + return decode_json_from_response(response) + + +def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Signature compatible with the OpenAI Responses API.""" + options = options or {} + payload = dict(params) + + if not isinstance(payload.get("input"), list) or not payload["input"]: + return { + "success": False, + "error": "input_missing", + "message": 'Parameter "input" is required and must be a non-empty list.', + } + + cfg = _config() + if not payload.get("model"): + payload["model"] = cfg["default_model"] + + return request(options.get("path"), payload, options) + + +def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Perform a raw request to the AI proxy.""" + cfg = _config() + options = options or {} + + resolved_path = path or options.get("path") or cfg["responses_path"] + if not resolved_path: + return { + "success": False, + "error": "project_id_missing", + "message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", + } + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting AI request.", + } + + if "project_uuid" not in payload and project_uuid: + payload["project_uuid"] = project_uuid + + url = _build_url(resolved_path, cfg["base_url"]) + timeout = int(options.get("timeout", cfg["timeout"])) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urlrequest.Request(url, data=body, method="POST") + for name, value in headers.items(): + req.add_header(name, value) + + context = None + if not verify_tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + try: + with urlrequest.urlopen(req, timeout=timeout, context=context) as resp: + status = resp.getcode() + response_body = resp.read().decode("utf-8", errors="replace") + except urlerror.HTTPError as exc: + status = exc.getcode() + response_body = exc.read().decode("utf-8", errors="replace") + except Exception as exc: # pylint: disable=broad-except + return { + "success": False, + "error": "request_failed", + "message": str(exc), + } + + decoded = None + if response_body: + try: + decoded = json.loads(response_body) + except json.JSONDecodeError: + decoded = None + + if 200 <= status < 300: + return { + "success": True, + "status": status, + "data": decoded if decoded is not None else response_body, + } + + error_message = "AI proxy request failed" + if isinstance(decoded, dict): + error_message = decoded.get("error") or decoded.get("message") or error_message + elif response_body: + error_message = response_body + + return { + "success": False, + "status": status, + "error": error_message, + "response": decoded if decoded is not None else response_body, + } + + +def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Attempt to decode JSON emitted by the model (handles markdown fences).""" + text = _extract_text(response) + if text == "": + return None + + try: + decoded = json.loads(text) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + pass + + stripped = text.strip() + if stripped.startswith("```json"): + stripped = stripped[7:] + if stripped.endswith("```"): + stripped = stripped[:-3] + stripped = stripped.strip() + if stripped and stripped != text: + try: + decoded = json.loads(stripped) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + return None + return None + + +def _extract_text(response: Dict[str, Any]) -> str: + payload = response.get("data") if response.get("success") else response.get("response") + if isinstance(payload, dict): + output = payload.get("output") + if isinstance(output, list): + combined = "" + for item in output: + content = item.get("content") if isinstance(item, dict) else None + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"): + combined += str(block["text"]) + if combined: + return combined + choices = payload.get("choices") + if isinstance(choices, list) and choices: + message = choices[0].get("message") + if isinstance(message, dict) and message.get("content"): + return str(message["content"]) + if isinstance(payload, str): + return payload + return "" + + +def _config() -> Dict[str, Any]: + global _CONFIG_CACHE # noqa: PLW0603 + if _CONFIG_CACHE is not None: + return _CONFIG_CACHE + + _ensure_env_loaded() + + base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com") + project_id = os.getenv("PROJECT_ID") or None + responses_path = os.getenv("AI_RESPONSES_PATH") + if not responses_path and project_id: + responses_path = f"/projects/{project_id}/ai-request" + + _CONFIG_CACHE = { + "base_url": base_url, + "responses_path": responses_path, + "project_id": project_id, + "project_uuid": os.getenv("PROJECT_UUID"), + "project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"), + "default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5"), + "timeout": int(os.getenv("AI_TIMEOUT", "30")), + "verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"}, + } + return _CONFIG_CACHE + + +def _build_url(path: str, base_url: str) -> str: + trimmed = path.strip() + if trimmed.startswith("http://") or trimmed.startswith("https://"): + return trimmed + if trimmed.startswith("/"): + return f"{base_url}{trimmed}" + return f"{base_url}/{trimmed}" + + +def _ensure_env_loaded() -> None: + """Populate os.environ from executor/.env if variables are missing.""" + if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"): + return + + env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env")) + if not os.path.exists(env_path): + return + + try: + with open(env_path, "r", encoding="utf-8") as handle: + for line in handle: + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + key = key.strip() + value = value.strip().strip('\'"') + if key and not os.getenv(key): + os.environ[key] = value + except OSError: + pass diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 3d6501c..0bce513 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index dadfaa7..5c3cab0 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 139db10..7898252 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 79ce690..0dd7252 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 3b7774e..cd7f0f2 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e41572..ea472c8 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6435d92..8020c36 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index f6e5c4e..ed1e061 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 5b41fe1..cc55955 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 4e4f113..5b5bea9 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 9d0ddd8..e0828b2 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 639ff3a..5246405 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,10 @@ from django.contrib import admin -from .models import Ticket +from .models import Company, Membership, Tender, Bid, Document, Note, Approval -@admin.register(Ticket) -class TicketAdmin(admin.ModelAdmin): - list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at') - list_filter = ('status', 'priority') - search_fields = ('subject', 'requester_email', 'description') +admin.site.register(Company) +admin.site.register(Membership) +admin.site.register(Tender) +admin.site.register(Bid) +admin.site.register(Document) +admin.site.register(Note) +admin.site.register(Approval) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index 7a6b83b..fe2e8d2 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,7 +1,22 @@ from django import forms -from .models import Ticket +from django.contrib.auth.forms import UserCreationForm +from .models import Company, Tender, Bid -class TicketForm(forms.ModelForm): +class SignUpForm(UserCreationForm): + class Meta(UserCreationForm.Meta): + fields = UserCreationForm.Meta.fields + ('email', 'first_name', 'last_name',) + +class CompanyForm(forms.ModelForm): class Meta: - model = Ticket - fields = ['subject', 'requester_email', 'priority', 'description'] + model = Company + fields = ['name'] + +class TenderForm(forms.ModelForm): + class Meta: + model = Tender + fields = ['title', 'description', 'deadline'] + +class BidForm(forms.ModelForm): + class Meta: + model = Bid + fields = ['amount'] diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 31c6f06..c563d3f 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,5 +1,7 @@ -# Generated by Django 5.2.7 on 2025-10-23 10:09 +# Generated by Django 5.2.7 on 2025-11-15 18:48 +import django.db.models.deletion +from django.conf import settings from django.db import migrations, models @@ -8,20 +10,89 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Ticket', + name='Bid', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('subject', models.CharField(max_length=255)), - ('status', models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('closed', 'Closed')], default='open', max_length=20)), - ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=20)), - ('requester_email', models.EmailField(max_length=254)), - ('description', models.TextField()), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ], ), + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Approval', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=20)), + ('comments', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('approver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('bid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.bid')), + ], + ), + migrations.AddField( + model_name='bid', + name='company', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company'), + ), + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('owner', 'Owner'), ('admin', 'Admin'), ('member', 'Member')], max_length=20)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Tender', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('deadline', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company')), + ], + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('note', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('tender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.tender')), + ], + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='documents/')), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('bid', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.bid')), + ('tender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.tender')), + ], + ), + migrations.AddField( + model_name='bid', + name='tender', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.tender'), + ), ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc index 64d8a55..9c758ad 100644 Binary files a/core/migrations/__pycache__/0001_initial.cpython-311.pyc and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 58b1c14..893d602 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 78b60d1..f4cc691 100644 --- a/core/models.py +++ b/core/models.py @@ -1,25 +1,78 @@ from django.db import models +from django.contrib.auth.models import User -class Ticket(models.Model): - STATUS_CHOICES = [ - ('open', 'Open'), - ('in_progress', 'In Progress'), - ('closed', 'Closed'), - ] - - PRIORITY_CHOICES = [ - ('low', 'Low'), - ('medium', 'Medium'), - ('high', 'High'), - ] - - subject = models.CharField(max_length=255) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') - priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium') - requester_email = models.EmailField() - description = models.TextField() +class Company(models.Model): + name = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return self.subject \ No newline at end of file + return self.name + +class Membership(models.Model): + ROLE_CHOICES = [ + ('owner', 'Owner'), + ('admin', 'Admin'), + ('member', 'Member'), + ] + user = models.ForeignKey(User, on_delete=models.CASCADE) + company = models.ForeignKey(Company, on_delete=models.CASCADE) + role = models.CharField(max_length=20, choices=ROLE_CHOICES) + + def __str__(self): + return f"{self.user.username} - {self.company.name} ({self.role})" + +class Tender(models.Model): + company = models.ForeignKey(Company, on_delete=models.CASCADE) + title = models.CharField(max_length=255) + description = models.TextField() + deadline = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + +class Bid(models.Model): + tender = models.ForeignKey(Tender, on_delete=models.CASCADE) + company = models.ForeignKey(Company, on_delete=models.CASCADE) + amount = models.DecimalField(max_digits=10, decimal_places=2) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Bid for {self.tender.title} by {self.company.name}" + +class Document(models.Model): + tender = models.ForeignKey(Tender, on_delete=models.CASCADE, null=True, blank=True) + bid = models.ForeignKey(Bid, on_delete=models.CASCADE, null=True, blank=True) + file = models.FileField(upload_to='documents/') + uploaded_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.file.name + +class Note(models.Model): + tender = models.ForeignKey(Tender, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + note = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Note on {self.tender.title} by {self.user.username}" + +class Approval(models.Model): + STATUS_CHOICES = [ + ('pending', 'Pending'), + ('approved', 'Approved'), + ('rejected', 'Rejected'), + ] + bid = models.ForeignKey(Bid, on_delete=models.CASCADE) + approver = models.ForeignKey(User, on_delete=models.CASCADE) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + comments = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Approval for {self.bid} by {self.approver.username}" diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..5c9e980 --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,56 @@ +{% load static %} + + + + + + {% block title %}BID Master{% endblock %} + + + + + + {% block head %}{% endblock %} + + + +
+ +
+ +
+ {% block content %}{% endblock %} +
+ + + + diff --git a/core/templates/core/article_detail.html b/core/templates/core/article_detail.html new file mode 100644 index 0000000..8820990 --- /dev/null +++ b/core/templates/core/article_detail.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}{{ article.title }}{% endblock %} + +{% block content %} +
+

{{ article.title }}

+

Published on {{ article.created_at|date:"F d, Y" }}

+
+
+ {{ article.content|safe }} +
+
+{% endblock %} diff --git a/core/templates/core/create_bid.html b/core/templates/core/create_bid.html new file mode 100644 index 0000000..69f6785 --- /dev/null +++ b/core/templates/core/create_bid.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Create a Bid for {{ tender.title }}

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/create_company.html b/core/templates/core/create_company.html new file mode 100644 index 0000000..cab2461 --- /dev/null +++ b/core/templates/core/create_company.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Create a Company

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/create_tender.html b/core/templates/core/create_tender.html new file mode 100644 index 0000000..93ac6d8 --- /dev/null +++ b/core/templates/core/create_tender.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Create a New Tender for {{ company.name }}

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/dashboard.html b/core/templates/core/dashboard.html new file mode 100644 index 0000000..4b2c203 --- /dev/null +++ b/core/templates/core/dashboard.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Welcome, {{ user.username }}!

+ + {% if companies %} +

Your Companies:

+
+ {% for company in companies %} + + {{ company.name }} + + {% endfor %} +
+ {% else %} +
+

You don't have a company yet.

+

Get started by creating a new company.

+ Create a Company +
+ {% endif %} + +
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index f4e4991..b05b8aa 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,157 +1,48 @@ - - +{% extends "base.html" %} - - - - {{ project_name }} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… +
+

Key Features

+
+
+
+
+ +
+

Tender Discovery

+

Find relevant tenders with our powerful search and filtering capabilities.

+
+
+
+
+
+ +
+

Document Management

+

Organize and manage all your tender-related documents in one place.

+
+
+
+
+
+ +
+

Collaboration

+

Collaborate with your team and track progress with our workflow tools.

+
-

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

-

This page will refresh automatically as the plan is implemented.

-

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

- -
- - - \ No newline at end of file + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/login.html b/core/templates/core/login.html new file mode 100644 index 0000000..80099a5 --- /dev/null +++ b/core/templates/core/login.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Login

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/signup.html b/core/templates/core/signup.html new file mode 100644 index 0000000..d0ef6e8 --- /dev/null +++ b/core/templates/core/signup.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Sign Up

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/tender_detail.html b/core/templates/core/tender_detail.html new file mode 100644 index 0000000..b0f1fa3 --- /dev/null +++ b/core/templates/core/tender_detail.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

{{ tender.title }}

+
+
+

Company: {{ tender.company.name }}

+

Description:

+

{{ tender.description }}

+

Deadline: {{ tender.deadline }}

+
+ +
+ +
+
+

Bids

+ Create Bid +
+
+ {% if bids %} +
    + {% for bid in bids %} +
  • + {{ bid.company.name }} - ${{ bid.amount }} +
  • + {% endfor %} +
+ {% else %} +

No bids yet.

+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/tender_list.html b/core/templates/core/tender_list.html new file mode 100644 index 0000000..5d4c84a --- /dev/null +++ b/core/templates/core/tender_list.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Tenders for {{ company.name }}

+ Create Tender +
+ + {% if tenders %} + + {% else %} +
+

No tenders yet.

+

Get started by creating a new tender.

+
+ {% endif %} +
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..4f0e941 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,16 @@ from django.urls import path - -from .views import home +from django.contrib.auth import views as auth_views +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('signup/', views.signup, name='signup'), + path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'), + path('logout/', views.logout_view, name='logout'), + path('dashboard/', views.dashboard, name='dashboard'), + path('create_company/', views.create_company, name='create_company'), + path('company//tenders/', views.tender_list, name='tender_list'), + path('tender//', views.tender_detail, name='tender_detail'), + path('company//create_tender/', views.create_tender, name='create_tender'), + path('tender//create_bid/', views.create_bid, name='create_bid'), + path('', views.home, name='home'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c1a6d45..1432241 100644 --- a/core/views.py +++ b/core/views.py @@ -1,37 +1,110 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render -from django.urls import reverse_lazy -from django.utils import timezone -from django.views.generic.edit import CreateView - -from .forms import TicketForm -from .models import Ticket - +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth import login, authenticate, logout +from django.contrib.auth.decorators import login_required +from .forms import SignUpForm, CompanyForm, TenderForm, BidForm +from .models import Company, Membership, Tender, Bid def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + return render(request, "core/index.html") +@login_required +def dashboard(request): + memberships = Membership.objects.filter(user=request.user) + companies = [m.company for m in memberships] context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + 'companies': companies } - return render(request, "core/index.html", context) + return render(request, "core/dashboard.html", context) +def signup(request): + if request.method == 'POST': + form = SignUpForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user) + return redirect('dashboard') + else: + form = SignUpForm() + return render(request, 'core/signup.html', {'form': form}) + +def logout_view(request): + logout(request) + return redirect('home') + +@login_required +def create_company(request): + if request.method == 'POST': + form = CompanyForm(request.POST) + if form.is_valid(): + company = form.save() + Membership.objects.create(user=request.user, company=company, role='owner') + return redirect('dashboard') + else: + form = CompanyForm() + return render(request, 'core/create_company.html', {'form': form}) + +@login_required +def tender_list(request, company_id): + company = get_object_or_404(Company, pk=company_id) + tenders = Tender.objects.filter(company=company) + context = { + 'company': company, + 'tenders': tenders + } + return render(request, 'core/tender_list.html', context) + +@login_required +def tender_detail(request, tender_id): + tender = get_object_or_404(Tender, pk=tender_id) + bids = Bid.objects.filter(tender=tender) + context = { + 'tender': tender, + 'bids': bids + } + return render(request, 'core/tender_detail.html', context) + +@login_required +def create_tender(request, company_id): + company = get_object_or_404(Company, pk=company_id) + if request.method == 'POST': + form = TenderForm(request.POST) + if form.is_valid(): + tender = form.save(commit=False) + tender.company = company + tender.save() + return redirect('tender_list', company_id=company.id) + else: + form = TenderForm() + context = { + 'form': form, + 'company': company + } + return render(request, 'core/create_tender.html', context) + +@login_required +def create_bid(request, tender_id): + tender = get_object_or_404(Tender, pk=tender_id) + membership = Membership.objects.filter(user=request.user).first() + if not membership: + # Handle case where user has no company + return redirect('dashboard') # Or show an error + + company = membership.company + + if request.method == 'POST': + form = BidForm(request.POST) + if form.is_valid(): + bid = form.save(commit=False) + bid.tender = tender + bid.company = company + bid.save() + return redirect('tender_detail', tender_id=tender.id) + else: + form = BidForm() + + context = { + 'form': form, + 'tender': tender + } + return render(request, 'core/create_bid.html', context) -class TicketCreateView(CreateView): - model = Ticket - form_class = TicketForm - template_name = "core/ticket_create.html" - success_url = reverse_lazy("home") diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..95890c0 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,63 @@ + +body { + background-color: #f8f9fa; + color: #333; + font-family: 'Inter', sans-serif; +} + +.header { + border-bottom: 1px solid #e9ecef; + background-color: #fff; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.hero-section { + padding: 4rem 0; + text-align: center; + background-color: #fff; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; + color: #212529; +} + +.hero-section .lead { + font-size: 1.25rem; + color: #6c757d; +} + +.hero-section .btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; + font-size: 1.25rem; + padding: 0.75rem 1.5rem; +} + +.features-section { + padding: 4rem 0; +} + +.features-section h2 { + text-align: center; + margin-bottom: 3rem; + font-size: 2.5rem; + font-weight: 700; +} + +.feature-card { + text-align: center; + padding: 2rem; + border: 1px solid #e9ecef; + border-radius: 0.5rem; + background-color: #fff; + margin-bottom: 2rem; + height: 100%; +} + +.feature-icon { + font-size: 3rem; + color: #0d6efd; + margin-bottom: 1rem; +} diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..95890c0 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,63 @@ -: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); -} body { - margin: 0; + background-color: #f8f9fa; + color: #333; 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; +} + +.header { + border-bottom: 1px solid #e9ecef; + background-color: #fff; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.hero-section { + padding: 4rem 0; + text-align: center; + background-color: #fff; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; + color: #212529; +} + +.hero-section .lead { + font-size: 1.25rem; + color: #6c757d; +} + +.hero-section .btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; + font-size: 1.25rem; + padding: 0.75rem 1.5rem; +} + +.features-section { + padding: 4rem 0; +} + +.features-section h2 { + text-align: center; + margin-bottom: 3rem; + font-size: 2.5rem; + font-weight: 700; +} + +.feature-card { + text-align: center; + padding: 2rem; + border: 1px solid #e9ecef; + border-radius: 0.5rem; + background-color: #fff; + margin-bottom: 2rem; + height: 100%; +} + +.feature-icon { + font-size: 3rem; + color: #0d6efd; + margin-bottom: 1rem; }